// RelAlgIR: String Diagram IR for Relational Algebra // // Query plans are instances of this theory. The string diagram structure: // - Wire elements are edges (carrying typed data streams) // - Op elements are boxes (transforming data) // - Composition is implicit via wire sharing (same Wire as output of one Op and input of another) // - Cycles are allowed; well-formedness axioms ensure they contain delays // // See loose_thoughts/2026-01-19_19:45_relalg_ir_design.md for full design. // // This theory extends GeologMeta to get Srt, Func, Elem, etc. // References use qualified names: GeologMeta/Srt, GeologMeta/Func, etc. theory RelAlgIR extends GeologMeta { // ============================================================ // SCHEMAS (types of data on wires) // ============================================================ // Schemas describe the "shape" of tuples flowing on a wire. // They mirror DSort but are specific to the relational algebra context. Schema : Sort; // Unit schema: empty tuple (for sources with no input) UnitSchema : Sort; UnitSchema/schema : UnitSchema -> Schema; // Base schema: single column of a given sort BaseSchema : Sort; BaseSchema/schema : BaseSchema -> Schema; BaseSchema/srt : BaseSchema -> GeologMeta/Srt; // Product schema: S ⊗ T (concatenation of columns) ProdSchema : Sort; ProdSchema/schema : ProdSchema -> Schema; ProdSchema/left : ProdSchema -> Schema; ProdSchema/right : ProdSchema -> Schema; // ============================================================ // WIRES (edges in the string diagram) // ============================================================ // Wires are first-class citizens. Each wire carries a stream of // tuples with a given schema. Composition is encoded by the same // Wire appearing as output of one Op and input of another. Wire : Sort; Wire/schema : Wire -> Schema; // ============================================================ // OPERATIONS (boxes in the string diagram) // ============================================================ Op : Sort; // ------------------------------------------------------------ // Sources (no input wires) // ------------------------------------------------------------ // Scan: emit all elements of a sort // () → BaseSchema(srt) ScanOp : Sort; ScanOp/op : ScanOp -> Op; ScanOp/srt : ScanOp -> GeologMeta/Srt; ScanOp/out : ScanOp -> Wire; // Constant: emit a single known element // () → BaseSchema(elem's sort) ConstOp : Sort; ConstOp/op : ConstOp -> Op; ConstOp/elem : ConstOp -> GeologMeta/Elem; ConstOp/out : ConstOp -> Wire; // Empty: emit nothing (identity for union) // () → S EmptyOp : Sort; EmptyOp/op : EmptyOp -> Op; EmptyOp/out : EmptyOp -> Wire; // ------------------------------------------------------------ // Unary operations (one input wire, one output wire) // ------------------------------------------------------------ // Filter: keep tuples satisfying a predicate // S → S FilterOp : Sort; FilterOp/op : FilterOp -> Op; FilterOp/in : FilterOp -> Wire; FilterOp/out : FilterOp -> Wire; FilterOp/pred : FilterOp -> Pred; // Project: select and reorder columns // S → T ProjectOp : Sort; ProjectOp/op : ProjectOp -> Op; ProjectOp/in : ProjectOp -> Wire; ProjectOp/out : ProjectOp -> Wire; ProjectOp/mapping : ProjectOp -> ProjMapping; // Distinct: deduplicate tuples (collapse multiplicities to 0/1) // S → S DistinctOp : Sort; DistinctOp/op : DistinctOp -> Op; DistinctOp/in : DistinctOp -> Wire; DistinctOp/out : DistinctOp -> Wire; // Negate: flip multiplicities (for computing differences) // S → S NegateOp : Sort; NegateOp/op : NegateOp -> Op; NegateOp/in : NegateOp -> Wire; NegateOp/out : NegateOp -> Wire; // Apply function: add a column by applying a function // S → S ⊗ BaseSchema(cod) ApplyOp : Sort; ApplyOp/op : ApplyOp -> Op; ApplyOp/in : ApplyOp -> Wire; ApplyOp/out : ApplyOp -> Wire; ApplyOp/func : ApplyOp -> GeologMeta/Func; ApplyOp/arg_col : ApplyOp -> ColRef; // ------------------------------------------------------------ // Binary operations (two input wires, one output wire) // ------------------------------------------------------------ // Join: combine tuples from two sources where condition holds // S × T → S ⊗ T (filtered) JoinOp : Sort; JoinOp/op : JoinOp -> Op; JoinOp/left_in : JoinOp -> Wire; JoinOp/right_in : JoinOp -> Wire; JoinOp/out : JoinOp -> Wire; JoinOp/cond : JoinOp -> JoinCond; // Union: combine tuples from two sources (Z-set addition) // S × S → S UnionOp : Sort; UnionOp/op : UnionOp -> Op; UnionOp/left_in : UnionOp -> Wire; UnionOp/right_in : UnionOp -> Wire; UnionOp/out : UnionOp -> Wire; // ------------------------------------------------------------ // DBSP Temporal Operators // ------------------------------------------------------------ // These operate on streams over discrete time. // They are essential for incremental computation and feedback loops. // Delay: z⁻¹, output at time t is input at time t-1 // S → S // IMPORTANT: Delays break instantaneous cycles, making feedback well-founded. DelayOp : Sort; DelayOp/op : DelayOp -> Op; DelayOp/in : DelayOp -> Wire; DelayOp/out : DelayOp -> Wire; // Differentiate: δ = 1 - z⁻¹, compute changes since last timestep // S → S (output is the delta/diff of input) DiffOp : Sort; DiffOp/op : DiffOp -> Op; DiffOp/in : DiffOp -> Wire; DiffOp/out : DiffOp -> Wire; // Integrate: ∫ = Σ, accumulate all inputs over time // S → S (output is running sum of all inputs) // NOTE: Has implicit delay semantics, also breaks instantaneous cycles. IntegrateOp : Sort; IntegrateOp/op : IntegrateOp -> Op; IntegrateOp/in : IntegrateOp -> Wire; IntegrateOp/out : IntegrateOp -> Wire; // ============================================================ // PREDICATES (for filter conditions) // ============================================================ Pred : Sort; // True: always satisfied TruePred : Sort; TruePred/pred : TruePred -> Pred; // False: never satisfied FalsePred : Sort; FalsePred/pred : FalsePred -> Pred; // Column equality: col_i = col_j ColEqPred : Sort; ColEqPred/pred : ColEqPred -> Pred; ColEqPred/left : ColEqPred -> ColRef; ColEqPred/right : ColEqPred -> ColRef; // Constant equality: col = constant element ConstEqPred : Sort; ConstEqPred/pred : ConstEqPred -> Pred; ConstEqPred/col : ConstEqPred -> ColRef; ConstEqPred/val : ConstEqPred -> GeologMeta/Elem; // Function result equality: f(col_arg) = col_result FuncEqPred : Sort; FuncEqPred/pred : FuncEqPred -> Pred; FuncEqPred/func : FuncEqPred -> GeologMeta/Func; FuncEqPred/arg : FuncEqPred -> ColRef; FuncEqPred/result : FuncEqPred -> ColRef; // Function result equals constant: f(col_arg) = expected_elem FuncConstEqPred : Sort; FuncConstEqPred/pred : FuncConstEqPred -> Pred; FuncConstEqPred/func : FuncConstEqPred -> GeologMeta/Func; FuncConstEqPred/arg : FuncConstEqPred -> ColRef; FuncConstEqPred/expected : FuncConstEqPred -> GeologMeta/Elem; // Conjunction: p ∧ q AndPred : Sort; AndPred/pred : AndPred -> Pred; AndPred/left : AndPred -> Pred; AndPred/right : AndPred -> Pred; // Disjunction: p ∨ q OrPred : Sort; OrPred/pred : OrPred -> Pred; OrPred/left : OrPred -> Pred; OrPred/right : OrPred -> Pred; // ============================================================ // JOIN CONDITIONS // ============================================================ JoinCond : Sort; // Equijoin: left.col_i = right.col_j EquiJoinCond : Sort; EquiJoinCond/cond : EquiJoinCond -> JoinCond; EquiJoinCond/left_col : EquiJoinCond -> ColRef; EquiJoinCond/right_col : EquiJoinCond -> ColRef; // Cross join: cartesian product (no condition) CrossJoinCond : Sort; CrossJoinCond/cond : CrossJoinCond -> JoinCond; // General predicate join PredJoinCond : Sort; PredJoinCond/cond : PredJoinCond -> JoinCond; PredJoinCond/pred : PredJoinCond -> Pred; // ============================================================ // COLUMN REFERENCES // ============================================================ // References to specific columns within a schema. // Used in predicates and projections. ColRef : Sort; ColRef/wire : ColRef -> Wire; // which wire's schema we're referencing ColRef/path : ColRef -> ColPath; // path into the schema // Column path: navigate into nested product schemas ColPath : Sort; // Here: we're at the target HerePath : Sort; HerePath/path : HerePath -> ColPath; // Left: descend into left of product LeftPath : Sort; LeftPath/path : LeftPath -> ColPath; LeftPath/rest : LeftPath -> ColPath; // Right: descend into right of product RightPath : Sort; RightPath/path : RightPath -> ColPath; RightPath/rest : RightPath -> ColPath; // ============================================================ // PROJECTION MAPPINGS // ============================================================ // Specifies how to construct output columns from input columns. ProjMapping : Sort; // Projection entries (which input columns become which output columns) ProjEntry : Sort; ProjEntry/mapping : ProjEntry -> ProjMapping; ProjEntry/source : ProjEntry -> ColRef; ProjEntry/target_path : ProjEntry -> ColPath; // ============================================================ // REACHABILITY RELATIONS (for cycle analysis) // ============================================================ // w1 reaches w2 via some path through operations reaches : [from: Wire, to: Wire] -> Prop; // Reachability through each operation type ax/reaches/scan : forall s : ScanOp, w : Wire. s ScanOp/out = w |- [from: w, to: w] reaches; // trivial self-reach for source ax/reaches/filter : forall f : FilterOp, w1 : Wire, w2 : Wire. f FilterOp/in = w1, f FilterOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/project : forall p : ProjectOp, w1 : Wire, w2 : Wire. p ProjectOp/in = w1, p ProjectOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/distinct : forall d : DistinctOp, w1 : Wire, w2 : Wire. d DistinctOp/in = w1, d DistinctOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/negate : forall n : NegateOp, w1 : Wire, w2 : Wire. n NegateOp/in = w1, n NegateOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/apply : forall a : ApplyOp, w1 : Wire, w2 : Wire. a ApplyOp/in = w1, a ApplyOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/join/left : forall j : JoinOp, w1 : Wire, w2 : Wire. j JoinOp/left_in = w1, j JoinOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/join/right : forall j : JoinOp, w1 : Wire, w2 : Wire. j JoinOp/right_in = w1, j JoinOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/union/left : forall u : UnionOp, w1 : Wire, w2 : Wire. u UnionOp/left_in = w1, u UnionOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/union/right : forall u : UnionOp, w1 : Wire, w2 : Wire. u UnionOp/right_in = w1, u UnionOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/delay : forall d : DelayOp, w1 : Wire, w2 : Wire. d DelayOp/in = w1, d DelayOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/diff : forall d : DiffOp, w1 : Wire, w2 : Wire. d DiffOp/in = w1, d DiffOp/out = w2 |- [from: w1, to: w2] reaches; ax/reaches/integrate : forall i : IntegrateOp, w1 : Wire, w2 : Wire. i IntegrateOp/in = w1, i IntegrateOp/out = w2 |- [from: w1, to: w2] reaches; // Transitive closure ax/reaches/trans : forall w1 : Wire, w2 : Wire, w3 : Wire. [from: w1, to: w2] reaches, [from: w2, to: w3] reaches |- [from: w1, to: w3] reaches; // ============================================================ // INSTANTANEOUS REACHABILITY (paths without delay) // ============================================================ // This relation tracks paths that do NOT go through DelayOp or IntegrateOp. // Used to detect "bad" feedback loops that would require instantaneous computation. reaches_instant : [from: Wire, to: Wire] -> Prop; // Same axioms as reaches, EXCEPT for DelayOp and IntegrateOp ax/reaches_instant/filter : forall f : FilterOp, w1 : Wire, w2 : Wire. f FilterOp/in = w1, f FilterOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/project : forall p : ProjectOp, w1 : Wire, w2 : Wire. p ProjectOp/in = w1, p ProjectOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/distinct : forall d : DistinctOp, w1 : Wire, w2 : Wire. d DistinctOp/in = w1, d DistinctOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/negate : forall n : NegateOp, w1 : Wire, w2 : Wire. n NegateOp/in = w1, n NegateOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/apply : forall a : ApplyOp, w1 : Wire, w2 : Wire. a ApplyOp/in = w1, a ApplyOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/join/left : forall j : JoinOp, w1 : Wire, w2 : Wire. j JoinOp/left_in = w1, j JoinOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/join/right : forall j : JoinOp, w1 : Wire, w2 : Wire. j JoinOp/right_in = w1, j JoinOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/union/left : forall u : UnionOp, w1 : Wire, w2 : Wire. u UnionOp/left_in = w1, u UnionOp/out = w2 |- [from: w1, to: w2] reaches_instant; ax/reaches_instant/union/right : forall u : UnionOp, w1 : Wire, w2 : Wire. u UnionOp/right_in = w1, u UnionOp/out = w2 |- [from: w1, to: w2] reaches_instant; // NOTE: No axioms for DelayOp or IntegrateOp! // They break instantaneous reachability. // DiffOp is instantaneous (it uses delay internally but outputs immediately) ax/reaches_instant/diff : forall d : DiffOp, w1 : Wire, w2 : Wire. d DiffOp/in = w1, d DiffOp/out = w2 |- [from: w1, to: w2] reaches_instant; // Transitive closure ax/reaches_instant/trans : forall w1 : Wire, w2 : Wire, w3 : Wire. [from: w1, to: w2] reaches_instant, [from: w2, to: w3] reaches_instant |- [from: w1, to: w3] reaches_instant; // ============================================================ // WELL-FORMEDNESS: NO INSTANTANEOUS CYCLES // ============================================================ // Every cycle must contain at least one DelayOp or IntegrateOp. // This ensures feedback loops are computable via iteration. ax/wf/no_instant_cycle : forall w : Wire. [from: w, to: w] reaches_instant |- false; // ============================================================ // WELL-FORMEDNESS: SCHEMA CONSISTENCY // ============================================================ // Operations must connect wires with compatible schemas. // Filter preserves schema ax/wf/filter_schema : forall f : FilterOp, w1 : Wire, w2 : Wire. f FilterOp/in = w1, f FilterOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Distinct preserves schema ax/wf/distinct_schema : forall d : DistinctOp, w1 : Wire, w2 : Wire. d DistinctOp/in = w1, d DistinctOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Negate preserves schema ax/wf/negate_schema : forall n : NegateOp, w1 : Wire, w2 : Wire. n NegateOp/in = w1, n NegateOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Delay preserves schema ax/wf/delay_schema : forall d : DelayOp, w1 : Wire, w2 : Wire. d DelayOp/in = w1, d DelayOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Diff preserves schema ax/wf/diff_schema : forall d : DiffOp, w1 : Wire, w2 : Wire. d DiffOp/in = w1, d DiffOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Integrate preserves schema ax/wf/integrate_schema : forall i : IntegrateOp, w1 : Wire, w2 : Wire. i IntegrateOp/in = w1, i IntegrateOp/out = w2 |- w1 Wire/schema = w2 Wire/schema; // Union requires same schema on both inputs ax/wf/union_schema_left : forall u : UnionOp, wl : Wire, wr : Wire, wo : Wire. u UnionOp/left_in = wl, u UnionOp/right_in = wr, u UnionOp/out = wo |- wl Wire/schema = wo Wire/schema; ax/wf/union_schema_right : forall u : UnionOp, wl : Wire, wr : Wire, wo : Wire. u UnionOp/left_in = wl, u UnionOp/right_in = wr, u UnionOp/out = wo |- wr Wire/schema = wo Wire/schema; // Scan output schema must be BaseSchema of the scanned sort // (This requires existential in conclusion, which geometric logic supports) ax/wf/scan_schema : forall s : ScanOp, srt : GeologMeta/Srt, w : Wire. s ScanOp/srt = srt, s ScanOp/out = w |- exists bs : BaseSchema. bs BaseSchema/srt = srt, w Wire/schema = bs BaseSchema/schema; // Join output schema is product of input schemas ax/wf/join_schema : forall j : JoinOp, wl : Wire, wr : Wire, wo : Wire. j JoinOp/left_in = wl, j JoinOp/right_in = wr, j JoinOp/out = wo |- exists ps : ProdSchema. ps ProdSchema/left = wl Wire/schema, ps ProdSchema/right = wr Wire/schema, wo Wire/schema = ps ProdSchema/schema; // ============================================================ // ALGEBRAIC LAWS (for query optimization) // ============================================================ // These axioms express equivalences between query plans. // An optimizer uses these to transform plans into more efficient forms. // // Notation: We describe semantic equivalence between operators. // In practice, equivalence means the output wire produces the same Z-set. // // These are stated as properties (Prop-valued relations) rather than // equational axioms, since geolog's geometric logic doesn't have // built-in equality on terms. The optimizer interprets these as rewrite rules. // Wire equivalence: two wires produce the same Z-set equiv : [a: Wire, b: Wire] -> Prop; // Reflexivity ax/equiv/refl : forall w : Wire. |- [a: w, b: w] equiv; // Symmetry ax/equiv/sym : forall w1 : Wire, w2 : Wire. [a: w1, b: w2] equiv |- [a: w2, b: w1] equiv; // Transitivity ax/equiv/trans : forall w1 : Wire, w2 : Wire, w3 : Wire. [a: w1, b: w2] equiv, [a: w2, b: w3] equiv |- [a: w1, b: w3] equiv; // ------------------------------------------------------------ // Filter Laws // ------------------------------------------------------------ // Filter(True, x) ≡ x ax/filter_true : forall f : FilterOp, t : TruePred, wi : Wire, wo : Wire. f FilterOp/pred = t TruePred/pred, f FilterOp/in = wi, f FilterOp/out = wo |- [a: wo, b: wi] equiv; // Filter(False, x) ≡ Empty // (Every tuple is filtered out, result is empty) // This would need EmptyOp with matching schema; omitted for simplicity. // Filter-Filter Fusion: Filter(p, Filter(q, x)) ≡ Filter(p ∧ q, x) // Expressed as: If f2.in = f1.out, then there exists a fused filter. ax/filter_fusion : forall f1 : FilterOp, f2 : FilterOp, p1 : Pred, p2 : Pred, wi : Wire, wm : Wire, wo : Wire. f1 FilterOp/in = wi, f1 FilterOp/out = wm, f1 FilterOp/pred = p1, f2 FilterOp/in = wm, f2 FilterOp/out = wo, f2 FilterOp/pred = p2 |- exists f3 : FilterOp, pa : AndPred. pa AndPred/left = p1, pa AndPred/right = p2, f3 FilterOp/in = wi, f3 FilterOp/pred = pa AndPred/pred, [a: wo, b: f3 FilterOp/out] equiv; // ------------------------------------------------------------ // Distinct Laws // ------------------------------------------------------------ // Distinct is idempotent: Distinct(Distinct(x)) ≡ Distinct(x) ax/distinct_idem : forall d1 : DistinctOp, d2 : DistinctOp, wi : Wire, wm : Wire, wo : Wire. d1 DistinctOp/in = wi, d1 DistinctOp/out = wm, d2 DistinctOp/in = wm, d2 DistinctOp/out = wo |- [a: wo, b: wm] equiv; // ------------------------------------------------------------ // Union Laws // ------------------------------------------------------------ // Union is commutative: Union(x, y) ≡ Union(y, x) ax/union_comm : forall u1 : UnionOp, wl : Wire, wr : Wire, wo : Wire. u1 UnionOp/left_in = wl, u1 UnionOp/right_in = wr, u1 UnionOp/out = wo |- exists u2 : UnionOp. u2 UnionOp/left_in = wr, u2 UnionOp/right_in = wl, [a: wo, b: u2 UnionOp/out] equiv; // Union is associative: Union(x, Union(y, z)) ≡ Union(Union(x, y), z) ax/union_assoc : forall u1 : UnionOp, u2 : UnionOp, wa : Wire, wb : Wire, wc : Wire, wyz : Wire, wo : Wire. u2 UnionOp/left_in = wb, u2 UnionOp/right_in = wc, u2 UnionOp/out = wyz, u1 UnionOp/left_in = wa, u1 UnionOp/right_in = wyz, u1 UnionOp/out = wo |- exists u3 : UnionOp, u4 : UnionOp, wab : Wire. u3 UnionOp/left_in = wa, u3 UnionOp/right_in = wb, u3 UnionOp/out = wab, u4 UnionOp/left_in = wab, u4 UnionOp/right_in = wc, [a: wo, b: u4 UnionOp/out] equiv; // Union with Empty: Union(x, Empty) ≡ x ax/union_empty_right : forall u : UnionOp, e : EmptyOp, wi : Wire, we : Wire, wo : Wire. e EmptyOp/out = we, u UnionOp/left_in = wi, u UnionOp/right_in = we, u UnionOp/out = wo |- [a: wo, b: wi] equiv; ax/union_empty_left : forall u : UnionOp, e : EmptyOp, wi : Wire, we : Wire, wo : Wire. e EmptyOp/out = we, u UnionOp/left_in = we, u UnionOp/right_in = wi, u UnionOp/out = wo |- [a: wo, b: wi] equiv; // ------------------------------------------------------------ // Negate Laws // ------------------------------------------------------------ // Double negation: Negate(Negate(x)) ≡ x ax/negate_involution : forall n1 : NegateOp, n2 : NegateOp, wi : Wire, wm : Wire, wo : Wire. n1 NegateOp/in = wi, n1 NegateOp/out = wm, n2 NegateOp/in = wm, n2 NegateOp/out = wo |- [a: wo, b: wi] equiv; // ------------------------------------------------------------ // Join Laws // ------------------------------------------------------------ // Cross join is commutative (up to column reordering) // Note: The output schemas differ, so this needs a projection to swap columns. // Omitted for now as it requires more complex schema manipulation. // Join with Empty: Join(x, Empty) ≡ Empty ax/join_empty_right : forall j : JoinOp, e : EmptyOp, wi : Wire, we : Wire, wo : Wire. e EmptyOp/out = we, j JoinOp/left_in = wi, j JoinOp/right_in = we, j JoinOp/out = wo |- exists e2 : EmptyOp. [a: wo, b: e2 EmptyOp/out] equiv; ax/join_empty_left : forall j : JoinOp, e : EmptyOp, wi : Wire, we : Wire, wo : Wire. e EmptyOp/out = we, j JoinOp/left_in = we, j JoinOp/right_in = wi, j JoinOp/out = wo |- exists e2 : EmptyOp. [a: wo, b: e2 EmptyOp/out] equiv; // ------------------------------------------------------------ // DBSP Laws // ------------------------------------------------------------ // Differentiation is inverse of integration (for streams of changes) // Diff(Integrate(x)) ≡ x (for Δ-streams) // Integrate(Diff(x)) ≡ x - x₀ (up to initial value) // These are more subtle and depend on stream semantics; omitted for now. // Delay respects Union: z⁻¹(x ∪ y) ≡ z⁻¹(x) ∪ z⁻¹(y) ax/delay_union : forall u : UnionOp, d : DelayOp, wl : Wire, wr : Wire, wu : Wire, wo : Wire. u UnionOp/left_in = wl, u UnionOp/right_in = wr, u UnionOp/out = wu, d DelayOp/in = wu, d DelayOp/out = wo |- exists dl : DelayOp, dr : DelayOp, u2 : UnionOp. dl DelayOp/in = wl, dr DelayOp/in = wr, u2 UnionOp/left_in = dl DelayOp/out, u2 UnionOp/right_in = dr DelayOp/out, [a: wo, b: u2 UnionOp/out] equiv; }