Skip to content

doc/semantics: Lean 4 mechanization of UnifiedStream error semantics#36614

Draft
antiguru wants to merge 119 commits into
MaterializeInc:mainfrom
antiguru:lean-semantics-skeleton
Draft

doc/semantics: Lean 4 mechanization of UnifiedStream error semantics#36614
antiguru wants to merge 119 commits into
MaterializeInc:mainfrom
antiguru:lean-semantics-skeleton

Conversation

@antiguru
Copy link
Copy Markdown
Member

Lean 4 mechanization of Materialize's error-handling design at doc/developer/semantics/, modeling the unified-stream proposal where:

  • row-scoped errors live in the carrier (UnifiedRow.err);
  • cell-scoped errors live in Datum.err;
  • collection-scoped errors live in the diff (DiffWithError.error).

Skeleton proves the algebraic laws an optimizer pass cites. 32 lean build jobs, all green. No sorry.

Coverage of src/transform/: 13 of ~66 optimizer passes have correspondents in Mz/*.lean (filter / project / negate / union fusion, bilateral predicate pushdown, threshold elision, demand, filter idem / comm). See doc/developer/semantics/transforms.md for the full mapping plus modelable / infra-gap / out-of-scope buckets.

Build: ci/test/lean-semantics.sh (docker, lean v4.29.1, lake).

antiguru and others added 30 commits May 18, 2026 23:17
Add design doc proposing per-cell `Datum::Error`, row-scoped errors via
the existing `DataflowError`, and a diff-semiring sketch for
collection-scoped errors. Define the four-valued AND/OR truth tables
({TRUE, FALSE, NULL, ERROR}) that the rest of the spec rests on.

Add a v1 Lean 4 mechanization of the boolean fragment at
doc/developer/semantics/. The skeleton models `Datum`, `Expr`,
`evalAnd`/`evalOr`, and proves all 32 cells of the AND/OR truth tables.
The pattern order in `evalAnd`/`evalOr` matches the current Rust
runtime in src/expr/src/scalar/func/variadic.rs; deviating from that
runtime requires a corresponding diff in Boolean.lean.

CI runs lake build in a self-built Docker image (ubuntu:26.04 + elan +
lean-toolchain pin), gated on changes under doc/developer/semantics/.
No Mathlib dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand the Lean skeleton with a small set of follow-up theorems that
the optimizer roadmap depends on.

* `Datum.IsErr` propositional predicate plus `DecidablePred` instance,
  for use as a hypothesis in algebraic laws.
* `Expr.not` constructor and `evalNot`, with the 4-cell `NOT` truth
  table and `not_not` (involution on the boolean fragment, no-op on
  `null` and `err`).
* New `Mz/Laws.lean`:
  - `evalAnd_idem`, `evalOr_idem`: idempotence holds unconditionally,
    including on `err` (same error preserved).
  - `evalAnd_comm_of_no_err`, `evalOr_comm_of_no_err`: commutativity
    conditional on neither operand being an error. This is the law an
    optimizer that has run `might_error` analysis can use to justify
    conjunct reordering. Unconditional commutativity fails over
    `(err e₁, err e₂)` with distinct payloads.

Associativity and `might_error` soundness are deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the optimizer-facing theorem the previous commit's roadmap called
out as priority one.

* Switch `eval` for `.ifThen` to a strict `evalIfThen` helper. The
  observable `Datum` is identical to the lazy form in this total
  skeleton (both branches are total functions); strictness collapses
  the ifThen proof case to a single `cases` on the condition.
* Redefine `Env.get` by primitive recursion so inductive proofs can
  `cases` on the defining equations directly without going through
  `List.getD`.
* New `Mz/MightError.lean`:
  - Per-operator helper lemmas: `evalAnd_not_err`, `evalOr_not_err`,
    `evalNot_not_err`, `evalIfThen_not_err`.
  - `Expr.might_error` conservative analyzer (literal err taints
    every ancestor; columns assumed err-free).
  - `Env.ErrFree` predicate and `Env.get_not_err` lemma.
  - `might_error_sound`: if `¬e.might_error` and `env.ErrFree` then
    `¬(eval env e).IsErr`. Structural induction on `e`, one helper
    per Expr constructor.
* `Mz/Laws.lean`: lift `evalAnd_comm_of_no_err` and
  `evalOr_comm_of_no_err` through `eval` via `might_error_sound`,
  giving `Expr`-level reorder-safety theorems
  `eval_and_comm_of_no_might_error` and
  `eval_or_comm_of_no_might_error`. These are the laws an optimizer
  would cite when reordering conjuncts in the boolean fragment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* `ErrStrictUnary`, `ErrStrictBinary`, `NullStrictUnary` predicates
  capturing per-position strictness. The cell-scoped analogue of
  PostgreSQL's `STRICT` qualifier on `NULL`, applied to `err`.
* Positive instances: `evalNot` is both err-strict and null-strict;
  the condition slot of `evalIfThen` is err-strict.
* Closure: `ErrStrictUnary.comp` proves strictness is preserved under
  function composition, which is the property an optimizer relies on
  when fusing chains of strict scalar functions into a single MFP
  expression.
* Negative results: `evalAnd` and `evalOr` are not err-strict in
  either argument position. The counterexamples (`FALSE AND ERR`,
  `TRUE OR ERR`) are also canonical regression tests — a future
  change to AND/OR that promoted these cells to `ERR` would break
  exactly these proofs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `evalCoalesce : List Datum → Datum` and prove the laws that
characterize the proposed cell-scoped coalesce.

Semantics: walk operands left to right; return the first concrete
value. When none exists, apply a `null`-beats-`err` tiebreak — if
any operand was `null`, return `null`; otherwise, return the first
`err`. The state machine in `Coalesce.go` tracks a `seenNull` sticky
bit and a first-`err` payload.

The defining error-rescue property
(`coalesce_err_rescue_bool`) and its symmetric null-rescue
(`coalesce_null_rescue_bool`) together establish that a later
concrete operand rescues an earlier `err` exactly as it rescues an
earlier `null`. This is the explicit, user-controllable error trap
referenced in the design doc.

The `null`-beats-`err` tiebreak (`coalesce_null_then_err`,
`coalesce_err_then_null`) preserves the PostgreSQL behavior users
expect when all operands are NULL, and is dual to the strict-function
rule documented in `Mz/Strict.lean`: strict functions promote `err`
above `null` in per-cell results; `coalesce` is non-strict and
demotes `err` below `null` in the tiebreak.

Defer wiring `evalCoalesce` into `Expr` as a `.coalesce` constructor
until the variadic `And`/`Or` ctors land — the termination story is
shared between them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the missing identity laws and the variadic-fold counterparts of
the binary AND/OR evaluators.

* `Mz/Laws.lean`: `TRUE` is the two-sided identity for `evalAnd` and
  `FALSE` is the two-sided identity for `evalOr`. The proofs verify
  each cell of the non-identity argument. These identities are also
  the seed values used by the variadic folds.
* `Mz/Variadic.lean`: `evalAndN`, `evalOrN : List Datum → Datum`
  defined by right-fold. Right-fold gives the cons recurrence by
  `rfl` and avoids the associativity argument a left-fold would
  require.
  - Cons recurrence, nil, singleton, and binary equivalence with
    `evalAnd` / `evalOr`. The binary equivalence is the bridge that
    transports every binary truth-table cell and algebraic law to
    the variadic form on lists of length two.
  - Absorption theorems: a single `FALSE` anywhere in the operand
    list forces `evalAndN` to `FALSE`; symmetric statement for
    `TRUE` and `evalOrN`. These justify the runtime short-circuit
    optimization.

Wiring `evalAndN`, `evalOrN`, and `evalCoalesce` into `Expr` as
list-carrying constructors is deferred — the termination story for
the nested inductive is shared between them and is best landed in a
single commit once it's been validated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `.andN`, `.orN`, and `.coalesce` to `Expr` as variadic
constructors carrying `List Expr`, with the corresponding `eval`
clauses and Expr-level reduction lemmas. The variadic primitives
(`evalAndN`, `evalOrN`, `evalCoalesce`) move to a new
`Mz/PrimEval.lean` to avoid the circular dependency that would
otherwise arise between `Mz/Eval.lean` and the variadic-law files.

File reorganization:

* New `Mz/PrimEval.lean`: every evaluator that operates on `Datum`
  or `List Datum` without referring to `Expr` — the binary boolean
  primitives, the environment, and the variadic primitives.
* `Mz/Eval.lean` shrinks to the `eval : Env → Expr → Datum`
  definition.
* `Mz/Variadic.lean` and `Mz/Coalesce.lean` drop their now-moved
  evaluator definitions and keep only the theorems.
* `Mz/Boolean.lean` and `Mz/Strict.lean` switch their imports to
  `Mz.PrimEval` since they never used the Expr-level evaluator.

`might_error_sound`:

* `induction` cannot be used on the now-nested-inductive `Expr`, so
  the soundness proof is rewritten as a recursive `theorem` that
  pattern-matches the constructor and recurses on subexpressions.
  The signature also makes `env` explicit so the recursive calls
  in compound cases stay readable.
* The list-carrying constructors are handled vacuously: the
  conservative `might_error` for them returns `true` unconditionally,
  so the soundness premise is absurd in those cases. A future
  refinement will tighten the analyzer to inspect operands.

New `Mz/ExprVariadic.lean`:

* `eval_andN`, `eval_orN`, `eval_coalesce` connect the Expr-level
  evaluator to the variadic primitives.
* Empty / singleton cases for all three.
* Binary equivalence: `eval env (.andN [a, b]) = eval env (.and a b)`
  and the dual for `.orN`, transporting `Mz/Variadic.lean`'s binary
  laws through `eval`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ness

Refine `Expr.might_error` for the `andN` and `orN` list-carrying
constructors so the analyzer is no longer maximally conservative for
them. The analyzer now recurses into the operand list via
`Expr.argsMightError`, declared mutually with `Expr.might_error` so
Lean's structural-recursion checker accepts both sides without an
explicit termination measure.

`Expr.argsMightError_of_mem` is the membership-driven introduction
form needed by the soundness proof — it turns "some operand might
error" into the bool fold. Its contrapositive is what extracts a
per-operand non-erroring hypothesis from the analyzer's verdict on
the whole list.

`might_error_sound` for the `.andN` and `.orN` constructors now
performs real work:

* Reduce `eval env (.andN args)` to `evalAndN (args.map (eval env))`
  via the existing `simp only [eval]` pattern.
* Apply the list-level `evalAndN_not_err` (also added in this
  commit) with a per-operand non-erroring hypothesis.
* Extract that per-operand hypothesis by destructuring
  `List.mem_map` and applying `Expr.argsMightError_of_mem`
  contrapositively against `hMe`.
* Recurse via `might_error_sound` on the individual operand.

`.coalesce` is still tainted unconditionally — the rescue analysis
is a separate follow-up since the helper `evalCoalesce_not_err`
needs a different list induction (state-machine over `Coalesce.go`).

`Expr.might_error` and `Expr.argsMightError` are now mutually
recursive, which forces well-founded compilation. Existing `simp
only [Expr.might_error]` patterns in the proof are unaffected
because they target equation lemmas, not definitional reduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refine `Expr.might_error` for the `.coalesce` constructor so the
analyzer captures the rescue rule: `coalesce` might error only when
every operand might error (and the operand list is non-empty). A
single statically-safe operand makes the whole expression safe.

Analyzer changes (mutual block):

* `Expr.argsAllMightError`: companion fold to `argsMightError`,
  empty-list base `true`, cons case `e.might_error && rest...`.
* `.coalesce []` returns `false` (empty coalesce is `.null`, never
  errors).
* `.coalesce (a :: rest)` returns `a.might_error && argsAllMightError
  rest`, which is equal by definition to `argsAllMightError (a :: rest)`.

Soundness chain for the `.coalesce` case:

1. `Expr.exists_safe_of_not_argsAllMightError` extracts a
   statically-safe operand from the negation of the analyzer.
2. `might_error_sound` recurses on that operand and produces a
   not-`IsErr` witness on its evaluated value.
3. `Coalesce.go_not_err` (new) is the state-machine lemma: any
   `Coalesce.go` invocation whose remaining list contains at least
   one non-erroring datum (or whose `seenNull` is already set) does
   not return an error. Proof is structural recursion on the list,
   plumbing the safety witness through the `.null` and `.err` arms.
4. `evalCoalesce_not_err_of_some_safe` packages the above into the
   surface lemma soundness invokes.

`Coalesce.go` is made non-private in `Mz/PrimEval.lean` so the
state-machine lemma can reference it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the `Expr`-level absorption theorems that an optimizer cites when
folding a variadic `AND` containing a `FALSE` operand to `FALSE`
(symmetric for `OR` with `TRUE`).

Two forms each:

* Semantic premise: `(∃ e ∈ args, eval env e = .bool false) →
  eval env (.andN args) = .bool false`. Sufficient for any operand
  the optimizer has reduced to a constant boolean.
* Syntactic premise: `Expr.lit (.bool false) ∈ args → eval env
  (.andN args) = .bool false`. The corollary specialized to the case
  where a literal `.bool false` is syntactically present in the
  operand list. Useful for simple constant folding.

Each proof reduces `eval env (.andN args)` to
`evalAndN (args.map (eval env))` via `eval_andN`, then witnesses
membership of `.bool false` in the mapped list through
`List.mem_map`, then invokes `evalAndN_false_absorbs` from
`Mz/Variadic.lean`. The `Or`/`True` direction is symmetric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model relations as `List Row` and define the two basic relational
operators on top of the per-row evaluator.

* `filterRel pred rel` keeps rows whose predicate evaluates to
  `.bool true`. Rows evaluating to `.bool false`, `.null`, or
  `.err` are dropped. The skeleton silently drops `err` rows; a
  real implementation would route them to a separate error
  collection. Noted in the docstring as a known modelling gap.
* `project es rel` evaluates each scalar in `es` against every row
  and produces a relation whose row width is `es.length`.

Laws:

* `filterRel_idem`: applying the same predicate twice is the same
  as applying it once. Proof: `List.filter_filter` + `Bool.and_self`.
* `filterRel_comm`: filter commutes with filter. Proof:
  `List.filter_filter` twice + `Bool.and_comm`.
* `project_length`: projection preserves cardinality. Direct
  consequence of `List.length_map`.
* `project_nil`: the empty projection collapses every row to `[]`.

All proofs are one-liners over Lean core's list lemmas; the laws
themselves are the first relational-level rewrites an optimizer
cites. Predicate pushdown across projection, joins, and aggregates
remain follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model the dataflow's data and error collections as a pair carried
together through operators, mirroring the structure of the real
Materialize dataflow where erroring rows are routed to a separate
collection rather than silently dropped.

* `BagStream` is a structure with `data : Relation` and
  `errors : List EvalError`.
* `BagStream.ofRelation` injects a plain relation with no
  accumulated errors.
* `BagStream.filter pred s` evaluates `pred` on every row of
  `s.data`. Survivors (`.bool true`) stay in the data field;
  erroring rows contribute their payloads to the error field via
  `errorRows`; everything else is dropped.

Two helper lemmas carry the structural fact "predicate evaluated to
`.bool true` on every survivor of `filterRel`":

* `rows_in_filterRel_eval_to_true` unfolds the survival condition.
* `errorRows_eq_nil_of_all_true` says a relation where every row
  evaluates to `.bool true` contributes no errors.

`errorRows_filterRel` combines them and is the key fact behind
`BagStream.filter_idem` — the second pass of an idempotent filter
sees only survivors of the first, so it contributes no new errors.
The overall stream is therefore unchanged on the second filter, at
both the data and the error level.

`BagStream.project` and stream-level filter commutativity follow in
later iterations; commutativity at the error level requires
multiset equality on `List EvalError` since list order differs
across permutations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the classical relational rewrite:

  filterRel p (project es rel) = project es (filterRel (p.subst es) rel)

The rewrite lets an optimizer move a `WHERE` clause through a
`SELECT` clause by substituting the projection's source scalars into
the predicate's column references.

* `Expr.subst es e` replaces each `Expr.col i` in `e` with the i-th
  scalar of `es`, with out-of-bounds references defaulting to
  `.lit .null` so the result still evaluates to `.null`. Defined
  mutually with `Expr.substArgs` for the nested-list constructors
  (`andN`, `orN`, `coalesce`).
* `Expr.substArgs_eq_map` bridges the recursive helper to the more
  ergonomic `args.map (·.subst es)` form for use in proofs.
* `Env.get_map_eval` is the column-lookup compatibility lemma:
  reading column `i` from the projected row equals evaluating the
  i-th projection scalar against the original row.
* `eval_subst` is the headline correctness theorem: substituting and
  then evaluating against the original row equals evaluating the
  unsubstituted expression against the projected row. Proof is
  structural pattern recursion mirroring `Expr.subst`.
* `filterRel_pushdown_project` packages `eval_subst` plus
  `List.filter_map` + `List.filter_congr` into the relational form
  an optimizer cites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model the diff-field encoding of collection-scoped (global) errors
proposed in the design doc. The standard differential dataflow diff
type (typically `ℤ` for multiset counts) is extended with an
absorbing `error` element; any sum or product involving `error`
yields `error`, encoding "this collection is invalid at time `t`".

* `DiffWithError α` is a two-constructor inductive: `val (x : α)` for
  ordinary diffs, `error` for the absorbing marker.
* `Add`, `Mul`, `Zero`, `One` instances lift the underlying
  operations and respect the absorbing behavior.
* Absorption laws (`error_add_left`, `error_add_right`,
  `error_mul_left`, `error_mul_right`) are the defining property.
* Commutativity, associativity, and left-distributivity are proved
  parameterically over the underlying `α`: each lemma takes the
  corresponding law on `α` as a hypothesis and discharges the
  `DiffWithError` version by case analysis.

The lemmas are not packaged as `Semiring`/`CommRing` typeclass
instances because the skeleton avoids depending on Mathlib for
build-time reasons. Adding the typeclass wiring is straightforward
once Mathlib is on the dependency list.

Tying `DiffWithError` to an actual `(Row, Time, Diff)` triple stream
and proving the operator-level propagation theorems is the next
step; the present file lays the algebraic groundwork those theorems
rely on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model the spec's preferred encoding where data rows and errors flow
through one collection rather than the `(data, errors)` pair carried
by `BagStream`. The split form in `Mz/ErrStream.lean` mirrors the
current Materialize runtime; the unified form mirrors what the
diff-semiring extension naturally produces (any record with an
`error` diff is a row-scoped error, and the same encoding extends
to collection-scoped errors).

* `UnifiedRow` is a sum type — `row (r : Row)` or `err (e :
  EvalError)`. The bare `err` variant preserves the current
  property that errors carry no row context.
* `UnifiedStream := List UnifiedRow`.
* `UnifiedStream.ofBag` packs a `BagStream` (data rows first,
  errors second). `UnifiedStream.split` is its inverse, splitting
  the carrier back via `filterMap pickRow` / `filterMap pickErr`.
* `UnifiedStream.filter` runs the predicate per record, routing
  erroring rows to `.err` records in place. Existing `err` records
  pass through unchanged — the carrier handles error propagation
  without per-operator boilerplate.

Round-trip theorem `UnifiedStream.split_ofBag : split (ofBag s) =
s` proved at both the data and error field level, using four
private filterMap-over-tagged-list helpers and structural
induction. The cross-direction `filter ∘ ofBag ≈ ofBag ∘ filter` is
left for future work — equivalence is only up to multiset equality
on errors because `ofBag` fixes a concat order between data and
errors, while filters that interleave the two would produce a
different list order.

`@[ext]` added to `BagStream` so the round-trip proof can use
`apply BagStream.ext`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`List.filterMap` was passed to four `simp` invocations in the
filterMap-of-mapped-list helpers but never actually used in the
rewriting — the cons recurrence and the `pickRow` / `pickErr` arms
combined with the IH are enough. Drop the unused arg; the
linter-clean.
Model the spec rule that SQL aggregates such as `SUM`, `MIN`, `MAX`,
and `AVG` propagate `err` like strict scalar functions. NULLs are
skipped (matching PostgreSQL's "ignore NULLs" reading); errors are
propagated, with the first one in scan order winning.

* `aggCountNonNull`: `COUNT(expr)` — counts rows whose value is
  neither `NULL` nor `err`. Matches PostgreSQL.
* `aggStrict f`: variadic strict reduction parametric over the
  combiner `f`. Empty list (or list of only `NULL`s) returns `NULL`;
  any `err` returns the first `err`; non-`NULL`/`err` values are
  combined via `f`.

Two propagation theorems:

* `aggStrict_err`: any `err` input forces an `err` output. Proof by
  structural recursion on the list; the `.bool` case branches on
  `aggStrict f rest` and shows the `err` arm of the inner match is
  the one that fires.
* `aggStrict_no_err`: dual statement under the additional
  hypothesis that the combiner preserves "no-err". Captures the
  precondition an aggregate operator can use to guarantee its
  output is not an error when its column is error-free.

`try_*` aggregates (the non-strict variants that swallow `err` into
`NULL`) are future work; they satisfy a coalesce-style law rather
than strict propagation and warrant their own file once the spec
side is clearer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model the proposed `try_sum`, `try_min`, etc. that swallow `err`
into `NULL` instead of propagating. Defined as a post-pass coalesce
on `aggStrict`:

  aggTry f xs = match aggStrict f xs with
                | err _ => null
                | r     => r

Equivalent reading via the existing `evalCoalesce`:

  aggTry f xs = evalCoalesce [aggStrict f xs, .null]

The defining property `aggTry_no_err` says the result is never an
`err`. The companion `aggTry_eq_aggStrict_of_no_err` says the
non-strict variant agrees with the strict one whenever the strict
one would not have erred — so an optimizer that has already proved
the inputs error-free can swap `aggTry` for `aggStrict` (and vice
versa). Both lemmas reduce to case analysis on `aggStrict f xs`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model the diff-only slice of differential dataflow's `compact` /
consolidation operation: summing a list of `DiffWithError α` values
and showing that an `error` diff absorbs the consolidated sum.

* `DiffWithError.sumAll` sums a list of diffs starting from `0`,
  right-associative. The result lives in `DiffWithError α` for any
  base `α` with `Zero` and `Add` instances.
* `sumAll_eq_error_of_mem`: the headline absorption theorem. If
  `error` appears anywhere in the list, the consolidated sum is
  `error`. Proof walks the list and uses the absorption laws from
  `Mz/DiffSemiring.lean` at the matching cons.
* `sumAll_val_of_all_val`: companion no-error-preservation theorem.
  An all-`val` list sums to `val` of some `α`.

The full `compact` operator also buckets records by `(row, time)`;
that bucketing is orthogonal to the absorption argument and would
require `DecidableEq Row` plus a time model, both follow-ups. The
per-bucket inner sum modeled here is what an operator-level proof
would invoke once those pieces are in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `TimedRecord` and `TimedStream` to model differential
dataflow's record format with errors. `Time := Nat`; the diff is
`DiffWithError α` parametric in the base diff type.

Operations:

* `TimedStream.consolidateAll` sums every diff in the stream,
  ignoring row and time. The collection-wide diff.
* `TimedStream.consolidateAt t` sums every diff at time `t`,
  ignoring row. The per-time collection diff.

Both reduce to `DiffWithError.sumAll`, so the absorption laws from
`Mz/Consolidate.lean` transport:

* `consolidateAll_eq_error_of_mem`: any `error`-carrying record
  forces the all-stream consolidation to `error`.
* `consolidateAt_eq_error_of_mem`: any `error`-carrying record at
  time `t` forces the per-time consolidation at `t` to `error`.

Per-`(row, time)` bucketing — the form `compact` actually uses in
the runtime — is the next refinement and requires `DecidableEq` on
`Row`. The current sums are the collection-global diffs at each
time slice, which is what operator-level proofs invoke once the
bucketing is layered on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Stream

Two-input relational join modeled on the unified single-collection
stream. Cross product is the building block; predicate join is
cross followed by `UnifiedStream.filter`.

* `UnifiedStream.cross l r` produces one output record per
  `(lu, ru)` pair: both rows ⇒ concatenated row; either side `err`
  ⇒ that side's `err` (left wins on conflict, matching the binary
  AND's first-error rule).
* `UnifiedStream.join pred l r := (cross l r).filter pred`.

Error propagation falls out of the carrier: an `err` record on
either side contributes one `err` to the output per record on the
other side, matching the diff-semiring's `error * diff = error`
intuition.

Theorems: `cross_nil_left` and `cross_nil_right` cover the empty
cases (joining anything with the empty stream is empty). The
cardinality theorem `(cross l r).length = l.length * r.length` is
deferred — needs `List.length_flatMap` + Nat arithmetic that the
current skeleton's lemma toolkit handles awkwardly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Partition a relation by an evaluated key expression and aggregate
per group. Closes the SQL `SELECT k, AGG(v) FROM r GROUP BY k` shape
the rest of the algebra implies.

* `Datum` derives `DecidableEq` (via the already-decidable
  `EvalError` payload) so keys can be compared at runtime.
* `groupBy keyExpr rel` walks rows, evaluates the key on each, and
  inserts into the existing group with that key or starts a new
  one. Output is `List (Datum × Relation)`.
* `aggregateBy keyExpr valExpr f rel` applies `aggStrict` to each
  group's evaluated value column, producing `List (Datum × Datum)`
  — one aggregated value per group.

Spec divergence on `err` keys documented inline: the skeleton uses
standard `DecidableEq Datum`, so two `Datum.err e` values with the
same payload collapse into one group. The spec requires every err
key to be its own group. The natural refinement is a custom
`Datum.groupKeyEq` that returns `false` whenever either side is an
err; left for future work.

Theorems for now cover the trivial cases (empty, singleton).
Cardinality (`sum of group sizes = rel.length`) is the next
concrete law and is deferred until the lemma toolkit picks up the
needed Nat / List arithmetic helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `Datum.groupKeyEq` — group-key equivalence that returns `false`
whenever either side is `.err`, so two error keys never coalesce
even if their payloads happen to match. Build `groupByErrDistinct`
and `aggregateByErrDistinct` on top, leaving the existing
`groupBy` / `aggregateBy` (which use derived `DecidableEq Datum`)
in place as the more permissive variant.

Theorems:
* `Datum.groupKeyEq_err_left`: simp lemma reducing the err-left
  case to `false`.
* `insertIntoDistinct_err`: inserting an err-keyed row into any
  group list appends a fresh singleton group at the end.
* `groupByErrDistinct_length_of_all_err`: when every row's key
  evaluates to err, the output has one group per row — closes the
  spec-divergence note from the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`BagStream.project es s` projects each row in `s.data` through the
list of scalar expressions `es`. A row stays in the data collection
only when every scalar succeeds on it; otherwise the row's err
payloads (one per erroring scalar) are appended to the error
collection.

Helpers:
* `rowAllSafe`: every projected scalar succeeds on a row.
* `rowErrs`: collect every err payload from one row's projections.
* `projectErrs`: aggregate `rowErrs` across the relation.

Laws:
* `BagStream.project_data` / `_errors` — per-field reductions.
* `BagStream.project_nil_es` — empty projection list keeps every
  row and produces width-zero output rows.
* `BagStream.project_empty_data` — empty data collection projects
  to empty data, preserving input errors.
* `rowErrs_nil_of_all_safe` and `projectErrs_eq_nil_of_all_safe` —
  when every scalar succeeds on every row, the error collection is
  unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.cross_length` proves the headline relational identity
`(cross l r).length = l.length * r.length`. Cross product on the
unified carrier produces exactly one output record per `(l, r)`
pair regardless of which side carries an error — every err
contributes one err record per element of the other side, matching
the diff-semiring's `error * d = error`.

Companion `UnifiedStream.filter_length_le` proves the predicate
filter on the unified stream is non-expanding (each input produces
zero or one output). Composing the two gives the corollary
`UnifiedStream.join_length_le`: a join's output length is bounded
above by `l.length * r.length`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`totalRows g = sum of g.rows.length` is the metric. Theorems:

* `totalRows_insertInto`: a single insert adds exactly one row,
  whether the key matches an existing group or creates a new one.
* `totalRows_insertIntoDistinct`: same bookkeeping for the
  err-distinct variant.
* `totalRows_groupBy`: sum of group sizes equals the input
  relation's length — partitioning loses and duplicates nothing.
* `totalRows_groupByErrDistinct`: the err-distinct variant
  preserves the same invariant.

Use an explicit recursive `totalRows` definition instead of
`((map ·.2.length).sum`. The latter trips `omega` because the
elaborator and the simp set produce two syntactically distinct
forms (`(·.2.length)` vs `List.length ·.snd`) that omega treats
as independent atoms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote `UnifiedStream` from `List UnifiedRow` to
`List (UnifiedRow × DiffWithError Int)`. Every record now carries
a differential-dataflow diff augmented with the absorbing `.error`
marker. Row-scoped errors still propagate through the `UnifiedRow`
carrier; collection-scoped errors propagate through diff
multiplication / addition.

Changes:

* `UnifiedStream.ofBag` tags every bag record with diff `.val 1`.
* `UnifiedStream.split` discards the diff component (round trip
  still goes through because `ofBag` only emits `.val 1` diffs;
  the cross-direction loses information for diffs ≠ `.val 1`).
* `UnifiedStream.filter` preserves the diff of survivors; rerouted
  rows (predicate errs) keep their multiplicity.
* `UnifiedStream.cross` combines carriers via the new
  `combineCarrier` helper and multiplies diffs through
  `DiffWithError`'s `Mul` instance. A `.error` diff on either
  side absorbs the product diff via
  `DiffWithError.error_mul_{left,right}`.

New theorem `cross_diff_error_left` witnesses the absorption:
crossing a left-side `.error` diff with any record on the right
produces a `.error` diff in the output. Existing theorems
(`cross_length`, `filter_length_le`, `join_length_le`,
`split_ofBag`) carry through to the new representation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten diff propagation in `UnifiedStream.filter` and add the
symmetric `cross` absorption theorem.

`filter` now special-cases records carrying a `.error` diff,
passing them through unconditionally regardless of predicate
outcome. The previous version dropped such records when the
predicate evaluated to `.bool false` or `.null`, which violates
the semiring law that `.error` must absorb every downstream
operator. `filter_length_le` still holds — each input still
produces at most one output.

Three new theorems witness the absorption:

* `cross_diff_error_right`: symmetric counterpart to the existing
  `cross_diff_error_left` — a `.error` diff on the right side
  forces the output diff to `.error` for every record on the left.
* `filter_preserves_error_diff`: a record `(uc, .error)` in the
  input survives the filter as `(uc, .error)` in the output, no
  matter what the predicate is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.consolidate` buckets unified-stream records by
carrier (data row or row-scoped err) and sums per-bucket diffs.
Headline theorem `consolidate_preserves_error` lifts the
diff-semiring absorption rule from `Mz/Consolidate.lean` to the
unified stream: an `.error` diff anywhere in the input forces a
`.error` diff in the consolidated output for that carrier.

Required:

* `deriving DecidableEq` on `UnifiedRow` (Row already has it via
  `Datum`, EvalError too).
* Two helper lemmas — `consolidateInto_error_diff` (inserting an
  `.error` diff yields `.error` for the bucket) and
  `consolidateInto_preserves_error_mem` (a pre-existing
  `.error` record survives every subsequent insert).

Adding times reduces to running `consolidate` inside each time
slice; that lifting is mechanical and left for the per-`(row, time)`
follow-up alongside `Mz/Triple.lean`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two companion theorems for `UnifiedStream.consolidate`.

`consolidate_length_le`: bucketing only merges, never expands.
The output has at most as many records as the input. Helper
`consolidateInto_length_le_succ` bounds a single insert at
`us.length + 1`.

`consolidate_no_error`: if every input diff is `.val n`, every
output diff is `.val m`. The diff-semiring's `.val + .val = .val
(· + ·)` keeps the consolidated buckets in the ordinary `Int`
slice, so `.error` is the only source of absorption.

Together with the existing `consolidate_preserves_error`, the
three theorems pin down the consolidation operator: absorption
on errors, non-expansion on cardinality, and stability on
error-free input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
antiguru and others added 29 commits May 18, 2026 23:17
Add per-file documentation for the new error-scope work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Mz/JoinPushdown.lean with the optimizer's canonical
join-pushdown rewrite:

  filter pred (cross l r) = cross (filter pred l) r

when pred references only left-input columns. Earlier abandoned
attempt at this proof failed on the show-tactic pattern matching;
this retry succeeds via:

* IsPureData predicate on r (no .err carriers, no .error diffs)
  — required because combineCarrier's left-wins rule and the
  absorbing .error * d = .error rule disagree across the two
  sides without it.
* cross_singleton reduction lemma with carrier/diff split out
  (avoids .fst/.snd projections that block later rfls under
  induction).
* Per-record helpers for each left-record shape:
  - filter_map_error_diff: .error diff passes through.
  - filter_map_err_carrier: .err e left, .val diff.
  - filter_map_row_val_keep: .row la, eval = .bool true.
  - filter_map_row_val_err: .row la, eval = .err — cell-to-row
    promotion site.
  - filter_map_row_val_drop: .row la, eval rejects.

Main theorem filter_cross_pushdown_left assembles these via
structural induction on l plus eval_append_left_of_bounded.

Caveats encoded in the hypotheses:

* hLWidth (∀ row la in l, la.length ≥ N) — needed because
  eval_append_left_of_bounded requires bounded by la.length.
* hRPure (IsPureData r) — without it, attribution conflicts
  arise (left .row pred-errs vs right .err records;
  .error diff in r interacts with filter's first arm).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add UnifiedStream.negate_filter:

  negate (filter pred us) = filter pred (negate us)

Earlier abandoned attempt failed on pattern-match shows; this
retry succeeds via per-record case analysis with computed reduction
lemmas for each filter branch.

Holds unconditionally — no hypothesis on pred or stream content.
Filter's row arm depends on eval r pred which is independent of
diff sign, so the negate slides through.

Cases handled:
* (uc, .error): filter passes through, .error negates to .error.
* (.err e, .val n): filter passes through, both orderings give
  (.err e, .val -n).
* (.row r, .val n) with eval = .bool true: keep, diff flips.
* (.row r, .val n) with eval = .err e_pred: cell-to-row promotion
  preserved by negate.
* (.row r, .val n) with eval drops: both orderings give [].

Enables optimizer rewrites that commute negation with filter
(EXCEPT ALL paths, retraction reordering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ptAll corollaries

Partial error-scope characterization for the clamp operators:

* clampPositive_errorDiffCarriers_iff: equality. .error records
  always pass through (isPositiveDiff .error = true), so the
  collection-err set is preserved exactly.
* clampPositive_errCarriers_of_mem: reverse direction only.
  clampPositive may drop (.err e, .val 0) records, so the row-err
  set may shrink. Forward direction fails.

Same shape for clampToOne, with the case analysis tracking
clampToOne's three branches (.error keep, .val n>0 collapse,
.val n≤0 drop).

clampToOne_error_inv: a (uc, .error) in clampToOne output came
from a (uc, .error) input. The .val arms never emit .error.

Composed corollaries:

* distinct_errorDiffCarriers_iff /
  distinct_errCarriers_of_mem: distinct = clampToOne ∘ consolidate.
* bagExceptAll_errorDiffCarriers_iff /
  bagExceptAll_errCarriers_of_mem: bagExceptAll = clampPositive ∘
  exceptAll.

bagIntersectAll deferred — intersectAll's lookup-based definition
needs separate analysis before composition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three reverse-direction theorems for intersectAll-family:

* intersectAll_errCarriers_of_mem: an err in the intersectAll
  output appears in BOTH inputs (consolidate carrier preservation
  for the left side; lookup-witness for the right).
* intersectAll_errorDiffCarriers_of_mem: a .error-diff carrier
  in the output exists (with some diff) in BOTH inputs. The min
  combinator only yields .error when at least one side has
  .error, but the carrier must be present on both for
  intersectAll to emit anything.
* bagIntersectAll_errCarriers_of_mem: composition with
  clampPositive.

Forward direction (errors in both inputs propagate to output)
holds for collection-err on left via existing
intersectAll_preserves_error_diff_left, with side conditions.
Full iff theorems for intersectAll are out of scope — would need
to characterize min combinator results per-bucket.

Proofs use a hF restate via defeq (.fst/.snd projection
reduction) before cases on lookup; otherwise rw [hLookup] fails
to find the lookup expression syntactically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lift error-scope extractors to TimedUnifiedStream:

* TimedUnifiedStream.errCarriers / errorDiffCarriers
* _nil / _append reductions matching UnifiedStream's

Per-operator theorems:

* advanceFrontier_errCarriers / advanceFrontier_errorDiffCarriers:
  exact equality. advanceFrontier only changes times; carriers
  and diffs untouched.
* atTime_errCarriers_subset / atTime_errorDiffCarriers_subset:
  errs in atTime t slice came from input s (subset direction).
  Time-slice projection loses records at other times, so this
  cannot be an iff.
* consolidateAtTime_errCarriers_subset /
  consolidateAtTime_errorDiffCarriers_subset: composition of
  atTime subset with consolidate iff.

Restructured: moved consolidate_errCarriers_iff /
consolidate_errorDiffCarriers_iff from Mz/SetOps.lean to
Mz/UnifiedConsolidate.lean. Mz/TimedConsolidate.lean cites them
but the import chain has SetOps downstream of TimedConsolidate.
SetOps continues to use them via its UnifiedConsolidate import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document new error-scope coverage:

* Clamp/clamp-distinct error-scope (clampPositive, clampToOne,
  distinct, bagExceptAll, intersectAll, bagIntersectAll): full
  iff for collection-err, reverse-only for row-err (clamp drops).
* negate_filter commutativity.
* Timed error-scope lift (errCarriers / errorDiffCarriers on
  TimedUnifiedStream; advanceFrontier exact; atTime /
  consolidateAtTime subset).
* JoinPushdown.lean entry: filter_cross_pushdown_left, IsPureData
  hypothesis, cross_singleton reduction.
* Note relocation of consolidate iff theorems to
  UnifiedConsolidate.lean (TimedConsolidate citation chain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DiffWithError.sumAll_error_inv: converse of absorption. A sum
equal to .error witnesses at least one .error summand. Proof
peels via add_eq_error_left_or_right (DiffSemiring inversion law)
on each cons step; nil case rules out via .val 0 ≠ .error.

Triple.lean reverse-direction theorems:

* consolidateAll_error_inv: .error total → ∃ record in stream
  with .error diff.
* consolidateAtTimeFlat_error_inv: .error total at time t →
  ∃ record at time t with .error diff.

Both are direct corollaries — extract the map preimage and
filter conditions from sumAll_error_inv's witness. The
consolidateAtTimeFlat version decodes the decide-wrapped time
predicate via of_decide_eq_true.

Closes the round-trip on flat consolidations: forward absorption
+ reverse inversion makes consolidateAll = .error iff some
record has .error diff (and similarly per-time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new theorems in Mz/Triple.lean:

* consolidateAll_eq_error_iff: forward + reverse combined.
  consolidateAll s = .error ↔ ∃ r ∈ s, r.2.2 = .error.
* consolidateAtTimeFlat_eq_error_iff: same shape, restricted to
  time-t slice.
* consolidateAll_eq_error_iff_errorDiffCarriers: bridges the flat
  consolidation to the timed error-scope extractor.
  consolidateAll s = .error ↔ errorDiffCarriers s ≠ [].

The bridge connects two views of the same observation: the
diff-semiring total (single .error or .val value) and the
extractor list (carrier-level collection-err set). They agree
exactly on whether the stream has any .error diff anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
negate commutes with project unconditionally:

  negate (project es us) = project es (negate us)

Mirror of negate_filter. project's row arm produces
rowProjectRecords es d r which preserves the input diff d on
every emitted record; negation flips d uniformly, sliding through.

Helper negate_rowProjectRecords: negation slides through the
row-project helper. Uses List.map_map for the err-branch case.

rowProjectRecords promoted from private to public so the
cross-file commute theorem can cite it directly.

Enables optimizer rewrites that commute negation with projection
(EXCEPT ALL through projection, retraction propagation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetric counterpart to cross_negate_left:

  cross l (negate r) = negate (cross l r)

Uses DiffWithError.mul_neg_int (a * -b = -(a * b)) on the diff
arithmetic; combineCarrier carrier is unchanged by negation.

Proof structure mirrors cross_negate_left: induction on l with
hL/hR reduction lemmas to expose flatMap output, then map_congr
on the per-record body.

Together with cross_negate_left, gives the bilinear law for
cross: negate slides through from either side. Useful for
optimizer rewrites where retractions on either input propagate
through cross.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single index of every equational / inclusion law on UnifiedStream
and TimedUnifiedStream. Grouped by algebraic shape:

* Append / unionAll distribution
* Commutativity (negate slides through)
* Pushdown (filter_cross_pushdown_left)
* Bilinearity (cross-negate)
* Associativity (cross, unionAll, combineCarrier)
* Involution / idempotence (negate_negate, clamp/escalate/advance idem)
* Length / cardinality bounds
* Trivial cases (nil / singleton reductions)
* Cons / step reductions (cross_cons, consolidateInto match/skip)
* Error-scope: row-err (errCarriers)
* Error-scope: collection-err (errorDiffCarriers)
* Error-diff record absorption (forward) and inversion (reverse)
* No-error preservation
* Multiplicity / shape constraints
* NoDup carrier-uniqueness
* Membership bridges
* Round-trip / iff
* DiffWithError underlying laws
* Column-reference analyzers

Each row links theorem name to source file. Optimizer rewrites
should index here first; ~190 theorems total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add 'Materialize optimizer passes → Lean coverage' section in
transforms.md. Inventories all 66 passes in src/transform/ and
classifies each as:

* Modeled — equivalent theorem shipped (6 passes).
* Modelable — current UnifiedStream / DiffWithError infra is
  sufficient; just write the theorem (11 passes incl.
  filter/map fusion, right-side pushdown, threshold elision,
  redundant_join, semijoin_idempotence, demand, canonicalize_mfp,
  equivalence_propagation use sites).
* Infra gap — needs new operator or analysis (10 passes:
  Reduce family, TopK, FlatMap, IndexedFilter, monotonicity,
  column lattice).
* Out of scope — physical planning, syntactic canonicalization,
  user-facing notices, type checking (~14 passes).

Closes with priority recommendations: filter_cross_pushdown_right,
filter/map fusion, threshold_elision as easy next ships. Reduce
operator is the largest single infra gap (unlocks 4+ passes).

Catalog derived via Explore-agent enumeration of src/transform/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of filter_cross_pushdown_left. Predicate references only
right-input columns (pred = pred'.colShift M, every left row of
width M), left input is pure data. Bridges via
eval_append_right_shift; symmetric to the left version's
colReferencesBoundedBy + width-monotone setup.

Single per-record helper filter_map_pure_data_right covers the
.row+.val case; .error diff / .err carrier rows on the left are
ruled out by IsPureData l.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move predicate_pushdown.rs entry from "left-only pending" to
bilateral. Drop redundant Modelable row. Rotate the priority
list — top-of-list slot now goes to filter ∘ filter fusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`filter p (filter q us) = filter (.and q p) us` under per-row
err-freedom: neither predicate may evaluate to .err on any data
row of the input. Side condition is sharp — without it, the
ordering between filter's err-promotion and evalAnd's
`.bool false` absorption clause disagrees, e.g. on
`(eval r q, eval r p) = (.err e, .bool false)`.

Side condition lives in new `UnifiedStream.predNoRowErr`. Carrier-
err records and `.error`-diff records pass through both pipelines
unchanged; the hypothesis is only needed at `.row` records.

Mirrors fusion/filter.rs in the Rust optimizer. New module
Mz/FilterFusion.lean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move fusion/filter.rs from Modelable to Modeled, cite
filter_filter_fuse in Mz/FilterFusion.lean. Rotate priority list:
project fusion now top of queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clampPositive_id_of_positive: clampPositive is identity on streams
where every record's diff is .error or .val n with n > 0. Models
threshold_elision.rs — drop the post-pass when input is already
sign-normalized.

transforms.md: move to Modeled, rotate priority list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`project es' ∘ project es = project (es'.map (·.subst es))` when
`es` is safe on every data row of the input. Side condition is
sharp: if `es` errors on a row, step 1 emits one err record per
erroring scalar of `es`; the fused form instead emits one err
record per `e' ∈ es'` whose substituted form errors. Those err
sets only agree in the safe-input case.

Row-level fusion lemma `rowProjectRecords_substList` is
unconditional; the stream-level theorem
`project_project_fuse` lifts it under `projsAllSafe`. Bridges via
`eval_subst` from `Mz/Pushdown.lean`.

Mirrors fusion/map.rs in the Rust optimizer. New module
Mz/ProjectFusion.lean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move fusion/map.rs from Modelable to Modeled. Rotate priority
list: semijoin_idempotence now top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.filter_replaceAtRow_of_unused`: filter and
`replaceAtRow n v` commute when the predicate doesn't reference
column `n`. Lifts `eval_replaceAt_of_unused` from `Mz/ColRefs.lean`
to the stream level.

Models the row-side of demand.rs: a column that nothing downstream
reads can be overwritten arbitrarily without changing the
operator's output. New module Mz/Demand.lean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`project_replaceAtRow_eq_of_unused`: when every expression in
`es` has column `n` unused and the input is pure data, replacing
column `n` of every input row leaves the project output equal.

Asymmetry vs filter: filter preserves the row content of surviving
records, so demand admits replacement on both input and output
sides; project rewrites row content via `es.map (eval r)`, so
column `n` of the output is unrelated to column `n` of the input,
and the simpler invariance form requires ruling out `.err`/
`.error` passthrough — hence the `IsPureData` hypothesis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move demand.rs from Modelable to Modeled. Cite both
filter_replaceAtRow_of_unused (full input) and
project_replaceAtRow_eq_of_unused (under IsPureData). Rotate
priority list; canonicalize_mfp now reachable on top of three
fusion theorems.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.filter_idem`: filter pred (filter pred us) =
filter pred us. Holds without err-freedom — when both filters
share a predicate, the err-promotion / evalAnd ordering mismatch
that forces predNoRowErr in filter_filter_fuse cannot arise.

Special case of filter_filter_fuse + evalAnd_idem, but proved
directly to avoid the unnecessary hypothesis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.filter_comm`: filter p (filter q us) =
filter q (filter p us) when neither predicate errors on the
input's data rows. Reduces to `filter_filter_fuse` applied both
ways, then equates `.and q p` and `.and p q` via
`evalAnd_comm_of_no_err`.

New helper `filter_eval_eq`: two filters with eval-equivalent
predicates on every data row produce equal outputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cite filter_idem (unconditional) and filter_comm (under
err-freedom) alongside filter_filter_fuse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UnifiedStream.filter_project_pushdown`: filter p ∘ project es =
project es ∘ filter (p.subst es), under projsAllSafe es us.
Lifts BagStream.project_filter_pushdown_data (data-side only)
to the unified stream — errs flow through both pipelines
symmetrically under the safety hypothesis, so no data/error
asymmetry remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Note filter_project_pushdown in the Modelable row for
projection-lifting passes. Still missing: project_cross_pushdown
(harder — depends on column-split semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`clampToOne_id_of_one`: clampToOne is identity when every
record's diff is already .val 1 or .error. Companion to
clampPositive_id_of_positive — same threshold-elision pattern,
but for the set-semantics post-pass instead of the bag-semantics
one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@antiguru
Copy link
Copy Markdown
Member Author

Foundational gaps

Ranked by what's currently blocking further coverage.

Tier 1: blocks most remaining modelable passes

  • Stream equivalence relation. UnifiedStream is List, so equality is list-equality. Many natural laws only hold up to multiset / bag equivalence:

    • unionAll_comm — fails as list eq (concat order).
    • consolidate_idemconsolidate reverses order on noDup inputs (see SetOps.lean algorithm), so consolidate ∘ consolidate ≠ consolidate.
    • cross_comm — Cartesian product is symmetric only as multiset.
    • unionAll_distrib_cross_right — interleaves vs groups.

    Fix: define UnifiedStream.Equiv ≈ List.Perm (or stronger: zero-diff and same-carrier-sum quotient), or switch to Multiset (UnifiedRow × DiffWithError Int).

  • Reduce operator (group-by aggregate). Mz/Aggregate.lean exists on List Datum only. No UnifiedStream lift. Single largest infra gap — blocks fusion/reduce, reduce_elision, reduce_reduction, reduction_pushdown. Needs group-key expressions, aggregate functions (sum / count / min / max / ...), and per-group error handling.

  • Map separate from project. UnifiedStream.project currently replaces the row with es.map (eval r). Rust's Map appends columns. Without separate Map, MapFilterProject and canonicalize_mfp can't be modeled cleanly.

Tier 2: specific pass clusters

  • Schema / column types. Rows are List Datum with no type info. Blocks:

    • non_null_requirements — needs per-column nullability lattice.
    • column_knowledge — needs per-column {literal, nullable, type} lattice.
    • Width-correct join statements (width is implicit and unchecked).
    • SQL type-rejection (currently evalAnd .int .int returns .int n for totality).
  • Other missing operators:

    • TopK (sort + limit, needs ordering on Row).
    • FlatMap (table-valued function).
    • Constant (literal stream — would unlock fold_constants).
    • Get / Let / LetRec (named bindings, fixpoint for recursive queries).
    • ArrangeBy (key-indexed view — physical but cited by passes).
  • Equivalence-class lattice for equivalence_propagation. Currently only use-sites modelable.

  • Monotonicity / NoRetraction predicate for monotonic.rs. Streams that never retract.

Tier 3: correctness reach

  • Bridge to actual Materialize code. No translation function `MirRelationExpr → UnifiedStream` composition, no soundness statement "the Rust optimizer's rewrites are valid under this semantics." All current theorems are about an idealized model.

  • Real time / frontier semantics. TimedUnifiedStream has Nat times. Missing:

    • Partial order on times.
    • Frontier (antichain) type.
    • since / upper bound machinery.
    • Capability / retraction-as-of-time laws.
  • Fixpoint semantics. No model of LetRec / iterate. WITH RECURSIVE is invisible to the spec.

  • Differential dataflow proper. cross is one-shot Cartesian product, not arrangement-based. No worker partitioning. Mostly physical, but the logical retraction semantics deserve modeling.

Recommendation order

Tier 1 stream-equivalence + Reduce + separate Map unlocks the most. Schema enables a different class of analyses cleanly. Bridge to MirRelationExpr is the biggest reach but the only way the spec becomes load-bearing for real review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant