Skip to main content

maskforge_core/
routing.rs

1//! Shared, bounded execution-cost analysis for regular and structured compilation.
2
3use crate::ir::{AdditionalPolicy, ContainsPolicy, ItemsPolicy, Node, RefSlice, SchemaIR};
4use crate::primitives::NodeId;
5
6/// Increment when a routing rule or cost model changes executable selection.
7pub 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/// A bounded estimate that never wraps attacker-controlled arithmetic.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum Bound {
16    /// The value is known and no greater than the estimator's cap.
17    Known(u64),
18    /// The value overflowed or exceeded the estimator's cap.
19    ExceedsLimit,
20}
21
22/// Adds two bounded values, short-circuiting on overflow or cap violation.
23#[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/// Multiplies two bounded values, short-circuiting on overflow or cap violation.
35#[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/// Computes `2^n` under a cap without an overflowing shift.
47#[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/// Runtime shape selected for one IR node.
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ExecutionKind {
57    /// Compile the complete subtree as one regular byte automaton.
58    Regular,
59    /// Execute the node and its descendants with structured semantics.
60    Structured,
61    /// Execute a structured node while retaining regular compiled descendants.
62    Hybrid,
63}
64
65/// Stable primary reason for a node's selected route.
66#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
67pub enum RouteReason {
68    /// The bounded regular estimate is below all construction limits.
69    CheapRegular,
70    /// Static or dynamic reference scope requires structured execution.
71    DynamicReference,
72    /// Evaluation annotations must be preserved across applicators.
73    EvaluationAnnotations,
74    /// Open-object matching needs decoded-key structured semantics.
75    OpenObject,
76    /// A user pattern runs over decoded Unicode scalar values.
77    UserPattern,
78    /// The conservative shuffle estimate exceeds a regular construction budget.
79    ShuffleCost,
80    /// The conservative product estimate exceeds a regular construction budget.
81    ProductCost,
82    /// A repetition estimate exceeds a regular construction budget.
83    RepetitionCost,
84    /// Estimated retained or peak automaton memory exceeds its budget.
85    AutomatonMemory,
86    /// The keyword's semantics are implemented only by the structured engine.
87    StructuredOnlySemantics,
88}
89
90/// Bounded construction and memory estimate for one execution strategy.
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub struct CostEstimate {
93    /// Estimated reachable states or structured plan nodes.
94    pub states: Bound,
95    /// Estimated transition cells or structured graph edges.
96    pub transition_cells: Bound,
97    /// Estimated construction work units.
98    pub work: Bound,
99    /// Estimated retained bytes.
100    pub retained_bytes: Bound,
101    /// Estimated peak construction bytes.
102    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/// One immutable route-table entry.
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub struct NodeRoute {
144    /// Selected execution shape.
145    pub kind: ExecutionKind,
146    /// Stable primary route reason.
147    pub reason: RouteReason,
148    /// Conservative regular construction estimate.
149    pub regular: CostEstimate,
150    /// Conservative structured construction estimate.
151    pub structured: CostEstimate,
152}
153
154/// The immutable per-node route analysis computed once for a frozen IR.
155#[derive(Clone, Debug)]
156pub struct RouteTable {
157    routes: Box<[NodeRoute]>,
158}
159
160impl RouteTable {
161    /// Route for `node`, if it belongs to this IR.
162    #[must_use]
163    pub fn get(&self, node: NodeId) -> Option<&NodeRoute> {
164        self.routes.get(node.get() as usize)
165    }
166
167    /// All routes in arena order.
168    #[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}