Skip to main content

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}