geolog-zeta-fork/theories/RelAlgIR.geolog

593 lines
23 KiB
Plaintext
Raw Normal View History

2026-02-26 11:50:51 +01:00
// 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;
}