shape/accepts.rs
1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use indexmap::IndexSet;
5
6use super::Shape;
7use super::ShapeCase;
8
9/// The `Shape::validate*` methods return a vector of `ShapeMismatch` errors
10/// that is non-empty when validation fails.
11///
12/// Each `ShapeMismatch` error contains the `expected` shape and the `received`
13/// shape, whose metadata (e.g. shape.locations) provide context for the
14/// mismatch.
15///
16/// Conveniently, the `expected`/`received` terminology works for both the
17/// `expected.accepts(received)` and `received.satisfies(expected)` directions
18/// of comparison, so `ShapeMismatch` does not encode whether the `accepts` or
19/// `satisfies` was used internally (though `validate` is similar to `accepts`
20/// in its order of arguments).
21///
22/// Once issue #5 is implemented, you'll be able to obtain source location
23/// information from the `expected` and `received` shapes (instead of adding
24/// additional fields to the `ShapeMismatch` struct).
25#[derive(Debug, PartialEq, Eq, Clone, Hash)]
26pub struct ShapeMismatch {
27 pub expected: Shape,
28 pub received: Shape,
29 pub causes: Vec<ShapeMismatch>,
30}
31
32/// Answers already computed during one top-level [`Shape::validate`] call, so
33/// a question the search asks twice is answered once.
34///
35/// Keyed structurally, on the pair of shapes and the split depth the answer was
36/// computed at. Structural keying is affordable only because each case stores
37/// its hash (see `CaseWithHash`): hashing a key is two field reads rather than
38/// two walks, and the walk is the work the memo exists to avoid. Address
39/// keying was the alternative and is strictly weaker -- it hits only when the
40/// repeated question is a repetition of the *same* allocation, and misses every
41/// shape that was rebuilt rather than cloned, which is common enough to have
42/// turned a measured win into overhead.
43///
44/// The depth belongs in the key because the bound is a path property: an answer
45/// computed with one distribution remaining is not necessarily the answer with
46/// three. Keyed that precisely, a hit is exactly what the search would have
47/// recomputed, so the memo can change how long a comparison takes and never
48/// what it answers. That is the difference between this and the comparison
49/// meter the crate used to carry, whose shared state was itself part of the
50/// answer.
51///
52/// "What it answers" is the verdict. The `ShapeMismatch` a hit returns is a
53/// different matter, because `PartialEq for Shape` ignores metadata: two
54/// occurrences of the same shapes at different source locations share an
55/// entry, and the entry holds whichever pair asked first. The outer frame of a
56/// hit is therefore rebuilt from the shapes in hand (see
57/// [`Shape::validate_within`]), so `expected` and `received` always carry the
58/// locations of the comparison that was actually made. The `causes` below
59/// that frame are not rebuilt: a location one level down on a memo hit may
60/// belong to another structurally identical occurrence of the same pair.
61/// Re-anchoring the whole tree is well defined for the same reason the hit is
62/// sound, and can be done if a consumer comes to need it.
63///
64/// [`MAX_SPLIT_DEPTH`] leans on this memo. Its value of twelve is affordable
65/// only because the member loop's repeated union-against-subtree questions are
66/// answered once; without the memo the nested doomed families take minutes
67/// rather than milliseconds, and the tests that pin the depth cap do not
68/// finish. Weakening the memo therefore hangs the suite rather than failing
69/// it, which is worth knowing before touching either.
70///
71/// Equality is verified rather than assumed: a bucket collision falls through
72/// to `PartialEq for Shape`, which short-circuits on pointer equality before
73/// walking. Trusting the 64-bit hash alone would be faster and is very
74/// probably safe at the few thousand keys one comparison generates, but a type
75/// system should not answer a subtyping question wrongly with any probability
76/// at all.
77///
78/// The map lives on the stack of one `validate` call and is dropped with it,
79/// so nothing is retained between comparisons. It is wrapped in an `Option` so
80/// that a comparison which never meets a union constructs nothing but a null:
81/// an empty `HashMap` does not allocate, but building one is not free either,
82/// and `validate` is called often enough on trivially cheap pairs that a fixed
83/// cost there is measurable (17ns per call when the map was built eagerly).
84type SplitMemo = RefCell<Option<HashMap<MemoKey, Option<ShapeMismatch>>>>;
85
86/// Expected shape, received shape, and the number of distributions already
87/// taken on the path to the comparison.
88type MemoKey = (Shape, Shape, usize);
89
90/// The (expected, received) pairs whose comparison is already in progress on
91/// the path from the top of one `validate` call down to here, recorded at
92/// every hop that resolves a bound [`ShapeCase::Name`] reference: the two
93/// `::Name` arms of `validate_uncached` and the union member deferral loop.
94///
95/// Name resolution is the only recursion in `validate` that does not descend
96/// into a strictly smaller shape: every other arm walks a finite tree, so an
97/// infinite descent must pass through a guarded resolution hop on every lap,
98/// and a lap that arrives at a pair already on this path will repeat
99/// deterministically forever. When such a revisit is detected, the pair is
100/// *assumed* valid and the answer is `None` -- the coinductive hypothesis,
101/// which the enclosing occurrence of the same comparison is still in the
102/// process of discharging. A descent that would find a mismatch finds it
103/// before the pair can recur, since everything after the recurrence is an
104/// exact repetition of what already ran, so the hypothesis never hides
105/// evidence that exists.
106///
107/// What a hypothesis must not do is leak. An answer computed while a
108/// hypothesis is live rests on it, and the hypothesis holds only until the
109/// frame that owns its pair completes -- which may refute it. [`SplitMemo`]
110/// answers questions for the rest of the `validate` call, including from
111/// contexts where a given hypothesis had already been refuted or never
112/// applied, so an answer computed under a live hypothesis must never be
113/// memoized. `lowest_assumed` makes that decidable: it is the smallest path
114/// index of any hypothesis assumed anywhere in the subtree currently being
115/// computed, and because a hypothesis about a pair is discharged when that
116/// pair's own frame pops, it is always either `usize::MAX` (nothing assumed)
117/// or the index of a pair still on the path, meaning the answer in progress
118/// still rests on it. `starved` plays the same role for depth-cap answers
119/// (see [`MAX_VALIDATE_DEPTH`]), which are approximations tuned to the
120/// budget that produced them.
121///
122/// The path holds the shapes themselves rather than their stored hashes:
123/// `PartialEq` compares the hashes first, so each probe of the stack costs
124/// two loads per entry, and keeping the whole pair means the answer never
125/// depends on a 64-bit collision going the right way. A `Vec` rather than a
126/// set because an empty `Vec` never allocates, and a comparison that never
127/// resolves a bound name -- the overwhelming majority -- pays nothing at
128/// all.
129struct Resolving {
130 path: Vec<(Shape, Shape)>,
131 /// Smallest `path` index of any hypothesis assumed in the subtree
132 /// currently being computed; `usize::MAX` when it has assumed none.
133 /// `validate_within` saves, resets, and merges this around each memoized
134 /// subtree, so at memo time it describes exactly that subtree.
135 lowest_assumed: usize,
136 /// Whether the depth cap fired anywhere in the subtree currently being
137 /// computed, saved and merged alongside `lowest_assumed`.
138 starved: bool,
139}
140
141impl Default for Resolving {
142 fn default() -> Self {
143 Resolving {
144 path: Vec::new(),
145 lowest_assumed: usize::MAX,
146 starved: false,
147 }
148 }
149}
150
151type ResolvingPairs = RefCell<Resolving>;
152
153/// Runs `f` with `pair` on the resolving path, or answers `None` -- the
154/// coinductive hypothesis -- if `pair` is already on it.
155///
156/// A hypothesis is scoped to the frame that owns its pair: it is discharged
157/// when that frame pops, because by then the pair's verdict stands on its
158/// own, the frame having either found real evidence or completed the
159/// coinductive proof. So a revisit only records the assumed pair's index in
160/// `lowest_assumed`, and when this frame pops, every hypothesis at its own
161/// index is discharged with it. Indices below it belong to pairs still on
162/// the path (stack discipline means nothing deeper survives to this point),
163/// so those stay live; indices above it cannot occur, because a deeper
164/// pair's pop has already discharged its own hypotheses by the time it
165/// merged its accounting upward.
166fn guarded(
167 resolving: &ResolvingPairs,
168 pair: (Shape, Shape),
169 f: impl FnOnce() -> Option<ShapeMismatch>,
170) -> Option<ShapeMismatch> {
171 let index = {
172 let mut r = resolving.borrow_mut();
173 if let Some(i) = r.path.iter().position(|p| *p == pair) {
174 r.lowest_assumed = r.lowest_assumed.min(i);
175 return None;
176 }
177 r.path.push(pair);
178 r.path.len() - 1
179 };
180 let mismatch = f();
181 let mut r = resolving.borrow_mut();
182 r.path.pop();
183 debug_assert_eq!(r.path.len(), index);
184 // Discharge hypotheses about this pair; keep any about pairs still on
185 // the path above.
186 if r.lowest_assumed >= index {
187 r.lowest_assumed = usize::MAX;
188 }
189 mismatch
190}
191
192/// The deepest `validate_within` recursion one comparison will perform before
193/// declining to look further, returning `None` -- no mismatch found -- for
194/// anything deeper.
195///
196/// This is stack-overflow insurance, not semantics. Every other bound in this
197/// file is a structural fact about the shapes, but this one exists because the
198/// recursion runs on the native call stack, which is finite and shared with
199/// whoever called `validate`. The budget counts `validate_within` frames, not
200/// semantic levels: one level of a shape costs one frame in a plain list,
201/// two in a record, and four to six when a bound `Name` resolves along the
202/// way, so 128 frames covers anywhere from ~128 levels of structural nesting
203/// down to ~25 levels of nullable-named-field chaining.
204///
205/// The value is measured, not guessed. In the debug profile on a 2MiB stack
206/// -- the smallest default stack in play, used by both Rust test threads and
207/// tokio workers -- descent overflows at roughly 300 frames for the fattest
208/// measured family (records chained through `One<Name, null>` fields, the
209/// connectors shape) and roughly 470 for the leanest (nested lists); the
210/// per-frame cost is dominated by this function's own locals and runs
211/// 4.5-9KB. 128 retains at least a 2.3x margin under the worst measurement,
212/// and release frames are several times smaller still. Fully validating a
213/// document at `serde_json`'s default 128-level nesting limit would take
214/// ~256 frames, which 2MiB debug stacks cannot afford; that gap is the
215/// known price of fail-open insurance, and closing it belongs to shrinking
216/// the frame, not raising the budget.
217///
218/// The failure direction at the cap is open, matching what the `::Name` arms
219/// already do for unbound names: a comparison too deep to finish is accepted
220/// rather than reported. A cap high enough to be unreachable in practice is
221/// what makes that acceptable -- it can only fire on inputs no honest schema
222/// or default-configured parser produces, where the alternative is a process
223/// abort. Because a starved answer is an approximation of what a
224/// better-funded comparison would compute, `depth_remaining` is not part of
225/// [`MemoKey`]; instead, starvation is tracked in [`Resolving`], and a
226/// starved answer is never memoized at all. The memo only ever serves what
227/// the search would recompute with the hypothesis slate clean and the budget
228/// intact.
229pub(crate) const MAX_VALIDATE_DEPTH: usize = 128;
230
231impl Shape {
232 /// Returns true if the other shape meets all the expectations of the self
233 /// shape. In set theory terms, the set of all values accepted by other is a
234 /// subset of the set of all values accepted by self.
235 #[must_use]
236 pub fn accepts(&self, other: &Shape) -> bool {
237 self.validate(other).is_none()
238 }
239
240 /// Returns true iff the given [`serde_json::Value`] satisfies self.
241 #[must_use]
242 pub fn accepts_json(&self, json: &serde_json::Value) -> bool {
243 self.accepts(&Shape::from_json(json))
244 }
245
246 /// Returns true iff the given [`serde_json_bytes::Value`] satisfies self.
247 pub fn accepts_json_bytes(&self, json: &serde_json_bytes::Value) -> bool {
248 self.accepts(&Shape::from_json_bytes(json))
249 }
250
251 /// `Shape::validate_json` is to `Shape::validate` as `Shape::accepts_json`
252 /// is to `Shape::accepts`.
253 #[must_use]
254 pub fn validate_json(&self, json: &serde_json::Value) -> Option<ShapeMismatch> {
255 self.validate(&Shape::from_json(json))
256 }
257
258 /// `Shape::validate_json_bytes` is to `Shape::validate` as
259 /// `Shape::accepts_json_bytes` is to `Shape::accepts`.
260 pub fn validate_json_bytes(&self, json: &serde_json_bytes::Value) -> Option<ShapeMismatch> {
261 self.validate(&Shape::from_json_bytes(json))
262 }
263
264 /// Returns `true` if the `self` shape meets all the expectations of the
265 /// `other` shape. In set theory terms, `self` satisfying `other` means the
266 /// set of all values accepted by `self` is a subset of the set of all
267 /// values accepted by `other`.
268 ///
269 /// The `satisfies` method is the inverse of the `accepts` method, in the
270 /// sense that `a.accepts(b)` is equivalent to `b.satisfies(a)`. For
271 /// historical reasons, the bulk of the `accepts`/`satisfies` logic happens
272 /// in the internal `validate` method, though I have since realized
273 /// the `accepts` direction generalizes a bit better to situations where
274 /// `other` is not a `Shape`, such as `Shape::accepts_json(&self, json:
275 /// &serde_json::Value)`.
276 #[must_use]
277 pub fn satisfies(&self, other: &Shape) -> bool {
278 other.validate(self).is_none()
279 }
280
281 /// Validates that all expectations of the `self` shape are met by the
282 /// `other` shape, erroring with a non-empty vector of `ShapeMismatch`
283 /// errors when validation fails.
284 #[must_use]
285 pub fn validate(&self, other: &Shape) -> Option<ShapeMismatch> {
286 // Nothing but a null until a union comparison needs somewhere to put an
287 // answer, so a comparison that never meets a union pays nothing here.
288 let memo = SplitMemo::default();
289 let resolving = ResolvingPairs::default();
290 self.validate_within(other, 0, MAX_VALIDATE_DEPTH, &memo, &resolving)
291 }
292
293 /// The body of [`Shape::validate`], carrying the union splitting search's
294 /// depth.
295 ///
296 /// `splits_taken` counts the union positions distributed along the path
297 /// from the top of this comparison down to here. Every recursion in the
298 /// body passes it along unchanged -- member loops, name resolution, and
299 /// each descent into a field or item -- and only the split recursion
300 /// itself increments it. Descending into a field therefore does not hand
301 /// back a fresh allowance, which is what makes [`MAX_SPLIT_DEPTH`] a bound
302 /// on the whole comparison rather than on each object level separately. A
303 /// per-level bound multiplies: with one union-typed field at each of d
304 /// levels, one level's worst case is paid d times over.
305 ///
306 /// It is a property of the path and never a ledger of work done. Sibling
307 /// comparisons do not spend each other's allowance, so the two fields of
308 /// an object are compared with the same one, and a pair of shapes reached
309 /// by the same number of distributions always answers the same way.
310 ///
311 /// `resolving` is the coinductive cycle guard for name resolution (see
312 /// [`Resolving`]). Like `splits_taken` it is a property of the path:
313 /// every recursion passes it along unchanged. The three hops that resolve
314 /// a bound name -- the two `ShapeCase::Name` arms and the union member
315 /// deferral loop -- push and probe its path through [`guarded`], and the
316 /// memo below consults its hypothesis accounting before inserting.
317 ///
318 /// `depth_remaining` is the stack-overflow insurance budget (see
319 /// [`MAX_VALIDATE_DEPTH`]). Each frame spends one unit on entry, so every
320 /// recursion passes the already-decremented value along unchanged.
321 #[must_use]
322 fn validate_within(
323 &self,
324 other: &Shape,
325 splits_taken: usize,
326 depth_remaining: usize,
327 memo: &SplitMemo,
328 resolving: &ResolvingPairs,
329 ) -> Option<ShapeMismatch> {
330 // Past the depth cap, decline to look further and accept: see
331 // MAX_VALIDATE_DEPTH for the measurement and the fail-open rationale.
332 // Mark the subtree starved so the answer is never memoized for a
333 // query holding more budget.
334 if depth_remaining == 0 {
335 resolving.borrow_mut().starved = true;
336 return None;
337 }
338 let depth_remaining = depth_remaining - 1;
339
340 // Only union comparisons are worth an entry. A union is the only
341 // expected shape whose comparison searches, and it is where a repeated
342 // question arrives: the fan-out that makes nesting expensive is the
343 // member loop asking the same union-versus-subtree question once per
344 // branch of every level above it. Everything else answers in time
345 // proportional to the shapes themselves and would spend more on
346 // bookkeeping than it saved.
347 //
348 // Restricting this further to comparisons inside an active search
349 // (splits_taken > 0) was measured. It leaves the allocation baseline
350 // untouched, but a union of records whose fields are wide unions never
351 // starts a search at all and so never gets an entry: that family goes
352 // from 4.6ms back to 402ms. The entries a top-level union comparison
353 // makes are the ones its own subtree comparisons hit.
354 if !matches!(self.case(), ShapeCase::One(_)) {
355 return self.validate_uncached(other, splits_taken, depth_remaining, memo, resolving);
356 }
357
358 let key: MemoKey = (self.clone(), other.clone(), splits_taken);
359 // The borrow ends with the statement, so the recursion below never runs
360 // while the map is borrowed.
361 if let Some(answer) = memo.borrow().as_ref().and_then(|map| map.get(&key)) {
362 // The key is structural and `PartialEq for Shape` ignores metadata,
363 // so the entry may have been computed for a different occurrence
364 // of these shapes, carrying that occurrence's source locations.
365 // The verdict is the same either way; the shapes on the mismatch
366 // are not, and carrying that metadata is what `ShapeMismatch` is
367 // for. Re-anchor the outer frame on the pair actually in hand.
368 // The `causes` beneath it are kept as computed for the first
369 // occurrence, so a location one level down may still belong to
370 // another structurally identical pair; see the note on
371 // `SplitMemo`.
372 return answer.as_ref().map(|mismatch| ShapeMismatch {
373 expected: self.clone(),
374 received: other.clone(),
375 causes: mismatch.causes.clone(),
376 });
377 }
378
379 // An answer that rests on a live hypothesis or on an exhausted depth
380 // budget is valid only in the context that produced it: serving it
381 // to another occurrence of the same key would hand over an
382 // assumption the asker did not make, or a leniency it did not need.
383 // Account for this subtree alone, and memoize only what the search
384 // would recompute anywhere. See Resolving.
385 let (saved_lowest, saved_starved) = {
386 let mut r = resolving.borrow_mut();
387 (
388 std::mem::replace(&mut r.lowest_assumed, usize::MAX),
389 std::mem::replace(&mut r.starved, false),
390 )
391 };
392 let answer = self.validate_uncached(other, splits_taken, depth_remaining, memo, resolving);
393 let mut r = resolving.borrow_mut();
394 if r.lowest_assumed == usize::MAX && !r.starved {
395 memo.borrow_mut()
396 .get_or_insert_with(HashMap::new)
397 .insert(key, answer.clone());
398 }
399 r.lowest_assumed = saved_lowest.min(r.lowest_assumed);
400 r.starved = saved_starved || r.starved;
401 answer
402 }
403
404 /// The uncached body of [`Shape::validate_within`]. Call that instead; this
405 /// exists only so the memo has a single place to wrap.
406 #[allow(clippy::too_many_lines)]
407 #[must_use]
408 fn validate_uncached(
409 &self,
410 other: &Shape,
411 splits_taken: usize,
412 depth_remaining: usize,
413 memo: &SplitMemo,
414 resolving: &ResolvingPairs,
415 ) -> Option<ShapeMismatch> {
416 // Since self.case and other.case are the only information that
417 // logically matters for validation, a shape accepts any shape whose
418 // case equals its own, and we can skip doing any actual validation
419 // work.
420 //
421 // Testing equality rather than pointer identity is what the stored
422 // case hash buys: unequal cases are separated by comparing two u64s,
423 // and equal ones are confirmed by `PartialEq for CaseWithHash`, which
424 // short circuits on the pointer before walking. Pointer identity alone
425 // caught only shapes that share an allocation, and misses the far more
426 // common case of a shape rebuilt rather than cloned.
427 if self.case == other.case {
428 return None;
429 }
430
431 // Helper closure that translates boolean results to vector results. The
432 // `path` and `mismatches` parameters are always the local parameter and
433 // variable of the same names from the outer scope, but passing them in
434 // this way simplifies ownership logic and strips mutability.
435 let report = |is_valid: bool, causes: Vec<ShapeMismatch>| -> Option<ShapeMismatch> {
436 if is_valid {
437 // Use the report closure only in situations where the truth of
438 // `is_valid` should hide any previous mismatches.
439 None
440 } else {
441 Some(ShapeMismatch {
442 expected: self.clone(),
443 received: other.clone(),
444 causes,
445 })
446 }
447 };
448
449 let self_causes = match self.case() {
450 ShapeCase::One(self_shapes) => {
451 // If the union is empty (i.e. self.is_never()), then the only
452 // other shapes that can be validated by this empty union are
453 // other never shapes and None.
454 if self_shapes.is_empty() {
455 return if other.is_never() || other.is_none() {
456 None
457 } else {
458 // Reporting this error is important, since there are no
459 // self shapes to produce mismatches below.
460 Some(ShapeMismatch {
461 expected: self.clone(),
462 received: other.clone(),
463 causes: Vec::new(),
464 })
465 };
466 }
467
468 // If other is validated by any single shape in self_shapes,
469 // then other is validated by the union. However, we want to try
470 // any shapes other than *bound* ShapeCase::Name shapes first,
471 // since bound ::Name shapes are the gateway to infinite cycles,
472 // and the names might resolve to shapes we've already seen in
473 // the union, so we don't need to validate them again.
474 let mut bound_name_target_shapes = Vec::new();
475
476 // It's acceptable not to use a MergeSet<Shape> here, because we
477 // don't care about name/location metadata, but only whether one
478 // of the resolved bound_name_target_shapes is one we've seen already,
479 // which concerns only ShapeCase equality/hashing.
480 let mut not_bound_names: IndexSet<&Shape> = IndexSet::new();
481
482 let mut causes = Vec::new();
483
484 for self_shape in self_shapes.iter() {
485 if let ShapeCase::Name(name, weak) = self_shape.case() {
486 if let Some(named_shape) = weak.upgrade(name) {
487 // Handle in a later loop, keeping the reference
488 // itself: the deferral hop resolves a name, so it
489 // goes through the same guard as the ::Name arms.
490 bound_name_target_shapes.push((self_shape, named_shape));
491 continue;
492 }
493 }
494 // If self_shape is not a bound named shape, validate it
495 // against other. If it validates other (validate returns
496 // None), `?` returns None early, skipping the rest of the
497 // shapes.
498 let mismatch = self_shape.validate_within(
499 other,
500 splits_taken,
501 depth_remaining,
502 memo,
503 resolving,
504 )?;
505 // If the unbound named shape does not validate other, we
506 // need to report it.
507 causes.push(mismatch);
508 not_bound_names.insert(self_shape);
509 }
510
511 for (name_shape, target_shape) in &bound_name_target_shapes {
512 // If we resolved a bound name to a shape we've already
513 // seen, we do not need to validate it again.
514 if !not_bound_names.contains(target_shape) {
515 // If the bound named shape validates other (validate
516 // returns None), `?` returns None early, skipping the
517 // rest of the shapes. The hop resolves a name without
518 // passing through a ::Name arm, so it is guarded
519 // here: a union whose members are nothing but each
520 // other's names would otherwise lap this loop until
521 // the depth cap. (Such non-contractive cycles are
522 // degenerate -- the equations carry no structure --
523 // and unreachable from GraphQL, where fields always
524 // interpose an object between name references.)
525 let mismatch =
526 guarded(resolving, ((*name_shape).clone(), other.clone()), || {
527 target_shape.validate_within(
528 other,
529 splits_taken,
530 depth_remaining,
531 memo,
532 resolving,
533 )
534 })?;
535 // If the bound named shape does not validate other, we
536 // need to report it.
537 causes.push(mismatch);
538 }
539 }
540
541 // A single union member can cover only part of other when
542 // other hides a union inside a field or item position. No
543 // member of One<{a: Int}, {a: String}> accepts
544 // {a: One<Int, String>} whole, even though every value of the
545 // latter belongs to one of the former. Distributing those
546 // inner unions yields branches the members can cover one at a
547 // time, so the union validates other if it validates every
548 // branch.
549 //
550 // Rather than materializing the whole cartesian product of
551 // union-typed positions, split one position at a time. Each
552 // child re-enters this arm, which tries every member whole
553 // before splitting again, so a child already covered by a
554 // single member prunes every completion below it without
555 // examining any of them. The full product is only ever walked
556 // when no member covers anything until the leaves.
557 //
558 // A union on the other side is not one of those cases. Its
559 // members are already the branches, and the ::One arm of the
560 // match on other.case() below tries all of them, so splitting
561 // here would ask the same question twice and pay for the
562 // privilege.
563 //
564 // That arm carries splits_taken rather than starting over, as
565 // every recursion here does, so it shares this comparison's
566 // remaining allowance instead of getting a fresh one. Skipping
567 // the split is still free of consequence -- the same branches
568 // are tried either way -- but a received union reached with the
569 // allowance already spent is answered without further
570 // distribution, exactly as any other shape would be.
571 //
572 // The search is bounded by three structural caps, so that
573 // where it stops is a fact about the shapes alone, readable
574 // off them in advance, rather than about a meter that ran
575 // out partway through: a union wider than MAX_SPLIT_MEMBERS
576 // never starts a search, a position of arity above
577 // MAX_SPLIT_ARITY is never distributed, and at most
578 // MAX_SPLIT_DEPTH positions are distributed along any one
579 // path through the comparison -- counted across descent into
580 // fields and items, not restarted at each object level, so
581 // that nesting adds to the count rather than multiplying the
582 // cost. A member covering a child whole still ends that
583 // branch immediately, so a shape whose product is
584 // astronomical but whose branches are covered a level down
585 // stays as cheap as ever. Falling outside any cap costs precision only:
586 // validate reports the mismatch it would have reported
587 // before splitting existed.
588 if !matches!(other.case(), ShapeCase::One(_))
589 && splits_taken < MAX_SPLIT_DEPTH
590 // take() so refusing a pathologically wide union costs a
591 // bounded walk, not a count of every member.
592 && self_shapes.iter().take(MAX_SPLIT_MEMBERS + 1).count() <= MAX_SPLIT_MEMBERS
593 {
594 if let Split::Children(children) = split_alternatives(other) {
595 if children.iter().all(|child| {
596 self.validate_within(
597 child,
598 splits_taken + 1,
599 depth_remaining,
600 memo,
601 resolving,
602 )
603 .is_none()
604 }) {
605 return None;
606 }
607 }
608 }
609
610 // Importantly, we do not return false here (in contrast with
611 // the ::All case below), because the union might still validate
612 // other, e.g. when other is Bool and the union contains both
613 // true and false as members.
614 //
615 // We will need to handle ShapeCase::One logic in the ::Bool
616 // case below, but otherwise the loop above saves most of the
617 // cases below from explicitly worrying about self being a
618 // ShapeCase::One.
619 causes
620 }
621
622 ShapeCase::All(self_shapes) => {
623 let mut causes = Vec::new();
624
625 for self_shape in self_shapes.iter() {
626 if let Some(mismatch) = self_shape.validate_within(
627 other,
628 splits_taken,
629 depth_remaining,
630 memo,
631 resolving,
632 ) {
633 // If the intersection member does not validate other,
634 // we need to report it.
635 causes.push(mismatch);
636 }
637 }
638
639 return report(causes.is_empty(), causes);
640 }
641
642 // Sometimes we don't know anything (yet) about the structure of a
643 // shape, but still want to refer to it by name and access the
644 // shapes of subproperties (symbolically if not concretely). That's
645 // what the ShapeCase::Name variant models.
646 ShapeCase::Name(name, weak) => {
647 return if let Some(named_shape) = weak.upgrade(name) {
648 // If this pair is already being compared on the path
649 // above, another lap of name resolution would repeat it
650 // deterministically, so assume the pair valid (the
651 // coinductive hypothesis) and let the enclosing
652 // comparison finish discharging it. See Resolving.
653 guarded(resolving, (self.clone(), other.clone()), || {
654 named_shape.validate_within(
655 other,
656 splits_taken,
657 depth_remaining,
658 memo,
659 resolving,
660 )
661 })
662 } else {
663 // When self is an unbound named shape reference, it
664 // validates any other shape trivially, since it imposes no
665 // expectations on the other shape (and could eventually
666 // resolve to a shape that validates other).
667 None
668 };
669 }
670
671 ShapeCase::Unknown => {
672 // Unknown shapes validate any shape.
673 return None;
674 }
675
676 _ => Vec::new(),
677 };
678
679 match other.case() {
680 ShapeCase::Bool(Some(value)) => report(
681 match self.case() {
682 ShapeCase::Bool(self_value) => {
683 Some(value) == self_value.as_ref() || self_value.is_none()
684 }
685 // We already handled the ::One and ::All cases above.
686 _ => false,
687 },
688 self_causes,
689 ),
690
691 ShapeCase::Bool(None) => report(
692 (|| match self.case() {
693 ShapeCase::Bool(None) => true,
694
695 // This case goes beyond the basic ShapeCase::One handling
696 // provided at the top of the function, because we want to allow
697 // ::Bool(None) to be validated by a union of true and false.
698 ShapeCase::One(self_shapes) => {
699 let true_shape = Shape::bool_value(true, []);
700 let false_shape = Shape::bool_value(false, []);
701 let mut true_found = false;
702 let mut false_found = false;
703
704 for self_shape in self_shapes.iter() {
705 // Note the direction: the question is whether a
706 // member CONTAINS true, not whether it is
707 // contained in {true}. The two coincide on the
708 // shapes simplification can produce, since the
709 // only simplified member contained in {true} is
710 // the true literal itself, which is why the
711 // reversed test was harmless. It is still the
712 // wrong question, and it stops being harmless the
713 // moment an unsimplified union reaches here.
714 if self_shape
715 .validate_within(
716 &true_shape,
717 splits_taken,
718 depth_remaining,
719 memo,
720 resolving,
721 )
722 .is_none()
723 {
724 true_found = true;
725 }
726 if self_shape
727 .validate_within(
728 &false_shape,
729 splits_taken,
730 depth_remaining,
731 memo,
732 resolving,
733 )
734 .is_none()
735 {
736 false_found = true;
737 }
738 // Between them the members cover both booleans, so
739 // the union covers Bool.
740 if true_found && false_found {
741 // Returns from the (|| match ...)() closure.
742 return true;
743 }
744 }
745
746 false
747 }
748
749 _ => false,
750 })(),
751 self_causes,
752 ),
753
754 ShapeCase::String(value) => report(
755 match self.case() {
756 ShapeCase::String(self_value) => value == self_value || self_value.is_none(),
757 // We already handled the ::One case above.
758 _ => false,
759 },
760 self_causes,
761 ),
762
763 ShapeCase::Int(value) => report(
764 match self.case() {
765 ShapeCase::Int(self_value) => value == self_value || self_value.is_none(),
766 // All Int values are also (trivially convertible to) Float
767 // values, so Int is a subshape of Float.
768 ShapeCase::Float => true,
769 // We already handled the ::One case above.
770 _ => false,
771 },
772 self_causes,
773 ),
774
775 ShapeCase::Float => report(matches!(self.case(), ShapeCase::Float), Vec::new()),
776
777 // Both ::Null and ::None accept/validate only themselves, but they
778 // have an important difference in behavior when they appear in
779 // ::All intersections. The null value "poisons" intersections,
780 // reducing the whole intersection to null, which can be appropriate
781 // behavior when reporting certain kinds of top-level errors. The
782 // None value simply disappears from intersections, as it imposes no
783 // additional constraints on the intersection shape.
784 ShapeCase::Null => report(matches!(self.case(), ShapeCase::Null), Vec::new()),
785 ShapeCase::None => report(matches!(self.case(), ShapeCase::None), Vec::new()),
786
787 // ShapeCase::Unknown is validated by no other shapes except itself.
788 // We already handled the case when self is ::Unknown above, so we
789 // can report false here.
790 ShapeCase::Unknown => report(false, Vec::new()),
791
792 ShapeCase::Array {
793 prefix: other_prefix,
794 tail: other_tail,
795 } => match self.case() {
796 ShapeCase::Array {
797 prefix: self_prefix,
798 tail: self_tail,
799 } => {
800 let mut causes = Vec::new();
801
802 for i in 0..self_prefix.len().max(other_prefix.len()) {
803 if let Some(self_item) = self_prefix.get(i) {
804 if let Some(other_item) = other_prefix.get(i) {
805 if let Some(mismatch) = self_item.validate_within(
806 other_item,
807 splits_taken,
808 depth_remaining,
809 memo,
810 resolving,
811 ) {
812 causes.push(mismatch);
813 }
814 } else {
815 // other is shorter, so position i in other is
816 // either governed by other_tail or absent
817 // entirely, since a tailed array may end at
818 // any length. self_item has to accept both
819 // possibilities: checking only other_tail
820 // accepts [Int, Int, ...] against
821 // [Int, ...Int], which the value [5] refutes.
822 // This mirrors the One<None, rest> probe the
823 // object walk already uses for a field other
824 // does not declare.
825 //
826 // The two short circuits are the ones the
827 // object walk takes for the same probe: a None
828 // tail means the position is simply absent,
829 // since One<None, None> is None, and an
830 // Unknown tail absorbs the None, since
831 // One<None, Unknown> is Unknown. Both spare a
832 // union build per position per call, and the
833 // closed tuple that takes the first one is the
834 // common case.
835 let possible = if other_tail.is_none() || other_tail.is_unknown() {
836 other_tail.clone()
837 } else {
838 Shape::one([Shape::none(), other_tail.clone()], [])
839 };
840 if let Some(mismatch) = self_item.validate_within(
841 &possible,
842 splits_taken,
843 depth_remaining,
844 memo,
845 resolving,
846 ) {
847 causes.push(mismatch);
848 }
849 }
850 } else if let Some(other_item) = other_prefix.get(i) {
851 // self has no prefix position i; self_tail
852 // governs.
853 if self_tail.is_none() {
854 // self is closed at length self_prefix.len()
855 // but other carries another element here.
856 causes.push(ShapeMismatch {
857 expected: self.clone(),
858 received: other.clone(),
859 causes: Vec::new(),
860 });
861 } else if let Some(mismatch) = self_tail.validate_within(
862 other_item,
863 splits_taken,
864 depth_remaining,
865 memo,
866 resolving,
867 ) {
868 causes.push(mismatch);
869 }
870 }
871 // `i < max(self_prefix.len(), other_prefix.len())`
872 // guarantees at least one of the two `get(i)` calls
873 // above returns `Some`, so there is no else branch.
874 }
875
876 #[allow(clippy::match_same_arms)]
877 match (self_tail.case(), other_tail.case()) {
878 // other's tail is None, so it is effectively a fixed
879 // tuple; nothing more to check against self_tail.
880 (_, ShapeCase::None) => {}
881 // self is closed but other has a tail, so other may
882 // carry elements beyond self_prefix.len() that self
883 // cannot accept.
884 (ShapeCase::None, _) => {
885 causes.push(ShapeMismatch {
886 expected: self.clone(),
887 received: other.clone(),
888 causes: Vec::new(),
889 });
890 }
891 _ => {
892 if let Some(mismatch) = self_tail.validate_within(
893 other_tail,
894 splits_taken,
895 depth_remaining,
896 memo,
897 resolving,
898 ) {
899 causes.push(mismatch);
900 }
901 }
902 }
903
904 report(causes.is_empty(), causes)
905 }
906
907 // We already handled the ::One and ::All cases above.
908 _ => report(false, Vec::new()),
909 },
910
911 ShapeCase::Object { fields, rest } => match self.case() {
912 ShapeCase::Object {
913 fields: self_fields,
914 rest: self_rest,
915 } => {
916 let mut causes = Vec::new();
917
918 // For each field self declares, determine the shape it
919 // can take under `other` and check self's expectation
920 // accepts it.
921 for (field_name, field_shape) in self_fields {
922 if let Some(other_field_shape) = fields.get(field_name) {
923 if let Some(mismatch) = field_shape.validate_within(
924 other_field_shape,
925 splits_taken,
926 depth_remaining,
927 memo,
928 resolving,
929 ) {
930 causes.push(mismatch);
931 }
932 } else {
933 // other does not declare field_name. Under other,
934 // v.field_name is either absent (None) or, if
935 // other has a non-None rest shape, a dynamic
936 // property of that rest shape. Self must accept
937 // any such possibility.
938 if rest.is_none() {
939 // Field is guaranteed absent under other;
940 // self must tolerate None (be optional).
941 if let Some(mismatch) = field_shape.validate_within(
942 &Shape::none(),
943 splits_taken,
944 depth_remaining,
945 memo,
946 resolving,
947 ) {
948 causes.push(mismatch);
949 }
950 } else if rest.is_unknown() {
951 // `One<None, Unknown>` simplifies to `Unknown`,
952 // which is the most permissive shape — it
953 // validates `field_shape` iff `field_shape`
954 // validates `Unknown`. This is the new
955 // open-by-default case after 0.8.0; short-
956 // circuiting it skips an allocation-heavy
957 // `Shape::one([Shape::none(), ...])` build per
958 // missing field per `accepts` call.
959 if let Some(mismatch) = field_shape.validate_within(
960 rest,
961 splits_taken,
962 depth_remaining,
963 memo,
964 resolving,
965 ) {
966 causes.push(mismatch);
967 }
968 } else {
969 let possible = Shape::one([Shape::none(), rest.clone()], []);
970 if let Some(mismatch) = field_shape.validate_within(
971 &possible,
972 splits_taken,
973 depth_remaining,
974 memo,
975 resolving,
976 ) {
977 causes.push(mismatch);
978 }
979 }
980 }
981 }
982
983 // For each field other declares that self does not, the
984 // field must be absorbed by self's rest shape. A closed
985 // self (rest: None) cannot absorb any extra declared
986 // field.
987 for (field_name, other_field_shape) in fields {
988 if self_fields.contains_key(field_name) {
989 continue;
990 }
991 // A field whose shape admits no value is provably
992 // always absent, so it constrains nothing and there is
993 // nothing for self's rest to absorb. Skipping it is
994 // exact, not generous, and it has to happen on both
995 // sides of the closed/open split below: skipping it
996 // only for a closed self is what made accepts
997 // non-transitive, since a closed middle shape could
998 // accept such a field while an open outer shape that
999 // accepts the middle could not.
1000 if other_field_shape.is_none() || other_field_shape.is_never() {
1001 continue;
1002 }
1003 if self_rest.is_none() {
1004 causes.push(ShapeMismatch {
1005 expected: self.clone(),
1006 received: other.clone(),
1007 causes: Vec::new(),
1008 });
1009 } else if let Some(mismatch) = self_rest.validate_within(
1010 other_field_shape,
1011 splits_taken,
1012 depth_remaining,
1013 memo,
1014 resolving,
1015 ) {
1016 causes.push(mismatch);
1017 }
1018 }
1019
1020 // Finally, reconcile the two rest shapes themselves.
1021 match (rest.case(), self_rest.case()) {
1022 // Either other declares no dynamic properties, so
1023 // there is nothing more for self.rest to absorb,
1024 // regardless of whether self is open or closed.
1025 (ShapeCase::None, _) => {}
1026 // other permits dynamic properties but self is
1027 // closed — other's values can carry extras that
1028 // self does not permit.
1029 (_, ShapeCase::None) => {
1030 causes.push(ShapeMismatch {
1031 expected: self.clone(),
1032 received: other.clone(),
1033 causes: Vec::new(),
1034 });
1035 }
1036 // Both sides permit dynamic properties; self's rest
1037 // must be at least as permissive as other's.
1038 _ => {
1039 if let Some(mismatch) = self_rest.validate_within(
1040 rest,
1041 splits_taken,
1042 depth_remaining,
1043 memo,
1044 resolving,
1045 ) {
1046 causes.push(mismatch);
1047 }
1048 }
1049 }
1050
1051 report(causes.is_empty(), causes)
1052 }
1053
1054 // We already handled the ::One and ::All cases above.
1055 _ => report(false, Vec::new()),
1056 },
1057
1058 // If *other* is a ShapeCase::One union, then every possibility must
1059 // be validated by self. For example, if other is One<true, false>,
1060 // and self is Bool, then since true and false are each individually
1061 // validated by Bool, Bool validates the union One<true, false>.
1062 ShapeCase::One(other_shapes) => {
1063 // If other is an empty union (Never), then only None or other
1064 // Never shapes can validate it. We already handled the case
1065 // when self is Never in the first match, so here we only need
1066 // to check for None.
1067 if other_shapes.is_empty() {
1068 return if self.is_none() {
1069 None
1070 } else {
1071 Some(ShapeMismatch {
1072 expected: self.clone(),
1073 received: other.clone(),
1074 causes: Vec::new(),
1075 })
1076 };
1077 }
1078
1079 let mut causes = Vec::new();
1080
1081 for other_shape in other_shapes.iter() {
1082 if let Some(mismatch) = self.validate_within(
1083 other_shape,
1084 splits_taken,
1085 depth_remaining,
1086 memo,
1087 resolving,
1088 ) {
1089 // If the union member does not validate self, we need
1090 // to report it.
1091 causes.push(mismatch);
1092 }
1093 }
1094
1095 report(causes.is_empty(), causes)
1096 }
1097
1098 // If other is a ShapeCase::All intersection, then it is validated
1099 // by self if any of the member shapes are validated by self.
1100 ShapeCase::All(other_shapes) => {
1101 let mut causes = Vec::new();
1102
1103 for other_shape in other_shapes.iter() {
1104 // If self validates this member (validate returns None), `?`
1105 // returns None early — the intersection is validated.
1106 let mismatch = self.validate_within(
1107 other_shape,
1108 splits_taken,
1109 depth_remaining,
1110 memo,
1111 resolving,
1112 )?;
1113 causes.push(mismatch);
1114 }
1115
1116 report(causes.is_empty(), causes)
1117 }
1118
1119 ShapeCase::Name(name, weak) => {
1120 if let Some(other_shape) = weak.upgrade(name) {
1121 // If other is a bound named shape reference, pretend we
1122 // validated against the named shape, under the same
1123 // coinductive guard as the self-side ::Name arm above.
1124 return guarded(resolving, (self.clone(), other.clone()), || {
1125 self.validate_within(
1126 &other_shape,
1127 splits_taken,
1128 depth_remaining,
1129 memo,
1130 resolving,
1131 )
1132 });
1133 }
1134 // When other is an unbound named shape reference, it is
1135 // accepted/validated by no shape except itself.
1136 report(other.case == self.case, Vec::new())
1137 }
1138 }
1139 }
1140}
1141
1142/// The three caps that bound the union splitting search, together. Splitting
1143/// distributes a cartesian product over union-typed positions, so a search
1144/// that reaches the leaves can blow up combinatorially and has to be stopped
1145/// somewhere. These caps are all structural: whether a pair of shapes falls
1146/// inside them is a fact about those shapes alone, readable off them in
1147/// advance, the same at every depth of every larger comparison — never a
1148/// meter that ran out partway, whose position nobody could have predicted.
1149///
1150/// The rule in one breath: a union of at most [`MAX_SPLIT_MEMBERS`] members
1151/// distributes received positions of arity at most [`MAX_SPLIT_ARITY`], at
1152/// most [`MAX_SPLIT_DEPTH`] positions deep along any one path through the
1153/// comparison.
1154///
1155/// The caps are deliberately not a census of the received shape. A member
1156/// that covers a branch whole prunes every completion below it, so only
1157/// positions the search actually resolves count against the depth: a record
1158/// with sixty-four union-typed fields absorbed by the expected members' rest
1159/// resolves one position and stays as cheap as it looks, however astronomical
1160/// its nominal product. Counting the product instead was this bound's first
1161/// design, and it refused exactly those cheap shapes; metering comparisons
1162/// performed was its second, and its cliff depended on how much of the meter
1163/// sibling comparisons had already spent. Falling outside any cap costs
1164/// precision only: `validate` reports the mismatch it would have reported
1165/// before splitting existed.
1166///
1167/// Worst case, all three caps at their limits with nothing pruning:
1168/// `MAX_SPLIT_MEMBERS * (MAX_SPLIT_ARITY ^ MAX_SPLIT_DEPTH and change)` is
1169/// about 300k member-vs-branch comparisons, tens of milliseconds, and
1170/// reaching it takes a shape as contrived as the tests' every-assignment
1171/// unions. The
1172/// common doomed walk — two-member positions that never prune, which is what
1173/// a union of wide records looks like — dies at the depth cap after a few
1174/// thousand comparisons.
1175///
1176/// Note that caps of any size are a cliff for transitivity, not just for
1177/// precision: inside them acceptance composes along chains, outside them the
1178/// fallback answer does not, so `accepts` is not transitive for chains that
1179/// straddle a cap — but which side of the cliff a chain sits on is now
1180/// decidable by inspection. The Lean model on the `lean-model` branch omits
1181/// this splitting clause entirely (its divergence 5), in part because such
1182/// cases do not compose under it.
1183///
1184/// A position is distributed only if its union has at most this many members.
1185/// Real unions reachable from GraphQL stay small — a nullable field is two, a
1186/// union or interface type is one per member or implementor — so eight covers
1187/// them with room while keeping the worst-case fanout per level a single
1188/// digit.
1189const MAX_SPLIT_ARITY: usize = 8;
1190
1191/// At most this many positions are distributed along any one path through a
1192/// comparison before the search falls back. Pruned positions do not count, so
1193/// this is a limit on how many unions must simultaneously be distributed
1194/// before any member covers, not on how many a shape may contain. The count
1195/// survives descent into fields and items (see [`Shape::validate_within`]),
1196/// so it bounds the comparison as a whole; were it per object level instead,
1197/// nesting would multiply one level's worst case once per level.
1198///
1199/// A dozen is far above what the search costs, and that is deliberate: this
1200/// cap buys precision, and it is [`MAX_SPLIT_MEMBERS`] that bounds cost. The
1201/// reason is that the `all` over a split's children short circuits on the
1202/// first child that fails, so a search whose answer is a rejection walks one
1203/// path and abandons it rather than walking the product. The product is only
1204/// walked when branches succeed, and making every branch need its own member
1205/// requires a member per branch, which the member cap already limits to 64.
1206/// Measured against twelve positions of arity eight and 64 members that cover
1207/// nothing, raising this from 4 through 32 moved the worst case from 3.9ms to
1208/// 6.9ms and nothing else; the value is chosen for what it admits, not for
1209/// what it forbids.
1210///
1211/// Twelve nested distributions is deeper than a selection set reaches, and
1212/// past it the benign families stop gaining: a chain needing one distribution
1213/// at each of twelve levels is accepted here and costs 402µs.
1214///
1215/// The short circuit is necessary for twelve to be affordable but not
1216/// sufficient: the memo in [`SplitMemo`] is the other half. The member loop
1217/// asks the same union-against-subtree question once per branch of every
1218/// level above it, and it is the memo answering that question once that keeps
1219/// a doomed search nested several levels deep in the milliseconds. Without it
1220/// the same shapes run for minutes, and the tests that pin this cap
1221/// (`test_union_splitting_cliffs_at_each_cap` and
1222/// `test_split_depth_counts_the_whole_comparison_not_each_object_level`) do
1223/// not finish. The two were raised and introduced together, and should be
1224/// changed together.
1225const MAX_SPLIT_DEPTH: usize = 12;
1226
1227/// An expected union wider than this never starts a split search, since every
1228/// resolved position is compared against every member, and no cap on the
1229/// received shape bounds that factor.
1230const MAX_SPLIT_MEMBERS: usize = 64;
1231
1232/// What [`split_alternatives`] found while walking a shape for a union-typed
1233/// position it can distribute. Only `Children` costs anything; the caller
1234/// treats the other two alike, and they are kept apart so the walk can say
1235/// which of the two reasons it came back empty for.
1236enum Split {
1237 /// No union-typed position to distribute anywhere in the shape.
1238 Nothing,
1239 /// Every union-typed position the walk found was wider than
1240 /// [`MAX_SPLIT_ARITY`], so none of them was distributed.
1241 ///
1242 /// An over-wide position is **skipped, not declined on the spot**: the
1243 /// walk keeps looking for a narrower one. Declining on the spot was the
1244 /// original rule, on the grounds that sliding ahead would let the cap's
1245 /// size decide which position gets distributed, and with it the answer.
1246 /// That is right about the hazard and wrong about which choice avoids it.
1247 /// The walk visits an object's fields in `IndexMap` order, so declining at
1248 /// the first over-wide position makes the answer depend on **field
1249 /// insertion order** -- and insertion order is not part of a shape's
1250 /// identity, since `PartialEq` and `Hash` for `ShapeCase` are both
1251 /// order-insensitive. Two shapes comparing equal could then be accepted
1252 /// and rejected respectively, which
1253 /// `test_union_splitting_ignores_field_order` pins against. A named
1254 /// constant a reader can look up is a far better thing for an answer to
1255 /// depend on than an order nobody can see.
1256 ///
1257 /// Skipping keeps the part of the cap that matters. What the cap is for is
1258 /// that [`split_alternatives`] clones the containing container once per
1259 /// member of the position it distributes, and a position that is skipped
1260 /// is never materialized, so the allocation blowup the cap exists to
1261 /// prevent is prevented either way. Measured on a received record holding
1262 /// a 200,000-member union beside a hundred sibling fields, best of three:
1263 ///
1264 /// ```text
1265 /// declining at the position 12.0ms
1266 /// skipping past it 14.9ms
1267 /// no arity cap at all 872ms
1268 /// ```
1269 ///
1270 /// The 3ms is the walk carrying on through the remaining positions looking
1271 /// for a narrower one and finding none, which is a scan rather than a
1272 /// materialization. That is the honest price of the change, and it buys an
1273 /// answer that does not depend on field order.
1274 TooWide,
1275 /// One shape per member of the first distributable position's union.
1276 Children(Vec<Shape>),
1277}
1278
1279/// Replaces one union-typed position of `shape` with each member of that
1280/// union, returning the resulting shapes as [`Split::Children`], or
1281/// [`Split::Nothing`] when `shape` has no union-typed position to split.
1282///
1283/// The returned shapes are strictly smaller than `shape`, since each drops a
1284/// union node in favor of one of its members. That is what makes the search in
1285/// `validate` terminate.
1286///
1287/// Each returned shape is a clone of the containing object or array, so the
1288/// materialization itself costs the union's arity times the container's
1289/// width. The arity cap is therefore checked before anything is built: a
1290/// union wider than [`MAX_SPLIT_ARITY`] comes back as [`Split::TooWide`], at
1291/// zero allocations, and the walk moves on to look for a narrower position
1292/// rather than giving up where it stood. Skipping rather than declining is
1293/// what keeps the answer independent of the order an object's fields were
1294/// inserted in; see [`Split::TooWide`] for why that matters more than the
1295/// property the earlier rule was protecting.
1296///
1297/// For example `{a: One<Int, String>, b: Bool}` yields `{a: Int, b: Bool}` and
1298/// `{a: String, b: Bool}`. Only the first distributable position found is
1299/// expanded; the caller recurses to reach the rest. A more sophisticated
1300/// version could order positions by which one best discriminates among the
1301/// union members, the way a word search tries the rarest letter first, but the
1302/// prune that matters is the caller's, which tests whole members before
1303/// splitting again.
1304///
1305/// Three positions deliberately never split:
1306///
1307/// * `rest` and `tail` shapes, which constrain unboundedly many values that
1308/// each choose independently. Distributing them would lose meaning, since
1309/// `{...One<Int, String>}` accepts `{"p": 1, "q": "s"}` while neither
1310/// `{...Int}` nor `{...String}` does.
1311/// * Members of a [`ShapeCase::All`] intersection, which the intersection
1312/// itself is responsible for resolving.
1313/// * [`ShapeCase::Name`] references, so splitting cannot follow a cycle.
1314fn split_alternatives(shape: &Shape) -> Split {
1315 match shape.case() {
1316 ShapeCase::One(shapes) => {
1317 // A union of one member, or the empty union, offers no choice to
1318 // distribute. Reporting Nothing for the singleton case also keeps
1319 // the caller from recursing on an unchanged shape forever. The
1320 // count is bounded and taken before any member is cloned, so a
1321 // union too wide to split costs nothing to refuse.
1322 let count = shapes.iter().take(MAX_SPLIT_ARITY + 1).count();
1323 if count <= 1 {
1324 Split::Nothing
1325 } else if count > MAX_SPLIT_ARITY {
1326 Split::TooWide
1327 } else {
1328 Split::Children(shapes.iter().cloned().collect())
1329 }
1330 }
1331
1332 ShapeCase::Object { fields, rest } => {
1333 let mut skipped_too_wide = false;
1334 for (key, field_shape) in fields {
1335 let options = match split_alternatives(field_shape) {
1336 Split::Nothing => continue,
1337 // Skip and keep looking rather than declining here, so
1338 // that field order cannot decide the answer. See
1339 // [`Split::TooWide`].
1340 Split::TooWide => {
1341 skipped_too_wide = true;
1342 continue;
1343 }
1344 Split::Children(options) => options,
1345 };
1346 return Split::Children(
1347 options
1348 .into_iter()
1349 .map(|option| {
1350 let mut split_fields = fields.clone();
1351 split_fields.insert(key.clone(), option);
1352 Shape::object(split_fields, rest.clone(), shape.locations().cloned())
1353 })
1354 .collect(),
1355 );
1356 }
1357 if skipped_too_wide {
1358 Split::TooWide
1359 } else {
1360 Split::Nothing
1361 }
1362 }
1363
1364 ShapeCase::Array { prefix, tail } => {
1365 let mut skipped_too_wide = false;
1366 for (index, item_shape) in prefix.iter().enumerate() {
1367 let options = match split_alternatives(item_shape) {
1368 Split::Nothing => continue,
1369 // Skip and keep looking, as in the object walk above.
1370 Split::TooWide => {
1371 skipped_too_wide = true;
1372 continue;
1373 }
1374 Split::Children(options) => options,
1375 };
1376 return Split::Children(
1377 options
1378 .into_iter()
1379 .map(|option| {
1380 let mut split_prefix = prefix.clone();
1381 split_prefix[index] = option;
1382 Shape::array(split_prefix, tail.clone(), shape.locations().cloned())
1383 })
1384 .collect(),
1385 );
1386 }
1387 if skipped_too_wide {
1388 Split::TooWide
1389 } else {
1390 Split::Nothing
1391 }
1392 }
1393
1394 _ => Split::Nothing,
1395 }
1396}