1use crate::ir::{AdditionalPolicy, ContainsPolicy, ItemsPolicy, Node, RefSlice, SchemaIR};
4use crate::primitives::NodeId;
5
6pub const ROUTE_POLICY_VERSION: u32 = 5;
8
9const STATE_CAP: u64 = 1 << 16;
10const CELL_CAP: u64 = 1 << 22;
11const MEMORY_CAP: u64 = 256 << 20;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum Bound {
16 Known(u64),
18 ExceedsLimit,
20}
21
22#[must_use]
24pub fn add_bound(a: Bound, b: Bound, cap: u64) -> Bound {
25 match (a, b) {
26 (Bound::Known(a), Bound::Known(b)) => a
27 .checked_add(b)
28 .filter(|sum| *sum <= cap)
29 .map_or(Bound::ExceedsLimit, Bound::Known),
30 _ => Bound::ExceedsLimit,
31 }
32}
33
34#[must_use]
36pub fn mul_bound(a: Bound, b: Bound, cap: u64) -> Bound {
37 match (a, b) {
38 (Bound::Known(a), Bound::Known(b)) => a
39 .checked_mul(b)
40 .filter(|product| *product <= cap)
41 .map_or(Bound::ExceedsLimit, Bound::Known),
42 _ => Bound::ExceedsLimit,
43 }
44}
45
46#[must_use]
48pub fn pow2_bound(n: u32, cap: u64) -> Bound {
49 1u64.checked_shl(n)
50 .filter(|value| *value <= cap)
51 .map_or(Bound::ExceedsLimit, Bound::Known)
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ExecutionKind {
57 Regular,
59 Structured,
61 Hybrid,
63}
64
65#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
67pub enum RouteReason {
68 CheapRegular,
70 DynamicReference,
72 EvaluationAnnotations,
74 OpenObject,
76 UserPattern,
78 ShuffleCost,
80 ProductCost,
82 RepetitionCost,
84 AutomatonMemory,
86 StructuredOnlySemantics,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub struct CostEstimate {
93 pub states: Bound,
95 pub transition_cells: Bound,
97 pub work: Bound,
99 pub retained_bytes: Bound,
101 pub peak_bytes: Bound,
103}
104
105impl CostEstimate {
106 fn regular(states: Bound, cells: Bound) -> Self {
107 let retained = add_bound(
108 mul_bound(states, Bound::Known(64), MEMORY_CAP),
109 mul_bound(cells, Bound::Known(8), MEMORY_CAP),
110 MEMORY_CAP,
111 );
112 let peak = add_bound(
113 retained,
114 mul_bound(states, Bound::Known(96), MEMORY_CAP),
115 MEMORY_CAP,
116 );
117 Self {
118 states,
119 transition_cells: cells,
120 work: cells,
121 retained_bytes: retained,
122 peak_bytes: peak,
123 }
124 }
125
126 fn structured(child_count: usize) -> Self {
127 let nodes = u64::try_from(child_count)
128 .ok()
129 .and_then(|count| count.checked_add(1))
130 .map_or(Bound::ExceedsLimit, Bound::Known);
131 Self {
132 states: nodes,
133 transition_cells: nodes,
134 work: nodes,
135 retained_bytes: mul_bound(nodes, Bound::Known(128), MEMORY_CAP),
136 peak_bytes: mul_bound(nodes, Bound::Known(256), MEMORY_CAP),
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub struct NodeRoute {
144 pub kind: ExecutionKind,
146 pub reason: RouteReason,
148 pub regular: CostEstimate,
150 pub structured: CostEstimate,
152}
153
154#[derive(Clone, Debug)]
156pub struct RouteTable {
157 routes: Box<[NodeRoute]>,
158}
159
160impl RouteTable {
161 #[must_use]
163 pub fn get(&self, node: NodeId) -> Option<&NodeRoute> {
164 self.routes.get(node.get() as usize)
165 }
166
167 #[must_use]
169 pub fn routes(&self) -> &[NodeRoute] {
170 &self.routes
171 }
172}
173
174pub(crate) fn analyze(ir: &SchemaIR) -> RouteTable {
175 let mut routes = vec![None; ir.node_count()];
176 let mut visit = vec![0u8; ir.node_count()];
177 let mut stack = Vec::with_capacity(ir.node_count().saturating_mul(2));
178 for index in (0..ir.node_count()).rev() {
179 stack.push((NodeId(u32::try_from(index).unwrap_or(u32::MAX)), false));
180 }
181 while let Some((id, done)) = stack.pop() {
182 let index = id.get() as usize;
183 if index >= visit.len() {
184 continue;
185 }
186 if done {
187 routes[index] = ir.node(id).map(|node| estimate_node(ir, node, &routes));
188 visit[index] = 2;
189 continue;
190 }
191 if visit[index] != 0 {
192 continue;
193 }
194 visit[index] = 1;
195 stack.push((id, true));
196 for_each_child(ir, ir.node(id), |child| {
197 let child_index = child.get() as usize;
198 if visit.get(child_index) == Some(&0) {
199 stack.push((child, false));
200 }
201 });
202 }
203 let fallback = NodeRoute {
204 kind: ExecutionKind::Structured,
205 reason: RouteReason::StructuredOnlySemantics,
206 regular: CostEstimate::regular(Bound::ExceedsLimit, Bound::ExceedsLimit),
207 structured: CostEstimate::structured(0),
208 };
209 RouteTable {
210 routes: routes
211 .into_iter()
212 .map(|route| route.unwrap_or(fallback))
213 .collect(),
214 }
215}
216
217fn estimate_node(ir: &SchemaIR, node: &Node, routes: &[Option<NodeRoute>]) -> NodeRoute {
218 let mut children = Vec::new();
219 for_each_child(ir, Some(node), |child| children.push(child));
220 let child_routes: Vec<NodeRoute> = children
221 .iter()
222 .filter_map(|child| routes.get(child.get() as usize).and_then(|route| *route))
223 .collect();
224 let unresolved_child = child_routes.len() != children.len();
225 let (regular, cost_reason) = regular_cost(ir, node, &child_routes);
226 let local_reason = local_structured_reason(ir, node, regular);
227 let child_non_regular = child_routes
228 .iter()
229 .find(|route| route.kind != ExecutionKind::Regular);
230 let (kind, reason) = if let Some(reason) = local_reason {
231 let has_regular_child = child_routes
232 .iter()
233 .any(|route| route.kind == ExecutionKind::Regular);
234 (
235 if has_regular_child {
236 ExecutionKind::Hybrid
237 } else {
238 ExecutionKind::Structured
239 },
240 reason,
241 )
242 } else if unresolved_child {
243 (ExecutionKind::Hybrid, RouteReason::DynamicReference)
244 } else if let Some(child) = child_non_regular {
245 (ExecutionKind::Hybrid, child.reason)
246 } else if cost_reason != RouteReason::CheapRegular {
247 (ExecutionKind::Hybrid, cost_reason)
248 } else {
249 (ExecutionKind::Regular, RouteReason::CheapRegular)
250 };
251 NodeRoute {
252 kind,
253 reason,
254 regular,
255 structured: CostEstimate::structured(children.len()),
256 }
257}
258
259fn local_structured_reason(
260 ir: &SchemaIR,
261 node: &Node,
262 regular: CostEstimate,
263) -> Option<RouteReason> {
264 match node {
265 Node::OpenObject { .. } => Some(RouteReason::OpenObject),
266 Node::StringPattern { .. } if ir.is_user_pattern(node) => Some(RouteReason::UserPattern),
267 Node::StringPattern { .. } if ir.is_unrolled_codepoint_string(node) => {
268 Some(RouteReason::RepetitionCost)
269 }
270 Node::Object {
271 fields,
272 dependent,
273 dependent_required,
274 ..
275 } => {
276 if ir
277 .props_at(*dependent)
278 .is_some_and(|items| !items.is_empty())
279 || ir
280 .dependent_required_at(*dependent_required)
281 .is_some_and(|items| !items.is_empty())
282 {
283 Some(RouteReason::EvaluationAnnotations)
284 } else if ir
285 .props_at(*fields)
286 .is_some_and(|items| items.len() > crate::ir::MAX_SAFE_SHUFFLE_FIELDS)
287 || regular.states == Bound::ExceedsLimit
288 || regular.transition_cells == Bound::ExceedsLimit
289 {
290 Some(RouteReason::ShuffleCost)
291 } else {
292 None
293 }
294 }
295 Node::Array {
296 unique_items,
297 items,
298 contains,
299 min_items,
300 max_items,
301 ..
302 } => {
303 if *unique_items || matches!(items, ItemsPolicy::AllowAny) || contains.is_some() {
304 Some(RouteReason::StructuredOnlySemantics)
305 } else if *min_items > crate::ir::MAX_UNROLLED_ARRAY_ITEMS
306 || max_items.is_some_and(|max| max > crate::ir::MAX_UNROLLED_ARRAY_ITEMS)
307 || regular.states == Bound::ExceedsLimit
308 {
309 Some(RouteReason::RepetitionCost)
310 } else {
311 None
312 }
313 }
314 Node::Tuple {
315 unique_items,
316 contains,
317 min_items,
318 max_items,
319 ..
320 } => {
321 if *unique_items || contains.is_some() {
322 Some(RouteReason::StructuredOnlySemantics)
323 } else if *min_items > crate::ir::MAX_UNROLLED_ARRAY_ITEMS
324 || max_items.is_some_and(|max| max > crate::ir::MAX_UNROLLED_ARRAY_ITEMS)
325 || regular.states == Bound::ExceedsLimit
326 {
327 Some(RouteReason::RepetitionCost)
328 } else {
329 None
330 }
331 }
332 Node::Intersection { branches } | Node::ExactlyOne { branches }
333 if combinator_has_expensive_branch(ir, *branches)
334 || regular.states == Bound::ExceedsLimit
335 || regular.transition_cells == Bound::ExceedsLimit =>
336 {
337 Some(RouteReason::ProductCost)
338 }
339 Node::Number {
340 integer_only,
341 minimum,
342 maximum,
343 multiple_of,
344 } if *integer_only || minimum.is_some() || maximum.is_some() || multiple_of.is_some() => {
345 Some(RouteReason::StructuredOnlySemantics)
346 }
347 Node::Not { .. } | Node::Unsupported { .. } => Some(RouteReason::StructuredOnlySemantics),
348 Node::Ref { .. } | Node::DynamicRef { .. } => Some(RouteReason::DynamicReference),
349 Node::Unevaluated { .. } => Some(RouteReason::EvaluationAnnotations),
350 _ if regular.retained_bytes == Bound::ExceedsLimit
351 || regular.peak_bytes == Bound::ExceedsLimit =>
352 {
353 Some(RouteReason::AutomatonMemory)
354 }
355 _ => None,
356 }
357}
358
359fn regular_cost(ir: &SchemaIR, node: &Node, children: &[NodeRoute]) -> (CostEstimate, RouteReason) {
360 if ir.is_large_string_enum(node) {
361 let states = match node {
362 Node::Enum { values } => ir
363 .lits_at(*values)
364 .and_then(|values| u64::try_from(values.len()).ok())
365 .map_or(Bound::ExceedsLimit, |count| {
366 add_bound(Bound::Known(count), Bound::Known(1), STATE_CAP)
367 }),
368 _ => Bound::ExceedsLimit,
369 };
370 let cells = mul_bound(states, Bound::Known(1), CELL_CAP);
371 return (
372 CostEstimate::regular(states, cells),
373 RouteReason::CheapRegular,
374 );
375 }
376 let child_states = |index: usize| {
377 children
378 .get(index)
379 .map_or(Bound::Known(1), |route| route.regular.states)
380 };
381 let states = match node {
382 Node::Union { .. } => children.iter().fold(Bound::Known(1), |total, child| {
383 add_bound(total, child.regular.states, STATE_CAP)
384 }),
385 Node::Intersection { .. } | Node::ExactlyOne { .. } => {
386 children.iter().fold(Bound::Known(1), |total, child| {
387 mul_bound(total, child.regular.states, STATE_CAP)
388 })
389 }
390 Node::StringPattern {
391 max_len: Some(max),
392 charset: crate::ir::Charset::Utf8CountedCodepoints,
393 ..
394 } => add_bound(
395 mul_bound(Bound::Known(u64::from(*max)), Bound::Known(8), STATE_CAP),
396 Bound::Known(16),
397 STATE_CAP,
398 ),
399 Node::StringPattern { max_len: None, .. } => Bound::Known(64),
400 Node::LexicalNumber { regex } => ir
401 .str_at(*regex)
402 .and_then(|regex| u64::try_from(regex.len()).ok())
403 .map_or(Bound::ExceedsLimit, |bytes| {
404 add_bound(Bound::Known(bytes), Bound::Known(16), STATE_CAP)
405 }),
406 Node::Object { fields, .. } => {
407 let count = ir.props_at(*fields).map_or(0usize, <[_]>::len);
408 if count == 0 {
409 return (
410 CostEstimate::regular(Bound::Known(1), Bound::Known(64)),
411 RouteReason::CheapRegular,
412 );
413 }
414 let count_u32 = u32::try_from(count).unwrap_or(u32::MAX);
415 let subsets = pow2_bound(count_u32, STATE_CAP);
416 let half = count_u32
417 .checked_sub(1)
418 .map_or(Bound::Known(1), |n| pow2_bound(n, STATE_CAP));
419 let sum = ir
420 .props_at(*fields)
421 .unwrap_or(&[])
422 .iter()
423 .fold(Bound::Known(0), |total, (_, child)| {
424 add_bound(total, shuffle_child_states(ir, *child), STATE_CAP)
425 });
426 add_bound(subsets, mul_bound(half, sum, STATE_CAP), STATE_CAP)
427 }
428 Node::Array { max_items, .. } | Node::Tuple { max_items, .. } => max_items.map_or_else(
429 || add_bound(child_states(0), Bound::Known(16), STATE_CAP),
430 |max| {
431 add_bound(
432 Bound::Known(1),
433 mul_bound(Bound::Known(u64::from(max)), child_states(0), STATE_CAP),
434 STATE_CAP,
435 )
436 },
437 ),
438 Node::Enum { values } => ir.lits_at(*values).map_or(Bound::ExceedsLimit, |values| {
439 values.iter().fold(Bound::Known(1), |total, value| {
440 let bytes = match value {
441 crate::ir::ScalarLit::Str(value) => {
442 u64::try_from(value.len()).unwrap_or(u64::MAX)
443 }
444 _ => 16,
445 };
446 add_bound(total, Bound::Known(bytes.saturating_add(1)), STATE_CAP)
447 })
448 }),
449 _ => Bound::Known(32),
450 };
451 let cells = if matches!(node, Node::Intersection { .. } | Node::ExactlyOne { .. }) {
452 mul_bound(states, Bound::Known(256), 1 << 24)
453 } else if matches!(node, Node::Object { fields, .. } if ir.props_at(*fields).is_some_and(|fields| !fields.is_empty()))
454 {
455 mul_bound(states, Bound::Known(256), 1 << 20)
456 } else {
457 mul_bound(states, Bound::Known(64), CELL_CAP)
458 };
459 let reason = if matches!(node, Node::Intersection { .. } | Node::ExactlyOne { .. })
460 && (states == Bound::ExceedsLimit || cells == Bound::ExceedsLimit)
461 {
462 RouteReason::ProductCost
463 } else if matches!(node, Node::Object { .. })
464 && (states == Bound::ExceedsLimit || cells == Bound::ExceedsLimit)
465 {
466 RouteReason::ShuffleCost
467 } else if states == Bound::ExceedsLimit || cells == Bound::ExceedsLimit {
468 RouteReason::AutomatonMemory
469 } else {
470 RouteReason::CheapRegular
471 };
472 (CostEstimate::regular(states, cells), reason)
473}
474
475fn shuffle_child_states(ir: &SchemaIR, id: NodeId) -> Bound {
476 match ir.node(id) {
477 Some(Node::StringPattern {
478 regex,
479 max_len: Some(max),
480 ..
481 }) => add_bound(
482 mul_bound(
483 Bound::Known(u64::from(*max)),
484 Bound::Known(if ir.str_at(*regex).is_some_and(|regex| regex.len() <= 8) {
485 8
486 } else {
487 96
488 }),
489 STATE_CAP,
490 ),
491 Bound::Known(16),
492 STATE_CAP,
493 ),
494 Some(Node::StringPattern { max_len: None, .. }) => Bound::Known(512),
495 Some(Node::Union { branches }) => ir
496 .refs_at(*branches)
497 .unwrap_or(&[])
498 .iter()
499 .map(|child| shuffle_child_states(ir, *child))
500 .max_by_key(|bound| match bound {
501 Bound::Known(value) => *value,
502 Bound::ExceedsLimit => u64::MAX,
503 })
504 .unwrap_or(Bound::Known(1)),
505 Some(Node::Enum { values }) => ir.lits_at(*values).map_or(Bound::ExceedsLimit, |values| {
506 let source_bytes = values.iter().fold(Bound::Known(1), |total, value| {
507 let bytes = match value {
508 crate::ir::ScalarLit::Str(value) => {
509 u64::try_from(value.len()).unwrap_or(u64::MAX)
510 }
511 _ => 16,
512 };
513 add_bound(total, Bound::Known(bytes.saturating_add(1)), STATE_CAP)
514 });
515 mul_bound(source_bytes, Bound::Known(8), STATE_CAP)
516 }),
517 Some(Node::Boolean | Node::Null | Node::Never) => Bound::Known(1),
518 Some(Node::Integer { .. } | Node::Number { .. }) => Bound::Known(512),
519 Some(Node::Array {
520 items: ItemsPolicy::Schema(child),
521 ..
522 }) => add_bound(
523 shuffle_child_states(ir, *child),
524 Bound::Known(32),
525 STATE_CAP,
526 ),
527 Some(Node::Tuple { prefix, tail, .. }) => {
528 let prefix_cost = ir
529 .refs_at(*prefix)
530 .unwrap_or(&[])
531 .iter()
532 .fold(Bound::Known(32), |total, child| {
533 add_bound(total, shuffle_child_states(ir, *child), STATE_CAP)
534 });
535 tail.map_or(prefix_cost, |child| {
536 add_bound(prefix_cost, shuffle_child_states(ir, child), STATE_CAP)
537 })
538 }
539 Some(Node::LexicalNumber { regex }) => ir
540 .str_at(*regex)
541 .and_then(|regex| u64::try_from(regex.len()).ok())
542 .map_or(Bound::ExceedsLimit, Bound::Known),
543 Some(_) => Bound::Known(2048),
544 None => Bound::ExceedsLimit,
545 }
546}
547
548fn combinator_has_expensive_branch(ir: &SchemaIR, branches: RefSlice) -> bool {
549 ir.refs_at(branches).is_some_and(|branches| {
550 branches.iter().any(|branch| {
551 ir.node(*branch).is_some_and(|node| match node {
552 Node::Integer {
553 multiple_of: Some(_),
554 ..
555 }
556 | Node::Number {
557 multiple_of: Some(_),
558 ..
559 } => true,
560 Node::Object { fields, .. } => {
561 ir.props_at(*fields).is_some_and(|fields| fields.len() >= 2)
562 }
563 Node::OpenObject { known, .. } => {
564 ir.props_at(*known).is_some_and(|fields| fields.len() >= 2)
565 }
566 Node::Array { max_items, .. } | Node::Tuple { max_items, .. } => {
567 max_items.is_none_or(|max| max > 5)
568 }
569 _ => ir.is_large_string_enum(node),
570 })
571 })
572 })
573}
574
575fn for_each_child(ir: &SchemaIR, node: Option<&Node>, mut visit: impl FnMut(NodeId)) {
576 match node {
577 Some(Node::Object {
578 fields, dependent, ..
579 }) => {
580 for &(_, child) in ir.props_at(*fields).unwrap_or(&[]) {
581 visit(child);
582 }
583 for &(_, child) in ir.props_at(*dependent).unwrap_or(&[]) {
584 visit(child);
585 }
586 }
587 Some(Node::OpenObject {
588 known,
589 patterns,
590 additional,
591 property_names,
592 dependent,
593 ..
594 }) => {
595 for &(_, child) in ir.props_at(*known).unwrap_or(&[]) {
596 visit(child);
597 }
598 for &(_, child) in ir.props_at(*patterns).unwrap_or(&[]) {
599 visit(child);
600 }
601 if let AdditionalPolicy::Schema(child) = additional {
602 visit(*child);
603 }
604 if let Some(child) = property_names {
605 visit(*child);
606 }
607 for &(_, child) in ir.props_at(*dependent).unwrap_or(&[]) {
608 visit(child);
609 }
610 }
611 Some(Node::Array {
612 items, contains, ..
613 }) => {
614 if let ItemsPolicy::Schema(child) = items {
615 visit(*child);
616 }
617 if let Some(contains) = contains {
618 if let ContainsPolicy::Schema(child) = contains.policy {
619 visit(child);
620 }
621 }
622 }
623 Some(Node::Tuple {
624 prefix,
625 tail,
626 contains,
627 ..
628 }) => {
629 for &child in ir.refs_at(*prefix).unwrap_or(&[]) {
630 visit(child);
631 }
632 if let Some(child) = tail {
633 visit(*child);
634 }
635 if let Some(contains) = contains {
636 if let ContainsPolicy::Schema(child) = contains.policy {
637 visit(child);
638 }
639 }
640 }
641 Some(Node::Union { branches })
642 | Some(Node::Intersection { branches })
643 | Some(Node::ExactlyOne { branches }) => {
644 for &child in ir.refs_at(*branches).unwrap_or(&[]) {
645 visit(child);
646 }
647 }
648 Some(Node::Not { inner }) => visit(*inner),
649 Some(Node::Ref { def }) => {
650 if let Some(child) = ir.def_target(*def) {
651 visit(child);
652 }
653 }
654 Some(Node::DynamicRef { initial_target, .. }) => visit(*initial_target),
655 Some(Node::Unevaluated {
656 scope, unevaluated, ..
657 }) => {
658 visit(*scope);
659 visit(*unevaluated);
660 }
661 _ => {}
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 fn bounded_arithmetic_closes_overflow_and_boundaries() {
671 assert_eq!(
672 add_bound(Bound::Known(4), Bound::Known(5), 9),
673 Bound::Known(9)
674 );
675 assert_eq!(
676 add_bound(Bound::Known(4), Bound::Known(6), 9),
677 Bound::ExceedsLimit
678 );
679 assert_eq!(
680 mul_bound(Bound::Known(3), Bound::Known(4), 12),
681 Bound::Known(12)
682 );
683 assert_eq!(
684 mul_bound(Bound::Known(u64::MAX), Bound::Known(2), u64::MAX),
685 Bound::ExceedsLimit
686 );
687 assert_eq!(pow2_bound(16, 1 << 16), Bound::Known(1 << 16));
688 assert_eq!(pow2_bound(17, 1 << 16), Bound::ExceedsLimit);
689 assert_eq!(pow2_bound(64, u64::MAX), Bound::ExceedsLimit);
690 }
691}