selene_gql/plan/ir/mod.rs
1//! Planner IR definitions.
2
3mod access;
4mod call;
5mod caps;
6mod catalog;
7mod execution;
8mod filter;
9mod mutation;
10mod session;
11mod subquery;
12mod tx;
13
14use crate::{
15 EdgeDirection, LabelExpr, MatchMode, PathMode, PathSelector, SetOp, SourceSpan,
16 analyze::{AnalyzedType, BindingId},
17};
18
19pub use access::{IndexKey, NodeIdOrdering, OrderAccess, ScanAccess, TypedIndexBounds};
20pub use call::{PlannedCall, PlannedYieldItem, YieldKind};
21pub use caps::ImplDefinedCaps;
22pub use catalog::{CatalogOp, PlannedTypePropertyConstraint, PlannedTypePropertyDef};
23pub use execution::{ExecutionPlan, PipelineOpId};
24pub use filter::{
25 Aggregate, AggregateArg, FilterPredicate, FilterPredicateKind, LimitAmount, OrderKey,
26 ProjectExpr,
27};
28pub use mutation::{DeleteTargetPlan, InsertEndpointRef, InsertSiteId, MutationOp, PropertyInit};
29pub use session::SessionOp;
30pub use subquery::{
31 OuterBindingRef, PlannedSubquery, PlannedTableSubquery, PlannedTableSubqueryYield,
32 SubqueryBody, SubqueryKind, SubqueryRegistry,
33};
34pub use tx::TxOp;
35
36/// Pattern-matching subplan for the leading MATCH prefix.
37#[derive(Clone, Debug)]
38pub struct PatternPlan {
39 /// Named pattern bindings visible to downstream pipeline operations.
40 pub bindings: Vec<BindingDef>,
41 /// Unoptimized join tree.
42 pub join_tree: JoinTree,
43 /// Inline and clause-level predicates attached to the pattern phase.
44 pub filters: Vec<FilterPredicate>,
45 /// Path-binding placeholders carried for later path execution work.
46 pub paths: Vec<PathPlan>,
47}
48
49/// Named binding defined by pattern analysis.
50#[derive(Clone, Debug, PartialEq)]
51pub struct BindingDef {
52 /// Analyzer-stable binding ID.
53 pub binding: BindingId,
54 /// Database-string binding name.
55 pub name: selene_core::DbString,
56 /// Element kind represented by the binding.
57 pub element: BindingElement,
58 /// Analyzer-inferred binding type.
59 pub ty: AnalyzedType,
60 /// Static label predicate from the declaring pattern, when present.
61 pub label_predicate: Option<LabelExpr>,
62 /// Source span of the declaration.
63 pub span: SourceSpan,
64}
65
66/// Binding element category.
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum BindingElement {
69 /// Node binding.
70 Node,
71 /// Edge binding.
72 Edge,
73 /// Path binding.
74 Path,
75}
76
77/// Binding endpoint used by path-level operators.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum TailBinding {
80 /// Named pattern binding.
81 Named(BindingId),
82 /// Executor-private hidden binding.
83 Hidden(HiddenBindingId),
84}
85
86impl TailBinding {
87 /// Return the named binding, if any.
88 #[must_use]
89 pub const fn named(self) -> Option<BindingId> {
90 match self {
91 Self::Named(binding) => Some(binding),
92 Self::Hidden(_) => None,
93 }
94 }
95
96 /// Return the hidden binding, if any.
97 #[must_use]
98 pub const fn hidden(self) -> Option<HiddenBindingId> {
99 match self {
100 Self::Named(_) => None,
101 Self::Hidden(binding) => Some(binding),
102 }
103 }
104}
105
106/// Source of one hop-count contribution for a path-search row.
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum HopContributor {
109 /// Static hop count from a fixed-length chain segment.
110 Fixed(u32),
111 /// One fixed hop from a named edge binding.
112 EdgeNamed(BindingId),
113 /// One fixed hop from an anonymous edge hidden binding.
114 EdgeHidden(HiddenBindingId),
115 /// Optional hop from a named questioned edge binding.
116 QuestionedNamed(BindingId),
117 /// Optional hop from an anonymous questioned edge hidden binding.
118 QuestionedHidden(HiddenBindingId),
119 /// Runtime hop count from a named quantified-edge group binding.
120 GroupNamed(BindingId),
121 /// Runtime hop count from an anonymous quantified-edge hidden binding.
122 GroupHidden(HiddenBindingId),
123}
124
125/// Source of one ordered path-mode validation contribution.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum PathContributor {
128 /// Node binding in binding-path order.
129 Node(TailBinding),
130 /// Fixed edge identity from a named edge binding.
131 EdgeNamed(BindingId),
132 /// Fixed edge identity from an executor-private edge binding.
133 EdgeHidden(HiddenBindingId),
134 /// Optional edge identity and final node from a named questioned edge binding.
135 QuestionedEdgeNamed {
136 /// Runtime binding containing either `EdgeRef` or `NULL`.
137 binding: BindingId,
138 /// Node binding on the final side when the edge is present.
139 final_binding: TailBinding,
140 },
141 /// Optional edge identity and final node from an anonymous questioned edge binding.
142 QuestionedEdgeHidden {
143 /// Runtime hidden slot containing either `EdgeRef` or `NULL`.
144 hidden: HiddenBindingId,
145 /// Node binding on the final side when the edge is present.
146 final_binding: TailBinding,
147 },
148 /// Quantified edge group with enough topology to rebuild intermediate nodes.
149 EdgeGroupNamed {
150 /// Runtime group binding containing `LIST<EdgeRef>`.
151 binding: BindingId,
152 /// Node binding at the source side of the quantified segment.
153 source: TailBinding,
154 /// Direction requested by the quantified edge pattern.
155 direction: EdgeDirection,
156 },
157 /// Anonymous quantified edge group with enough topology to rebuild intermediate nodes.
158 EdgeGroupHidden {
159 /// Runtime hidden slot containing `LIST<EdgeRef>`.
160 hidden: HiddenBindingId,
161 /// Node binding at the source side of the quantified segment.
162 source: TailBinding,
163 /// Direction requested by the quantified edge pattern.
164 direction: EdgeDirection,
165 },
166}
167
168/// Pattern join tree.
169#[derive(Clone, Debug)]
170#[non_exhaustive]
171pub enum JoinTree {
172 /// One-row, all-null anchor used to model leading optional graph patterns.
173 Unit,
174 /// Scan nodes or edges.
175 Scan(NodeOrEdgeScan),
176 /// Expand from a child tree across one edge pattern.
177 Expand {
178 /// Input side of the expansion.
179 child: Box<JoinTree>,
180 /// Edge pattern to traverse.
181 edge: EdgeMatch,
182 /// Direction requested by the source pattern.
183 direction: EdgeDirection,
184 },
185 /// Optional single-edge expansion for ISO questioned path primary (`?`).
186 ///
187 /// The skipped row binds the edge as `NULL` and unifies the final node
188 /// with the source node. The taken row behaves like a one-hop expansion.
189 Questioned {
190 /// Input side of the expansion.
191 child: Box<JoinTree>,
192 /// Edge pattern to traverse when the optional edge is present.
193 edge: EdgeMatch,
194 /// Direction requested by the source pattern.
195 direction: EdgeDirection,
196 /// Binding for the source-side node.
197 source_binding: TailBinding,
198 /// Binding for the final node after the questioned edge.
199 final_binding: TailBinding,
200 },
201 /// Bounded or future variable-length expansion across one edge pattern.
202 ///
203 /// `max: None` represents an ISO unbounded quantifier after the analyzer's
204 /// 16.4 legality gate has authorized it.
205 Repeat {
206 /// Input side of the expansion.
207 child: Box<JoinTree>,
208 /// Quantified edge pattern to traverse.
209 edge: RepeatEdgeMatch,
210 /// Direction requested by the source pattern.
211 direction: EdgeDirection,
212 /// Minimum number of hops.
213 min: u32,
214 /// Maximum number of hops, or `None` for future unbounded forms.
215 max: Option<u32>,
216 /// Path mode in scope for this repeat.
217 path_mode: PathMode,
218 },
219 /// Selector wrapper over one complete path pattern.
220 ///
221 /// The child materializes every candidate row. `PathSearch` then applies
222 /// endpoint partitioning and hop-count selection for `ANY` / `SHORTEST`
223 /// without changing the child join-tree shape.
224 PathSearch {
225 /// Selector to apply to the child path pattern.
226 selector: PathSelector,
227 /// Complete path-pattern child.
228 child: Box<JoinTree>,
229 /// Binding for the first node in the selected path.
230 source_binding: TailBinding,
231 /// Binding for the final node in the selected path.
232 final_binding: TailBinding,
233 /// Hop-count contributors in path order.
234 hop_contributors: Vec<HopContributor>,
235 },
236 /// Restrictive path-mode wrapper over one complete path pattern.
237 ///
238 /// The child materializes every candidate row. `PathModeFilter` then applies
239 /// per-row binding-path validation for `TRAIL`, `SIMPLE`, or `ACYCLIC` using
240 /// the explicit ordered contributors captured during lowering.
241 PathModeFilter {
242 /// Restrictive path mode to validate.
243 path_mode: PathMode,
244 /// Complete path-pattern child.
245 child: Box<JoinTree>,
246 /// Ordered node and edge contributors in binding-path order.
247 path_contributors: Vec<PathContributor>,
248 },
249 /// Pattern-wide match-mode wrapper over a whole `<graph pattern>`.
250 ///
251 /// Per ISO/IEC 39075:2024 §16.4, a `<match mode>` is a prefix on the entire
252 /// comma-separated path-pattern list of one MATCH (not a per-path-pattern
253 /// modifier like [`Self::PathModeFilter`]). The child materializes every
254 /// candidate row across all path patterns; this wrapper then applies the
255 /// pattern-wide edge-uniqueness filter for `DIFFERENT EDGES` (§16.4 GR4 /
256 /// GR8(a)) over the union of every edge column in the row. `REPEATABLE
257 /// ELEMENTS` installs no filter (GR8(b): BINDINGS = INNER), so the lowering
258 /// never constructs this variant for that mode.
259 MatchModeFilter {
260 /// Match mode to enforce. Only [`MatchMode::DifferentEdges`] reaches a
261 /// constructed wrapper; the variant is kept mode-tagged for EXPLAIN and
262 /// for an exhaustive runtime match.
263 match_mode: MatchMode,
264 /// Complete graph-pattern child spanning all path patterns of the MATCH.
265 child: Box<JoinTree>,
266 /// Every edge contributor across all path patterns, in binding-path
267 /// order. The union enables pattern-wide deduplication per §16.4 GR4.
268 path_contributors: Vec<PathContributor>,
269 },
270 /// Binary join between two pattern fragments.
271 HashJoin {
272 /// Left input.
273 left: Box<JoinTree>,
274 /// Right input.
275 right: Box<JoinTree>,
276 /// Shared binding names used as the join key.
277 key: Vec<selene_core::DbString>,
278 /// Planner-selected build input.
279 build_side: BuildSide,
280 },
281 /// Left-outer join used for OPTIONAL MATCH.
282 Outer {
283 /// Preserved left input.
284 left: Box<JoinTree>,
285 /// Optional right input.
286 right: Box<JoinTree>,
287 /// Shared binding names used as the join key.
288 key: Vec<selene_core::DbString>,
289 /// Predicates scoped to the optional right side.
290 right_filters: Vec<FilterPredicate>,
291 },
292 /// Marker for future WCO rewrites.
293 WorstCaseOptimal {
294 /// Intersected subplans.
295 intersection: Vec<JoinTree>,
296 /// Node-id orderings used to break symmetric WCO traversals.
297 node_id_ordering: Vec<NodeIdOrdering>,
298 },
299 /// A fully nested [`ExecutionPlan`] executed as a single join-tree node.
300 ///
301 /// Reserved for correlated-`CALL` subquery lowering: a `CALL { ... }` whose
302 /// body imports outer bindings will lower to this variant so the inner
303 /// pipeline runs per outer row. No production lowering rule constructs it
304 /// yet (correlated-`CALL` is not lowered at HEAD; only tests build it), but
305 /// the runtime path is complete — the sole executor is the runtime subplan
306 /// executor reached from the `JoinTree::Subplan` arm of pattern walking. It
307 /// is kept (not removed) as the working substrate for that near-term
308 /// direction.
309 Subplan(Box<ExecutionPlan>),
310 /// Per-label sub-scans wrapping a flat-disjunctive-label pattern.
311 ///
312 /// Emitted by the `disjunctive_label_expansion` optimizer rule when a node
313 /// scan carries a flat `LabelExpr::Disjunction([Single, Single, …])` label
314 /// expression and at least one per-label branch has an applicable typed,
315 /// composite, or in-list index. Each branch is a clone of the original
316 /// scan with `label_predicate = Some(LabelExpr::Single(L_i))`, allowing the
317 /// downstream index-selection rules (`composite_index_lookup`,
318 /// `in_list_optimization`, `range_index_scan`) to set per-branch
319 /// `ScanAccess` independently.
320 ///
321 /// Runtime executes each branch via the standard `scan_pattern` entry and
322 /// concatenates the per-branch `Binding` rows with `UNION ALL` semantics
323 /// (no dedup; a node carrying labels A AND B appears in both branches'
324 /// candidate sets, matching the manual `MATCH (n:A) UNION ALL MATCH (n:B)`
325 /// behaviour). Per-branch label filtering applies via the existing
326 /// `label_matches_scan` machinery against each branch's single-label
327 /// predicate.
328 DisjunctiveScan {
329 /// Per-label sub-scans, each with `label_predicate =
330 /// Some(LabelExpr::Single(L_i))` and a clone of the original scan's
331 /// property predicates + bindings.
332 ///
333 /// `Vec`, not `Vec2OrMore`, because the source
334 /// `LabelExpr::Disjunction(Vec2OrMore<LabelExpr>)` already guarantees
335 /// `≥ 2` branches at construction time.
336 branches: Vec<NodeOrEdgeScan>,
337 /// The original scan, retained for EXPLAIN diagnostics and to preserve
338 /// the original disjunctive `label_predicate` for post-commit walks.
339 /// Carries the same `binding` / `hidden_binding` IDs that the branches
340 /// inherit, so downstream pipeline ops resolve `(n)` against the
341 /// unioned binding table consistently.
342 scan_anchor: NodeOrEdgeScan,
343 },
344}
345
346/// Planner-selected hash-join build side.
347#[derive(Clone, Copy, Debug, Eq, PartialEq)]
348pub enum BuildSide {
349 /// Build the hash table from the left input.
350 Left,
351 /// Build the hash table from the right input.
352 Right,
353}
354
355/// Executor-private binding slot for anonymous pattern elements.
356#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
357pub struct HiddenBindingId(u32);
358
359impl HiddenBindingId {
360 pub(crate) const fn new(raw: u32) -> Self {
361 Self(raw)
362 }
363
364 /// Return this hidden slot's zero-based numeric index.
365 #[must_use]
366 pub const fn get(self) -> u32 {
367 self.0
368 }
369}
370
371/// Node or edge scan.
372#[derive(Clone, Debug, PartialEq)]
373pub struct NodeOrEdgeScan {
374 /// Named binding, or `None` for anonymous pattern elements.
375 pub binding: Option<BindingId>,
376 /// Executor-private slot for anonymous scan elements.
377 pub hidden_binding: Option<HiddenBindingId>,
378 /// Scan kind.
379 pub kind: ScanKind,
380 /// Label predicate attached to the scanned element.
381 pub label_predicate: Option<LabelExpr>,
382 /// Inline property predicates from the pattern.
383 pub property_predicates: Vec<FilterPredicate>,
384 /// Optimizer-selected access path.
385 pub access: ScanAccess,
386 /// Source span.
387 pub span: SourceSpan,
388}
389
390/// Scan element kind.
391#[derive(Clone, Copy, Debug, Eq, PartialEq)]
392pub enum ScanKind {
393 /// Node scan.
394 Node,
395 /// Edge scan.
396 Edge,
397}
398
399/// Edge pattern in an expansion.
400#[derive(Clone, Debug, PartialEq)]
401pub struct EdgeMatch {
402 /// Named edge binding, or `None` for anonymous edge patterns.
403 pub binding: Option<BindingId>,
404 /// Executor-private slot for anonymous edge patterns.
405 pub hidden_binding: Option<HiddenBindingId>,
406 /// Label predicate attached to the edge.
407 pub label_predicate: Option<LabelExpr>,
408 /// Inline property predicates from the edge pattern.
409 pub property_predicates: Vec<FilterPredicate>,
410 /// Binding on the syntactic left side of the edge, if named.
411 pub left_binding: Option<BindingId>,
412 /// Executor-private slot on the syntactic left side of the edge.
413 pub left_hidden_binding: Option<HiddenBindingId>,
414 /// Binding on the syntactic right side of the edge, if named.
415 pub right_binding: Option<BindingId>,
416 /// Executor-private slot on the syntactic right side of the edge.
417 pub right_hidden_binding: Option<HiddenBindingId>,
418 /// Label predicate on the syntactic right-side node, if any.
419 pub right_label_predicate: Option<LabelExpr>,
420 /// Property-map equality predicates on the syntactic right-side node.
421 pub right_property_predicates: Vec<FilterPredicate>,
422 /// Optimizer-selected access path.
423 pub access: ScanAccess,
424 /// Source span.
425 pub span: SourceSpan,
426}
427
428/// Quantified edge pattern in a variable-length expansion.
429#[derive(Clone, Debug, PartialEq)]
430pub struct RepeatEdgeMatch {
431 /// Named group edge binding, or `None` for anonymous quantified edges.
432 pub group_binding: Option<BindingId>,
433 /// Hidden group edge binding for anonymous quantified edges under selectors.
434 pub group_hidden_binding: Option<HiddenBindingId>,
435 /// Label predicate attached to each traversed edge.
436 pub label_predicate: Option<LabelExpr>,
437 /// Property-map predicates evaluated against each traversed edge.
438 pub property_predicates: Vec<FilterPredicate>,
439 /// Inline edge predicates evaluated once per traversed edge.
440 pub inline_predicates: Vec<FilterPredicate>,
441 /// Binding on the syntactic left side of the repeat, if named.
442 pub left_binding: Option<BindingId>,
443 /// Executor-private slot on the syntactic left side of the repeat.
444 pub left_hidden_binding: Option<HiddenBindingId>,
445 /// Binding on the syntactic final node, if named.
446 pub final_binding: Option<BindingId>,
447 /// Executor-private slot on the syntactic final node.
448 pub final_hidden_binding: Option<HiddenBindingId>,
449 /// Label predicate on the syntactic final node, if any.
450 pub final_label_predicate: Option<LabelExpr>,
451 /// Property-map equality predicates on the syntactic final node.
452 pub final_property_predicates: Vec<FilterPredicate>,
453 /// Optimizer-selected access path for future repeat-aware planning.
454 pub access: ScanAccess,
455 /// Source span.
456 pub span: SourceSpan,
457}
458
459/// Pipeline operation over binding tables.
460///
461/// `#[non_exhaustive]` so future planner work (e.g., MERGE lowering, CALL
462/// subquery form, INDEX DDL) can add variants without breaking downstream
463/// pattern matches.
464#[derive(Clone, Debug)]
465#[allow(clippy::large_enum_variant)]
466#[non_exhaustive]
467pub enum PipelineOp {
468 /// Retain rows satisfying a predicate.
469 Filter(FilterPredicate),
470 /// Project expressions into output columns.
471 Project(Vec<ProjectExpr>),
472 /// Extend the binding table with new aliases without dropping prior columns.
473 Let(Vec<ProjectExpr>),
474 /// Expand a list expression to one row per element.
475 Unwind {
476 /// Source list expression.
477 source: ProjectExpr,
478 /// Alias bound to each list element.
479 alias: selene_core::DbString,
480 /// Optional position output for ISO `FOR`.
481 position: Option<crate::RowExpansionPosition>,
482 /// Source span.
483 span: SourceSpan,
484 },
485 /// Sort rows.
486 OrderBy(Vec<OrderKey>),
487 /// Offset and limit rows.
488 Limit {
489 /// Rows to skip.
490 offset: LimitAmount,
491 /// Rows to retain after offset.
492 count: LimitAmount,
493 },
494 /// Sort rows while retaining only the bounded top range.
495 TopK {
496 /// Sort keys preserved from the fused `OrderBy`.
497 keys: Vec<OrderKey>,
498 /// Rows to skip before yielding.
499 offset: LimitAmount,
500 /// Rows to retain after offset.
501 count: LimitAmount,
502 },
503 /// Group and aggregate rows.
504 GroupBy {
505 /// Grouping keys.
506 keys: Vec<ProjectExpr>,
507 /// Aggregate expressions.
508 aggregates: Vec<Aggregate>,
509 },
510 /// Deduplicate rows.
511 Distinct,
512 /// Apply a set-composition operation with another plan.
513 Union {
514 /// Parser set operator, preserved exactly.
515 op: SetOp,
516 /// Right-hand plan.
517 rhs: Box<ExecutionPlan>,
518 },
519 /// Evaluate a NEXT block after the current plan.
520 Chain(Box<ExecutionPlan>),
521 /// Evaluate a NEXT block once per input row because it references prior bindings.
522 CorrelatedChain(Box<ExecutionPlan>),
523 /// Match a graph pattern against each incoming row.
524 Match(PatternPlan),
525 /// Optionally match a graph pattern against each incoming row.
526 OptionalMatch(PatternPlan),
527 /// Planned procedure call.
528 Call(PlannedCall),
529 /// Inline `CALL { ... }` table subquery.
530 CallSubquery(Box<PlannedTableSubquery>),
531 /// Mutation operation.
532 Mutation(MutationOp),
533 /// Catalog operation.
534 Catalog(CatalogOp),
535 /// Return a textual dump of an inner execution plan.
536 ExplainPlan {
537 /// Planned inner statement. It is never executed by this operation.
538 inner: Box<ExecutionPlan>,
539 /// Source span.
540 span: SourceSpan,
541 },
542 /// Transaction-control operation.
543 Tx(TxOp),
544 /// Session-control operation (ISO/IEC 39075:2024 section 7).
545 Session(SessionOp),
546}
547
548/// Binding-table output schema.
549#[derive(Clone, Debug, Eq, PartialEq)]
550pub struct BindingTableSchema {
551 /// Output columns in order.
552 pub columns: Vec<BindingTableColumn>,
553}
554
555/// One binding-table output column.
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct BindingTableColumn {
558 /// Stable column name for aliases and bare variable projections.
559 pub name: Option<selene_core::DbString>,
560 /// Executor-private anonymous pattern slot.
561 pub hidden: Option<HiddenBindingId>,
562 /// Analyzer-inferred column type.
563 pub ty: AnalyzedType,
564}
565
566/// Path binding placeholder.
567#[derive(Clone, Debug, Eq, PartialEq)]
568pub struct PathPlan {
569 /// Analyzer path binding.
570 pub binding: BindingId,
571 /// Source span.
572 pub span: SourceSpan,
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn repeat_join_tree_carries_group_and_final_node_slots_separately() {
581 let tree = JoinTree::Repeat {
582 child: Box::new(JoinTree::Scan(NodeOrEdgeScan {
583 binding: None,
584 hidden_binding: Some(HiddenBindingId::new(0)),
585 kind: ScanKind::Node,
586 label_predicate: None,
587 property_predicates: Vec::new(),
588 access: ScanAccess::Linear,
589 span: SourceSpan::default(),
590 })),
591 edge: RepeatEdgeMatch {
592 group_binding: None,
593 group_hidden_binding: None,
594 label_predicate: None,
595 property_predicates: Vec::new(),
596 inline_predicates: Vec::new(),
597 left_binding: None,
598 left_hidden_binding: Some(HiddenBindingId::new(0)),
599 final_binding: None,
600 final_hidden_binding: Some(HiddenBindingId::new(1)),
601 final_label_predicate: None,
602 final_property_predicates: Vec::new(),
603 access: ScanAccess::Linear,
604 span: SourceSpan::default(),
605 },
606 direction: EdgeDirection::Right,
607 min: 0,
608 max: Some(2),
609 path_mode: PathMode::Walk,
610 };
611
612 let JoinTree::Repeat { edge, min, max, .. } = tree else {
613 panic!("expected repeat");
614 };
615 assert_eq!(edge.group_binding, None);
616 assert_eq!(edge.group_hidden_binding, None);
617 assert_eq!(edge.left_hidden_binding, Some(HiddenBindingId::new(0)));
618 assert_eq!(edge.final_hidden_binding, Some(HiddenBindingId::new(1)));
619 assert_eq!(min, 0);
620 assert_eq!(max, Some(2));
621 }
622}