diff --git a/Cargo.lock b/Cargo.lock index 0da43128..a0d82305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,6 +626,7 @@ dependencies = [ "egglog-ast", "egglog-bridge", "egglog-core-relations", + "egglog-numeric-id", "egglog-reports", ] diff --git a/egglog-experimental/dd/src/dd_native.rs b/egglog-experimental/dd/src/dd_native.rs index de44048e..c6222c8c 100644 --- a/egglog-experimental/dd/src/dd_native.rs +++ b/egglog-experimental/dd/src/dd_native.rs @@ -66,32 +66,40 @@ type DeltaMap = HashMap; /// One `step`'s captured output deltas, parallel to the fused join's rule list. type StepOutput = Vec; /// A per-rule output-capture buffer shared with the DD closure (fixed-width -/// [`Row`] rows). -type CaptureBuf = Rc>>; - -/// Fixed binding-row width (DD `Data` needs a `Sized + Ord + Hash` type; an -/// array gives us that). Set to 48 to cover the widest live-variable frontier -/// in the backend corpus: `luminal-llama`'s `@rebuild_rule34` uses 35 distinct -/// body vars in a wide congruence-closure rebuild. A larger live frontier is -/// reported as a row-width-cap wall. Raising `W` extends coverage at a cost of -/// `W * 4` bytes per relation or binding row. +/// [`RowN`] rows). +type CaptureBuf = Rc, isize)>>>; + +/// Maximum binding-row width the planner accepts. Set to 48 to cover the +/// widest live-variable frontier in the backend corpus: `luminal-llama`'s +/// `@rebuild_rule34` uses 35 distinct body vars in a wide congruence-closure +/// rebuild. A larger live frontier is reported as a row-width-cap wall. +/// +/// The dataflow itself does NOT run at this width: [`FusedDdJoin::build`] +/// selects the smallest width in [`WIDTH_LADDER`] that fits the ruleset's +/// plans, so arrangements store (and compare) only as many columns as the +/// widest rule in that ruleset actually needs. pub const W: usize = 48; +/// Row widths the fused dataflow is monomorphized over. Every ruleset runs at +/// the smallest ladder width `>=` its widest plan ([`JoinPlan::width`]). +pub const WIDTH_LADDER: [usize; 4] = [8, 16, 32, 48]; + /// A fixed-width relation or binding row flowing through the DD dataflow. Input /// rows store relation columns in the low slots. Intermediate binding columns /// are assigned by the current `ProjectionPlan` stage, and captured outputs /// repack surviving variables into the low slots in [`JoinPlan::var_order`]. /// -/// A NEWTYPE over `[u32; W]` (rather than the bare array) because timely's +/// A NEWTYPE over `[u32; WIDTH]` (rather than the bare array) because timely's /// `ExchangeData` bound required by DD joins is /// `Serialize + Deserialize`, and `serde` only derives those for arrays up to /// length 32. The hand-written serde impl (serialize as a fixed-length seq of -/// `W` `u32`s) lifts that cap so `W` can exceed 32 (the corpus needs 35). All -/// other derives (`Ord`/`Hash`/`Clone`/`Copy`) are auto for any array size. +/// `WIDTH` `u32`s) lifts that cap so `WIDTH` can exceed 32 (the corpus needs +/// 35). All other derives (`Ord`/`Hash`/`Clone`/`Copy`) are auto for any array +/// size. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct Row([u32; W]); +pub struct RowN([u32; WIDTH]); -impl std::ops::Index for Row { +impl std::ops::Index for RowN { type Output = u32; #[inline] fn index(&self, i: usize) -> &u32 { @@ -99,19 +107,19 @@ impl std::ops::Index for Row { } } -impl std::ops::IndexMut for Row { +impl std::ops::IndexMut for RowN { #[inline] fn index_mut(&mut self, i: usize) -> &mut u32 { &mut self.0[i] } } -impl serde::Serialize for Row { +impl serde::Serialize for RowN { fn serialize(&self, s: S) -> Result { use serde::ser::SerializeTuple; - // Fixed-length tuple of W u32s — bincode-friendly, no length prefix - // needed (the deserializer knows W). Sidesteps serde's 32-array cap. - let mut t = s.serialize_tuple(W)?; + // Fixed-length tuple of WIDTH u32s — bincode-friendly, no length prefix + // needed (the deserializer knows WIDTH). Sidesteps serde's 32-array cap. + let mut t = s.serialize_tuple(WIDTH)?; for v in &self.0 { t.serialize_element(v)?; } @@ -119,31 +127,34 @@ impl serde::Serialize for Row { } } -impl<'de> serde::Deserialize<'de> for Row { - fn deserialize>(d: D) -> Result { - struct RowVisitor; - impl<'de> serde::de::Visitor<'de> for RowVisitor { - type Value = Row; +impl<'de, const WIDTH: usize> serde::Deserialize<'de> for RowN { + fn deserialize>(d: D) -> Result, D::Error> { + struct RowVisitor; + impl<'de, const WIDTH: usize> serde::de::Visitor<'de> for RowVisitor { + type Value = RowN; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "a tuple of {W} u32s") + write!(f, "a tuple of {WIDTH} u32s") } - fn visit_seq>(self, mut seq: A) -> Result { - let mut a = [0u32; W]; + fn visit_seq>( + self, + mut seq: A, + ) -> Result, A::Error> { + let mut a = [0u32; WIDTH]; for (i, slot) in a.iter_mut().enumerate() { *slot = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?; } - Ok(Row(a)) + Ok(RowN(a)) } } - d.deserialize_tuple(W, RowVisitor) + d.deserialize_tuple(WIDTH, RowVisitor) } } -impl Default for Row { +impl Default for RowN { fn default() -> Self { - Row([0; W]) + RowN([0; WIDTH]) } } @@ -153,6 +164,10 @@ pub struct JoinPlan { atoms: Vec, /// Per-step column allocation for variables live at each join stage. projection: ProjectionPlan, + /// Minimum row width this plan needs: the widest atom arity or live + /// binding-column frontier. [`FusedDdJoin::build`] picks the smallest + /// [`WIDTH_LADDER`] entry covering every plan in the ruleset. + width: usize, } struct PlanAtom { @@ -212,6 +227,34 @@ pub fn plan_join(rule: &RuleSpec) -> Result { if atoms.is_empty() { return Err("no body table atoms (atom-less rule)".to_string()); } + // Diagnostic: dump the emitted (naive, body-order) join sequence, flagging + // any stage that joins with no shared variable (a cartesian product). + if std::env::var("EGGLOG_DD_DUMP_PLANS").is_ok() { + let mut bound: hashbrown::HashSet = hashbrown::HashSet::new(); + let mut desc: Vec = Vec::new(); + for (i, atom) in atoms.iter().enumerate() { + let vars = atom_vars(&atom.slots); + let shared = vars.iter().filter(|v| bound.contains(*v)).count(); + let tag = if i > 0 && shared == 0 { + " CARTESIAN" + } else { + "" + }; + desc.push(format!( + "{:?}/{}(arity {}, shared {}{tag})", + atom.read_key.func, + match atom.read_key.mode { + egglog_backend_trait::ReadMode::Live => "live", + egglog_backend_trait::ReadMode::Subsumed => "sub", + egglog_backend_trait::ReadMode::All => "all", + }, + atom.slots.len(), + shared, + )); + bound.extend(vars); + } + eprintln!("[dd-plan] rule {:?}: {}", rule.name, desc.join(" ⋈ ")); + } let projection = build_projection(&atoms, &rule.core.head.0, &rule.core.body.atoms) .ok_or_else(|| { format!( @@ -219,7 +262,27 @@ pub fn plan_join(rule: &RuleSpec) -> Result { body_vars.len() ) })?; - Ok(JoinPlan { atoms, projection }) + let width = plan_width(&atoms, &projection); + Ok(JoinPlan { + atoms, + projection, + width, + }) +} + +/// The narrowest row width that fits every atom's input columns and every +/// step's binding-column frontier (columns are allocated low-first, so the +/// frontier is `max column + 1`). +fn plan_width(atoms: &[PlanAtom], projection: &ProjectionPlan) -> usize { + let arity = atoms.iter().map(|a| a.slots.len()).max().unwrap_or(0); + let cols = projection + .step_col + .iter() + .flat_map(|layout| layout.values().map(|&c| c + 1)) + .chain(projection.head_cols.iter().map(|&c| c + 1)) + .max() + .unwrap_or(0); + arity.max(cols).max(1) } /// Per-step binding columns produced by linear-scan allocation over body-atom @@ -392,11 +455,97 @@ fn collect_head_vars( /// A fused, delta-fed body join for a WHOLE ruleset on a single shared timely /// `Worker`. Built once via [`FusedDdJoin::build`]; driven across epochs via /// [`FusedDdJoin::step`] with a SINGLE `worker.step_while` per call. -pub struct FusedDdJoin { +/// +/// The variants are the [`WIDTH_LADDER`] monomorphizations of the same +/// dataflow: `build` picks the smallest width that fits the ruleset's plans, so +/// arrangement keys/values only pay for the columns the widest rule needs. +pub enum FusedDdJoin { + W8(FusedDdJoinW<8>), + W16(FusedDdJoinW<16>), + W32(FusedDdJoinW<32>), + W48(FusedDdJoinW<48>), +} + +impl FusedDdJoin { + /// Build ONE worker + ONE dataflow for the whole ruleset at the smallest + /// ladder width covering every plan. `plans` pairs each rule's index with + /// its [`JoinPlan`], in the order they should fire. + pub fn build(plans: &[(usize, JoinPlan)]) -> Result { + let need = plans.iter().map(|(_, p)| p.width).max().unwrap_or(1); + match WIDTH_LADDER.iter().find(|&&w| w >= need) { + Some(8) => Ok(FusedDdJoin::W8(FusedDdJoinW::build(plans)?)), + Some(16) => Ok(FusedDdJoin::W16(FusedDdJoinW::build(plans)?)), + Some(32) => Ok(FusedDdJoin::W32(FusedDdJoinW::build(plans)?)), + Some(48) => Ok(FusedDdJoin::W48(FusedDdJoinW::build(plans)?)), + Some(w) => bail!("DD width ladder entry {w} has no monomorphization"), + None => bail!("plan width {need} exceeds the row-width cap {W}"), + } + } + + /// The rule indices this fused worker serves (build order). + pub fn rule_indices(&self) -> Vec { + self.dispatch( + |j| j.rule_indices(), + |j| j.rule_indices(), + |j| j.rule_indices(), + |j| j.rule_indices(), + ) + } + + /// Distinct body relation read views across the ruleset, in first-use order. + pub fn read_keys(&self) -> &[ReadKey] { + self.dispatch( + |j| j.read_keys(), + |j| j.read_keys(), + |j| j.read_keys(), + |j| j.read_keys(), + ) + } + + /// Captured variable order for the fused rule at build position `pos`. + pub fn rule_var_order(&self, pos: usize) -> &[u32] { + self.dispatch( + |j| j.rule_var_order(pos), + |j| j.rule_var_order(pos), + |j| j.rule_var_order(pos), + |j| j.rule_var_order(pos), + ) + } + + /// Feed one epoch of signed relation deltas, advance the timestamp, run the + /// worker to this epoch's fixpoint, and return per-rule binding deltas. See + /// [`FusedDdJoinW::step`]. + pub fn step(&mut self, deltas: &DeltaMap) -> Result { + match self { + FusedDdJoin::W8(j) => j.step(deltas), + FusedDdJoin::W16(j) => j.step(deltas), + FusedDdJoin::W32(j) => j.step(deltas), + FusedDdJoin::W48(j) => j.step(deltas), + } + } + + fn dispatch<'a, T>( + &'a self, + f8: impl FnOnce(&'a FusedDdJoinW<8>) -> T, + f16: impl FnOnce(&'a FusedDdJoinW<16>) -> T, + f32: impl FnOnce(&'a FusedDdJoinW<32>) -> T, + f48: impl FnOnce(&'a FusedDdJoinW<48>) -> T, + ) -> T { + match self { + FusedDdJoin::W8(j) => f8(j), + FusedDdJoin::W16(j) => f16(j), + FusedDdJoin::W32(j) => f32(j), + FusedDdJoin::W48(j) => f48(j), + } + } +} + +/// One [`WIDTH_LADDER`] monomorphization of the fused join dataflow. +pub struct FusedDdJoinW { worker: Worker, /// One shared input session per DISTINCT body relation read view across all /// rules. - inputs: HashMap>, + inputs: HashMap, isize>>, /// Single probe on all rule outputs (they share the dataflow scope, so one /// probe gates the whole epoch's fixpoint). probe: ProbeHandle, @@ -405,32 +554,35 @@ pub struct FusedDdJoin { reads: Vec, /// The fused rules in caller-supplied build order. The sorted rule-index list /// identifies the ruleset cache entry but does not reorder these outputs. - rules: Vec, + rules: Vec>, /// Current epoch (monotonic; advanced once per [`step`]). epoch: u32, } -/// One rule's lowering inside a [`FusedDdJoin`]: its rule index (for routing +/// One rule's lowering inside a [`FusedDdJoinW`]: its rule index (for routing /// bindings to its head), its per-epoch output capture buffer, and the variable /// order used to unpack captured rows. -struct FusedRule { +struct FusedRule { idx: usize, /// This rule's per-epoch output binding-delta capture (`inspect_batch` - /// appends `(row, weight)`; drained by [`FusedDdJoin::step`]). - captured: CaptureBuf, + /// appends `(row, weight)`; drained by [`FusedDdJoinW::step`]). + captured: CaptureBuf, /// Variable ids packed into each captured row, in capture-column order. var_order: Vec, } -impl FusedDdJoin { - /// Build ONE worker + ONE dataflow for the whole ruleset. `plans` pairs each - /// rule's index with its [`JoinPlan`], in the order they should fire. Every - /// rule — congruence, user, and canonicalization — runs through the same - /// general fused join. - pub fn build(plans: &[(usize, JoinPlan)]) -> Result { +impl FusedDdJoinW { + /// Build ONE worker + ONE dataflow for the whole ruleset. Every rule — + /// congruence, user, and canonicalization — runs through the same general + /// fused join. + fn build(plans: &[(usize, JoinPlan)]) -> Result> { if plans.is_empty() { bail!("cannot build a fused DD join without any rule plans"); } + debug_assert!( + plans.iter().all(|(_, p)| p.width <= WIDTH), + "DD width invariant: every plan must fit the selected row width" + ); let alloc = Allocator::Thread(Thread::default()); let mut worker = Worker::new( WorkerConfig::default(), @@ -475,7 +627,7 @@ impl FusedDdJoin { let probe_in = probe.clone(); // Per-rule capture buffers, allocated outside the closure so we can keep a // clone here and route each rule's output to its head after `step`. - let captures: Vec = rule_plans + let captures: Vec> = rule_plans .iter() .map(|_| Rc::new(RefCell::new(Vec::new()))) .collect(); @@ -496,17 +648,20 @@ impl FusedDdJoin { let inputs = worker.dataflow::(move |scope| { // ONE shared input + base collection per distinct relation, shared by // every atom occurrence (in every rule) that reads it. - let mut inputs: HashMap> = HashMap::new(); + let mut inputs: HashMap, isize>> = + HashMap::new(); let mut rel_coll: HashMap = HashMap::new(); for &read in &reads_in { - let mut session: InputSession = InputSession::new(); + let mut session: InputSession, isize> = InputSession::new(); let coll = session.to_collection(scope); inputs.insert(read, session); rel_coll.insert(read, coll); } - // A collection-level join arranges both inputs at every call site. - // Share base-relation arrangements with the same key projection. - let mut arranged_right = HashMap::new(); + // ONE shared arrangement per (relation view, key-column projection), + // used by EVERY join call site in the ruleset — the right side of + // every stage and BOTH sides of each rule's first join. Only + // intermediate results of 3+-atom rules still arrange privately. + let mut arranged = HashMap::new(); for (rp, cap) in rule_plans.iter().zip(captures_in.iter()) { // This rule's per-atom collection vector, from the SHARED relation @@ -515,58 +670,108 @@ impl FusedDdJoin { let atom_slots = &rp.atoms; let proj = &rp.projection; let step_col = &proj.step_col; - let slots0 = atom_slots[0].clone(); - let sc0 = step_col[0].clone(); - let mut cur = rel_coll[&rp.atom_reads[0]] - .clone() - .flat_map(move |r: Row| bind_atom(&r, &slots0, &sc0)); - - for i in 1..n_atoms { - let slots = atom_slots[i].clone(); - let prev = &step_col[i - 1]; - let next = &step_col[i]; - let shared: Vec = atom_vars(&slots) - .into_iter() - .filter(|v| prev.contains_key(v)) - .collect(); - let shared_cols_left: Vec = shared.iter().map(|v| prev[v]).collect(); - let shared_atom_cols: Vec = shared + + // Positions of `shared` variables (first occurrence) in an atom. + let atom_key_cols = |slots: &[Slot], shared: &[u32]| -> Vec { + shared .iter() .map(|v| { slots .iter() .position(|s| matches!(s, Slot::Var(x) if x == v)) .expect( - "DD join invariant: a shared variable must occur in the joined atom", + "DD join invariant: a shared variable must occur in the atom", ) }) + .collect() + }; + + let mut cur = if n_atoms == 1 { + // Single-atom rule: no join, no arrangement — bind directly. + let ops0 = AtomOps::bind_stage(&atom_slots[0], &step_col[0]); + rel_coll[&rp.atom_reads[0]] + .clone() + .flat_map(move |r: RowN| ops0.apply(&RowN::default(), &r)) + } else { + // First join: BOTH sides are shared raw-relation arrangements + // keyed by the join columns. The bind/remap slot programs run + // inside the join closure; rows the old pre-join bind would + // have filtered (const/dup mismatches) are dropped there, and + // bind is injective on surviving rows, so multiplicities are + // unchanged. + let slots1 = &atom_slots[1]; + let prev = &step_col[0]; + let next = &step_col[1]; + let shared: Vec = atom_vars(slots1) + .into_iter() + .filter(|v| prev.contains_key(v)) + .collect(); + let left_cols = atom_key_cols(&atom_slots[0], &shared); + let right_cols = atom_key_cols(slots1, &shared); + let left = arranged + .entry((rp.atom_reads[0], left_cols.clone())) + .or_insert_with(|| { + rel_coll[&rp.atom_reads[0]] + .clone() + .map(move |r: RowN| (pack_key128(&r, &left_cols), r)) + .arrange_by_key() + }) + .clone(); + let right = arranged + .entry((rp.atom_reads[1], right_cols.clone())) + .or_insert_with(|| { + rel_coll[&rp.atom_reads[1]] + .clone() + .map(move |r: RowN| (pack_key128(&r, &right_cols), r)) + .arrange_by_key() + }) + .clone(); + let ops0 = AtomOps::bind_stage(&atom_slots[0], prev); + let ops1 = AtomOps::join_stage(slots1, prev, next); + left.join_core(right, move |_key, r0: &RowN, r1: &RowN| { + ops0.apply(&RowN::default(), r0) + .and_then(|b| ops1.apply(&b, r1)) + }) + }; + + for i in 2..n_atoms { + let slots = &atom_slots[i]; + let prev = &step_col[i - 1]; + let next = &step_col[i]; + let shared: Vec = atom_vars(slots) + .into_iter() + .filter(|v| prev.contains_key(v)) .collect(); + let shared_cols_left: Vec = shared.iter().map(|v| prev[v]).collect(); + let shared_atom_cols = atom_key_cols(slots, &shared); let left_cols = shared_cols_left.clone(); - let left = cur.map(move |b: Row| (pack_key(&b, &left_cols), b)); - let arrangement_key = (rp.atom_reads[i], shared_atom_cols.clone()); - let right = arranged_right - .entry(arrangement_key) + let left = + cur.map(move |b: RowN| (pack_key128(&b, &left_cols), b)); + let right = arranged + .entry((rp.atom_reads[i], shared_atom_cols.clone())) .or_insert_with(|| { let right_cols = shared_atom_cols.clone(); rel_coll[&rp.atom_reads[i]] .clone() - .map(move |r: Row| (pack_key(&r, &right_cols), r)) + .map(move |r: RowN| (pack_key128(&r, &right_cols), r)) .arrange_by_key() }) .clone(); - let previous_layout = prev.clone(); - let next_layout = next.clone(); + // The compiled `checks` re-verify EVERY shared variable's + // equality, so a fold collision in a wide (>4-column) packed + // key is filtered here rather than producing a false match. + let ops = AtomOps::join_stage(slots, prev, next); cur = left.join_core(right, move |_key, binding, row| { - remap_merge_atom_into(binding, row, &slots, &previous_layout, &next_layout) + ops.apply(binding, row) }); } // Pack the variables needed by body primitives and head actions // into the capture columns expected by `var_order()`. let head_cols = proj.head_cols.clone(); - let cur = cur.map(move |binding: Row| pack_key(&binding, &head_cols)); + let cur = cur.map(move |binding: RowN| pack_cols(&binding, &head_cols)); let cap = Rc::clone(cap); // `step` accumulates captured deltas by binding row before @@ -582,10 +787,25 @@ impl FusedDdJoin { .probe_with(&probe_in); } + if std::env::var("EGGLOG_DD_DUMP_PLANS").is_ok() { + let stages: usize = rule_plans.iter().map(|rp| rp.atoms.len() - 1).sum(); + let private: usize = rule_plans + .iter() + .map(|rp| rp.atoms.len().saturating_sub(2)) + .sum(); + eprintln!( + "[dd-arrange] width={WIDTH} rules={} join_stages={} shared_arrangements={} private_intermediate_arrangements={} input_relations={}", + rule_plans.len(), + stages, + arranged.len(), + private, + reads_in.len(), + ); + } inputs }); - let rules: Vec = rule_meta + let rules: Vec> = rule_meta .into_iter() .zip(captures) .map(|((idx, var_order), captured)| FusedRule { @@ -595,7 +815,7 @@ impl FusedDdJoin { }) .collect(); - Ok(FusedDdJoin { + Ok(FusedDdJoinW { worker, inputs, probe, @@ -606,17 +826,17 @@ impl FusedDdJoin { } /// The rule indices this fused worker serves (build order). - pub fn rule_indices(&self) -> Vec { + fn rule_indices(&self) -> Vec { self.rules.iter().map(|r| r.idx).collect() } /// Distinct body relation read views across the ruleset, in first-use order. - pub fn read_keys(&self) -> &[ReadKey] { + fn read_keys(&self) -> &[ReadKey] { &self.reads } /// Captured variable order for the fused rule at build position `pos`. - pub fn rule_var_order(&self, pos: usize) -> &[u32] { + fn rule_var_order(&self, pos: usize) -> &[u32] { &self.rules[pos].var_order } @@ -627,7 +847,7 @@ impl FusedDdJoin { /// /// CRUCIAL: the InputSessions are NEVER cleared — only the delta is pushed, so /// the DD arrangements persist and the join is genuinely incremental. - pub fn step(&mut self, deltas: &DeltaMap) -> Result { + fn step(&mut self, deltas: &DeltaMap) -> Result { let mut pushed = false; for (read, rows) in deltas { let inp = self.inputs.get_mut(read).ok_or_else(|| { @@ -653,17 +873,26 @@ impl FusedDdJoin { self.drive_to(next_epoch); self.epoch = next_epoch; - let mut accs: Vec, isize>> = + // Net captured deltas keyed by the fixed-width row (`Copy`, no per-pair + // allocation); only surviving nonzero rows allocate an output vec. + // Captured rows are packed to the low `var_order` slots with zeros + // beyond, so whole-row equality is prefix equality. + let mut accs: Vec, isize>> = (0..self.rules.len()).map(|_| HashMap::new()).collect(); for (rule, acc) in self.rules.iter().zip(accs.iter_mut()) { for (row, weight) in rule.captured.borrow_mut().drain(..) { - let key = (0..rule.var_order.len()).map(|i| row[i]).collect(); - *acc.entry(key).or_insert(0) += weight; + *acc.entry(row).or_insert(0) += weight; } } Ok(accs .into_iter() - .map(|acc| acc.into_iter().filter(|(_, w)| *w != 0).collect()) + .zip(&self.rules) + .map(|(acc, rule)| { + acc.into_iter() + .filter(|(_, w)| *w != 0) + .map(|(row, w)| ((0..rule.var_order.len()).map(|i| row[i]).collect(), w)) + .collect() + }) .collect()) } @@ -679,29 +908,53 @@ impl FusedDdJoin { } /// Pack a slice of column values into a fixed-width row (0-padded). -fn pack_row(vals: &[u32]) -> Result { - if vals.len() > W { +fn pack_row(vals: &[u32]) -> Result> { + if vals.len() > WIDTH { bail!( - "DD input row has {} columns, exceeding fixed row width {W}", + "DD input row has {} columns, exceeding fixed row width {WIDTH}", vals.len() ); } - let mut a = Row::default(); + let mut a = RowN::default(); for (i, v) in vals.iter().enumerate() { a[i] = *v; } Ok(a) } -/// Build a join key from selected columns (packed into the low slots). -fn pack_key(r: &Row, cols: &[usize]) -> Row { - let mut a = Row::default(); +/// Repack selected columns into the low slots of a fresh row (0-padded). +fn pack_cols(r: &RowN, cols: &[usize]) -> RowN { + let mut a = RowN::default(); for (i, &c) in cols.iter().enumerate() { a[i] = r[c]; } a } +/// Pack the selected columns into a single `u128` join key. Up to four columns +/// are packed exactly (one per 32-bit lane); wider key sets pack three exact +/// columns plus a fold of the rest into the top lane. A fold collision only +/// routes extra pairs into `join_core`'s output closure, where the compiled +/// [`AtomOps::checks`] on every shared variable filter them out. +fn pack_key128(r: &RowN, cols: &[usize]) -> u128 { + let mut k = 0u128; + if cols.len() <= 4 { + for (i, &c) in cols.iter().enumerate() { + k |= (r[c] as u128) << (32 * i); + } + } else { + for (i, &c) in cols.iter().take(3).enumerate() { + k |= (r[c] as u128) << (32 * i); + } + let mut h = 0x9E37_79B9u32; + for &c in &cols[3..] { + h = h.rotate_left(5) ^ r[c].wrapping_mul(0x85EB_CA6B); + } + k |= (h as u128) << 96; + } + k +} + /// Distinct variables appearing in an atom (column order). fn atom_vars(slots: &[Slot]) -> Vec { let mut out = Vec::new(); @@ -715,91 +968,112 @@ fn atom_vars(slots: &[Slot]) -> Vec { out } -/// Match the first atom's relation row against its slots, producing the initial -/// binding row under the first-step layout (or no row if a constraint -/// fails). Returns a `Vec` for `flat_map`. -fn bind_atom(r: &Row, slots: &[Slot], layout: &HashMap) -> Vec { - let mut out = Row::default(); - let mut local: HashMap = HashMap::new(); - for (i, s) in slots.iter().enumerate() { - let val = r[i]; - match s { - Slot::Const(c) => { - if *c != val { - return Vec::new(); +/// A bind/remap slot program compiled once at dataflow-build time, so the +/// per-tuple closures do pure array reads/writes — no hashing, lookups, or +/// allocation per row. +/// +/// Stage 0 ("bind") matches the first atom's relation row against its slots +/// and lays out its variables under the first-step layout. Later stages +/// ("join") merge an atom's row into the carried binding while changing column +/// layouts; reusing freed columns is what keeps the frontier within the row +/// width. `apply` returns `None` when any constraint fails. +#[derive(Clone, Default)] +struct AtomOps { + /// `row[i]` must equal the constant. + consts: Vec<(usize, u32)>, + /// `row[i]` must equal `row[j]` (repeated variable; `j` is its first slot). + dups: Vec<(usize, usize)>, + /// Copy `binding[prev_col]` into `out[cur_col]` (still-live carried vars; + /// empty at stage 0). + carries: Vec<(usize, usize)>, + /// `row[i]` must equal the carried `out[col]` (shared variable — covers + /// every shared var, including any not in the packed join key). + checks: Vec<(usize, usize)>, + /// Write `row[i]` into `out[col]` (variable born at this atom). + writes: Vec<(usize, usize)>, +} + +impl AtomOps { + /// Compile the first atom: every variable is born here. + fn bind_stage(slots: &[Slot], layout: &HashMap) -> AtomOps { + Self::compile(slots, None, layout) + } + + /// Compile a join stage: `prev` is the left-row layout (step `i-1`), `cur` + /// the output layout (step `i`). + fn join_stage(slots: &[Slot], prev: &HashMap, cur: &HashMap) -> AtomOps { + Self::compile(slots, Some(prev), cur) + } + + fn compile(slots: &[Slot], prev: Option<&HashMap>, cur: &HashMap) -> AtomOps { + let mut ops = AtomOps::default(); + if let Some(prev) = prev { + // Carry over every still-live var (present in `cur`) that the left + // row already holds. Atom-fresh vars are absent from `prev`, so they + // are written from the atom row below instead. + for (&v, &pc) in prev { + if let Some(&cc) = cur.get(&v) { + ops.carries.push((pc, cc)); } } - Slot::Var(v) => { - if let Some(&prev) = local.get(v) { - if prev != val { - return Vec::new(); + } + let mut seen: Vec<(u32, usize)> = Vec::new(); + for (i, s) in slots.iter().enumerate() { + match s { + Slot::Const(c) => ops.consts.push((i, *c)), + Slot::Var(v) => { + if let Some(&(_, j)) = seen.iter().find(|(sv, _)| sv == v) { + ops.dups.push((i, j)); + continue; + } + seen.push((*v, i)); + // Every atom var is live at this step ⇒ present in `cur`. + let cc = cur[v]; + if prev.is_some_and(|p| p.contains_key(v)) { + ops.checks.push((i, cc)); + } else { + ops.writes.push((i, cc)); } - } else { - local.insert(*v, val); - out[layout[v]] = val; } } } + // Deterministic carry order (prev iterates a HashMap). + ops.carries.sort_unstable(); + ops } - vec![out] -} -/// Merge one atom into a binding while changing column layouts. `prev` is the -/// left-row layout (step `i-1`), `cur` is the output layout (step `i`). Carried -/// vars (live in both layouts but not produced here) are copied `prev[v]→cur[v]`; -/// the atom's vars are validated (shared) or written (fresh) at `cur[v]`; every -/// other output column is left zeroed. This is what reuses freed columns and so -/// keeps the frontier within `W`. Returns no row on a constraint failure. -fn remap_merge_atom_into( - b: &Row, - r: &Row, - slots: &[Slot], - prev: &HashMap, - cur: &HashMap, -) -> Vec { - let mut out = Row::default(); - // Carry over every still-live var (present in `cur`) that the left row - // already holds (present in `prev`). Atom-fresh vars are absent from `prev`, - // so they are NOT copied here — they are written from `r` below. - for (&v, &pc) in prev { - if let Some(&cc) = cur.get(&v) { - out[cc] = b[pc]; + /// Run the compiled program over one (binding, atom-row) pair. Stage 0 + /// passes a zero binding (it has no carries or checks). + #[inline] + fn apply( + &self, + b: &RowN, + r: &RowN, + ) -> Option> { + for &(i, c) in &self.consts { + if r[i] != c { + return None; + } } - } - let mut local: HashMap = HashMap::new(); - for (i, s) in slots.iter().enumerate() { - let val = r[i]; - match s { - Slot::Const(c) => { - if *c != val { - return Vec::new(); - } + for &(i, j) in &self.dups { + if r[i] != r[j] { + return None; } - Slot::Var(v) => { - if let Some(&prior) = local.get(v) { - if prior != val { - return Vec::new(); - } - continue; - } - local.insert(*v, val); - // Every atom var is live at this step ⇒ present in `cur`. - let cc = cur[v]; - if prev.contains_key(v) { - // Shared (already bound): the carried value must agree. (The - // join key already enforces this for the key columns; this - // also covers shared vars not in the key, if any.) - if out[cc] != val { - return Vec::new(); - } - } else { - // Fresh var born at this atom: write it. - out[cc] = val; - } + } + let mut out = RowN::default(); + for &(pc, cc) in &self.carries { + out[cc] = b[pc]; + } + for &(i, cc) in &self.checks { + if out[cc] != r[i] { + return None; } } + for &(i, cc) in &self.writes { + out[cc] = r[i]; + } + Some(out) } - vec![out] } #[cfg(test)] @@ -899,14 +1173,48 @@ mod tests { #[test] fn pack_row_checks_the_fixed_width_boundary() { let values = (0..W as u32).collect::>(); - let packed = pack_row(&values).expect("a width-W row must fit"); + let packed = pack_row::(&values).expect("a width-W row must fit"); assert_eq!(&packed.0[..], values.as_slice()); - let error = pack_row(&[0; W + 1]).unwrap_err(); + let error = pack_row::(&[0; W + 1]).unwrap_err(); assert!(error.to_string().contains(&format!("{} columns", W + 1))); assert!(error.to_string().contains(&format!("fixed row width {W}"))); } + #[test] + fn plans_pick_the_narrowest_ladder_width() { + let f = FunctionId::new(0); + // Two ternary atoms sharing one var: frontier of 5 vars → width 8. + let plan = plan_of(&[(f, &[0, 1, 2]), (f, &[2, 3, 4])]); + assert!(plan.width <= 8); + let fused = FusedDdJoin::build(&[(0, plan)]).unwrap(); + assert!(matches!(fused, FusedDdJoin::W8(_))); + } + + #[test] + fn wide_key_fold_still_joins_exactly() { + let f = FunctionId::new(0); + let g = FunctionId::new(1); + // Six shared columns: the u128 key packs 3 exact + a fold of the rest, + // and the compiled checks must reject near-miss rows. + let atoms: &[(FunctionId, &[u32])] = + &[(f, &[0, 1, 2, 3, 4, 5, 6]), (g, &[1, 2, 3, 4, 5, 6, 7])]; + let mut rows = HashMap::new(); + rows.insert(f, vec![(vec![10, 1, 2, 3, 4, 5, 6], 1)]); + rows.insert( + g, + vec![ + (vec![1, 2, 3, 4, 5, 6, 20], 1), + // Differs only in the FOLDED tail — must not match. + (vec![1, 2, 3, 4, 5, 7, 21], 1), + ], + ); + assert_eq!( + run_once(atoms, &rows), + vec![(vec![10, 1, 2, 3, 4, 5, 6, 20], 1)] + ); + } + #[test] fn plan_reuses_columns_across_wide_variable_chain() { let func = FunctionId::new(0); diff --git a/egglog-experimental/dd/src/interpret.rs b/egglog-experimental/dd/src/interpret.rs index 554c6ef0..ac2083eb 100644 --- a/egglog-experimental/dd/src/interpret.rs +++ b/egglog-experimental/dd/src/interpret.rs @@ -29,6 +29,69 @@ use anyhow::{anyhow, Result}; use egglog_ast::core::{GenericAtom, GenericAtomTerm, GenericCoreAction, GenericCoreActions}; + +/// Env-gated (`EGGLOG_DD_TIMING=1`) per-iteration phase timing, printed to +/// stderr as one line per `run_iteration`. Diagnostic-only. +pub(crate) mod phase_timing { + use std::cell::RefCell; + use std::time::{Duration, Instant}; + thread_local! { + static PHASES: RefCell> = const { RefCell::new(Vec::new()) }; + static NOTES: RefCell> = const { RefCell::new(Vec::new()) }; + } + pub fn enabled() -> bool { + static ON: std::sync::OnceLock = std::sync::OnceLock::new(); + *ON.get_or_init(|| std::env::var("EGGLOG_DD_TIMING").is_ok()) + } + pub fn time(name: &'static str, f: impl FnOnce() -> T) -> T { + if !enabled() { + return f(); + } + let t = Instant::now(); + let r = f(); + let d = t.elapsed(); + PHASES.with(|p| p.borrow_mut().push((name, d))); + r + } + pub fn note(name: &'static str, value: usize) { + if enabled() { + NOTES.with(|n| n.borrow_mut().push((name, value))); + } + } + pub fn flush() { + if !enabled() { + return; + } + let mut parts: Vec = Vec::new(); + PHASES.with(|p| { + let mut merged: Vec<(&'static str, Duration)> = Vec::new(); + for (n, d) in p.borrow_mut().drain(..) { + if let Some(e) = merged.iter_mut().find(|(m, _)| *m == n) { + e.1 += d; + } else { + merged.push((n, d)); + } + } + for (n, d) in merged { + parts.push(format!("{n}={:.1}ms", d.as_secs_f64() * 1e3)); + } + }); + NOTES.with(|no| { + let mut merged: Vec<(&'static str, usize)> = Vec::new(); + for (n, v) in no.borrow_mut().drain(..) { + if let Some(e) = merged.iter_mut().find(|(m, _)| *m == n) { + e.1 += v; + } else { + merged.push((n, v)); + } + } + for (n, v) in merged { + parts.push(format!("{n}={v}")); + } + }); + eprintln!("[dd-timing] {}", parts.join(" ")); + } +} use egglog_ast::generic_ast::Change; use egglog_backend_trait::{ FunctionId, ReadMode, RuleActionCall, RuleBodyCall, RuleSpec, RuleValue, RuleVar, Value, @@ -37,13 +100,12 @@ use egglog_numeric_id::NumericId; use hashbrown::{HashMap, HashSet}; use crate::compile::{ReadKey, Row}; -use crate::{EGraph, TableDefault}; +use crate::{EGraph, TableDefault, ViewOp}; /// Binding environment: variable id → bound `u32` value. pub(crate) type Env = HashMap; type DdDeltaRows = HashMap, isize)>>; -type LookupIndex = HashMap>; /// Retractions batched per function: the key length plus the set of keys to /// remove, so one `retain` pass drops them all. @@ -122,13 +184,9 @@ pub fn run_iteration(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result = Vec::new(); - // Iteration-scoped `key -> outputs` index for `lookup_or_create` (eq-sort - // constructor hash-cons). Built lazily per function so repeated lookups in - // one iteration are O(1) instead of rescanning the growing mirror each time. - let mut lookup_index = LookupIndex::new(); // Compute every rule's binding envs FIRST (so the whole atom-bearing ruleset // runs on one fused DD worker via `fused_bindings`), THEN @@ -137,17 +195,14 @@ pub fn run_iteration(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result Result<()> { + for ((_, rule), envs) in rules.iter().zip(envs_by_rule.into_iter()) { + for mut env in envs { + apply_head(eg, &rule.core.head, &mut env, &mut writes)?; + } } - } + Ok(()) + })?; // Apply collected writes to the mirror. // @@ -162,7 +217,7 @@ pub fn run_iteration(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result = Vec::new(); let mut subsumes: Vec<(FunctionId, Vec)> = Vec::new(); @@ -178,20 +233,25 @@ pub fn run_iteration(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result subsumes.push((f, prefix)), } } - for (f, (keylen, keys)) in removes_by_func { - // A `delete`/rebuild retraction clears the row from BOTH the live mirror - // and the subsumed side-set (a rebuilt-away subsumed row must not linger). - changed |= eg.remove_matching_keys(f, keylen, &keys); - } - // The backend transaction retains head-emission order within a table, - // orders tables by merge-read dependencies, and processes merge-generated - // writes in subsequent waves until reaching a fixed point. - changed |= eg.apply_sets(sets)?; - // Subsumes last: a row `set` this iteration can then be subsumed, and the - // move reads the just-updated live mirror. - for (f, prefix) in subsumes { - changed |= eg.subsume_rows(f, &prefix); - } + phase_timing::note("n_sets", sets.len()); + phase_timing::time("apply_writes", || -> Result<()> { + for (f, (keylen, keys)) in removes_by_func { + // A `delete`/rebuild retraction clears the row from BOTH the live mirror + // and the subsumed side-set (a rebuilt-away subsumed row must not linger). + changed |= eg.remove_matching_keys(f, keylen, &keys); + } + // The backend transaction retains head-emission order within a table, + // orders tables by merge-read dependencies, and processes merge-generated + // writes in subsequent waves until reaching a fixed point. + changed |= eg.apply_sets(sets)?; + // Subsumes last: a row `set` this iteration can then be subsumed, and the + // move reads the just-updated live mirror. + for (f, prefix) in subsumes { + changed |= eg.subsume_rows(f, &prefix); + } + Ok(()) + })?; + phase_timing::flush(); Ok(changed) } @@ -268,7 +328,7 @@ fn fused_bindings(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result Result = Vec::new(); + phase_timing::time("diff", || { + if !eg.dd_fused_cursors.contains_key(&key) { + let mut cursors = HashMap::new(); + let mut seed: DdDeltaRows = HashMap::new(); + for &read in &all_reads { + let log = eg.event_logs.entry(read).or_default(); + cursors.insert(read, log.end()); + let rows = view_rows(eg, read); + if !rows.is_empty() { + seed.insert(read, rows); + } + } + eg.dd_fused_cursors.insert(key.clone(), cursors); + if !seed.is_empty() { + delta_batches.push(seed); + } + return; + } + + let mut removals_batch: DdDeltaRows = HashMap::new(); + let mut insertions_batch: DdDeltaRows = HashMap::new(); + let cursors = eg + .dd_fused_cursors + .get_mut(&key) + .expect("DD cursor invariant: checked above"); for &read in &all_reads { - let cur = match read.mode { - ReadMode::Live => eg.live_versions.get(&read.func), - ReadMode::Subsumed => eg.subsumed_versions.get(&read.func), - ReadMode::All => eg.all_versions.get(&read.func), - }; - let cur_empty: HashMap = HashMap::new(); - let cur = cur.unwrap_or(&cur_empty); - let prev = fed.entry(read).or_default(); - if prev == cur { + let log = eg + .event_logs + .get(&read) + .expect("DD cursor invariant: a cached worker subscribes to every view it reads"); + let cursor = cursors + .get_mut(&read) + .expect("DD cursor invariant: a cached worker has a cursor per view"); + let start = cursor + .checked_sub(log.base) + .expect("DD cursor invariant: consumed events are never drained past a cursor"); + if start >= log.events.len() { continue; } - + phase_timing::note("diff_rows_scanned", log.events.len() - start); + + // Fold the window per row: net presence change plus last event + // sign (events are real state transitions, so the last sign is + // end-of-window presence). + let mut folded: HashMap<&Row, (isize, i8)> = HashMap::new(); + for (row, sign) in &log.events[start..] { + let entry = folded.entry(row).or_insert((0, 0)); + entry.0 += *sign as isize; + entry.1 = *sign; + } let mut removals = Vec::new(); let mut insertions = Vec::new(); - for row in prev.keys() { - if !cur.contains_key(row) { - removals.push((row.to_vec(), -1)); - } - } - for (row, version) in cur { - match prev.get(row) { - None => insertions.push((row.to_vec(), 1)), - Some(prev_version) if prev_version != version => { + for (row, (net, last)) in folded { + match net.cmp(&0) { + std::cmp::Ordering::Less => removals.push((row.to_vec(), -1)), + std::cmp::Ordering::Greater => insertions.push((row.to_vec(), 1)), + std::cmp::Ordering::Equal if last > 0 => { removals.push((row.to_vec(), -1)); insertions.push((row.to_vec(), 1)); } - Some(_) => {} + std::cmp::Ordering::Equal => {} } } if !removals.is_empty() { @@ -335,16 +428,24 @@ fn fused_bindings(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result = Vec::new(); - if !removals_batch.is_empty() { - delta_batches.push(removals_batch); - } - if !insertions_batch.is_empty() { - delta_batches.push(insertions_batch); - } + phase_timing::note( + "delta_removals", + removals_batch.values().map(|r| r.len()).sum::(), + ); + phase_timing::note( + "delta_insertions", + insertions_batch.values().map(|r| r.len()).sum::(), + ); + if !removals_batch.is_empty() { + delta_batches.push(removals_batch); + } + if !insertions_batch.is_empty() { + delta_batches.push(insertions_batch); + } + }); + drain_consumed_events(eg, &all_reads); // Step the shared worker once per nonempty signed-delta phase. A version // change may require a removal phase followed by an insertion phase. @@ -355,7 +456,12 @@ fn fused_bindings(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result(), + ); + let stepped = phase_timing::time("dd_step", || fused.step(delta))?; for (acc, rows) in per_rule_bindings.iter_mut().zip(stepped) { acc.extend(rows); } @@ -365,29 +471,36 @@ fn fused_bindings(eg: &mut EGraph, rules: &[(usize, RuleSpec)]) -> Result = Vec::new(); - for (bind, w) in &bindings { - if *w <= 0 { - continue; - } - let mut env: Env = Env::new(); - for (i, &v) in var_order.iter().enumerate() { - env.insert(v, bind[i]); - } - let mut es: Vec = vec![env]; - for atom in &rule.core.body.atoms { - if matches!(atom.head, RuleBodyCall::Primitive { .. }) { - es = step_prim(eg, atom, es)?; + phase_timing::note( + "bindings_out", + per_rule_bindings.iter().map(|b| b.len()).sum::(), + ); + phase_timing::time("envs_prims", || -> Result<()> { + for (fpos, bindings) in per_rule_bindings.into_iter().enumerate() { + let caller_pos = fused_positions[fpos]; + let rule = &rules[caller_pos].1; + let var_order = &var_orders[fpos]; + let mut envs: Vec = Vec::new(); + for (bind, w) in &bindings { + if *w <= 0 { + continue; } + let mut env: Env = Env::new(); + for (i, &v) in var_order.iter().enumerate() { + env.insert(v, bind[i]); + } + let mut es: Vec = vec![env]; + for atom in &rule.core.body.atoms { + if matches!(atom.head, RuleBodyCall::Primitive { .. }) { + es = step_prim(eg, atom, es)?; + } + } + envs.extend(es); } - envs.extend(es); + out[caller_pos] = envs; } - out[caller_pos] = envs; - } + Ok(()) + })?; Ok(out) } @@ -450,7 +563,6 @@ fn apply_head( head: &GenericCoreActions, env: &mut Env, writes: &mut Vec, - lookup_index: &mut LookupIndex, ) -> Result<()> { for action in &head.0 { match action { @@ -463,7 +575,7 @@ fn apply_head( .copied() .map(Value::new) .collect::>(); - let values = lookup_or_create(eg, *id, &key, lookup_index)?; + let values = lookup_or_create(eg, *id, &key)?; Some(Value::new(values[0])) } RuleActionCall::Primitive { id, .. } => { @@ -472,7 +584,17 @@ fn apply_head( .copied() .map(Value::new) .collect::>(); - eg.eval_prim_internal(*id, &arguments)? + // The term encoder's `set-if-empty` / view-proof ops are + // serviced against the mirror here (the db external + // function for them only panics); every other primitive + // runs on the embedded db. + if let Some(op) = eg.set_if_empty_ops.get(id).cloned() { + Some(set_if_empty_apply(eg, &op, &arguments)?) + } else if let Some(op) = eg.view_proof_ops.get(id).cloned() { + Some(view_proof_apply(eg, &op, &arguments)?) + } else { + eg.eval_prim_internal(*id, &arguments)? + } } }; if let Some(result) = result { @@ -516,6 +638,47 @@ fn apply_head( Ok(()) } +/// All rows currently visible in a relation read view, as `+1` seed deltas for +/// a freshly built fused worker. +fn view_rows(eg: &EGraph, read: ReadKey) -> Vec<(Vec, isize)> { + let mut out = Vec::new(); + let mut push = |store: &HashMap>| { + if let Some(rows) = store.get(&read.func) { + out.extend(rows.iter().map(|r| (r.to_vec(), 1isize))); + } + }; + match read.mode { + ReadMode::Live => push(&eg.mirror), + ReadMode::Subsumed => push(&eg.subsumed), + ReadMode::All => { + push(&eg.mirror); + push(&eg.subsumed); + } + } + out +} + +/// Advance each view's event log past the prefix that every subscribed fused +/// worker has already consumed, so logs stay proportional to unconsumed work. +fn drain_consumed_events(eg: &mut EGraph, reads: &[ReadKey]) { + for read in reads { + let min = eg + .dd_fused_cursors + .values() + .filter_map(|cursors| cursors.get(read)) + .min() + .copied(); + let Some(min) = min else { continue }; + if let Some(log) = eg.event_logs.get_mut(read) { + let consumed = min.saturating_sub(log.base); + if consumed > 0 { + log.events.drain(..consumed); + log.base = min; + } + } + } +} + fn term_value(term: &GenericAtomTerm, env: &Env) -> Option { match term { GenericAtomTerm::Var(_, variable) => env.get(&variable.id).copied(), @@ -536,15 +699,10 @@ fn resolve_terms(terms: &[GenericAtomTerm], env: &Env) -> Re /// Look up the output of `func` for input `key`. If absent, create the row with /// a fresh id (eq-sort constructor semantics — mirrors `add_term`). The created -/// row is written directly into the mirror so subsequent lookups in the same -/// iteration see it (hash-cons). -pub(crate) fn lookup_or_create( - eg: &mut EGraph, - func: FunctionId, - key: &[Value], - index: &mut LookupIndex, -) -> Result { - if let Some(values) = lookup_existing(eg, func, key, index) { +/// row is written directly into the mirror (which maintains the persistent +/// key index) so subsequent lookups in the same iteration see it (hash-cons). +pub(crate) fn lookup_or_create(eg: &mut EGraph, func: FunctionId, key: &[Value]) -> Result { + if let Some(values) = lookup_existing(eg, func, key) { return Ok(values); } let (n_keys, _, default, _) = eg.function_spec(func); @@ -571,9 +729,7 @@ pub(crate) fn lookup_or_create( )); } }; - let k: Row = key.iter().map(|value| value.rep()).collect(); let values: Row = vec![value].into_boxed_slice(); - index.entry(func).or_default().insert(k, values.clone()); let mut full: Vec = key.iter().map(|v| v.rep()).collect(); full.push(value); let row = full.into_boxed_slice(); @@ -581,40 +737,49 @@ pub(crate) fn lookup_or_create( Ok(values) } -/// Look up all current outputs of `func` for input `key` without creating a row. -pub(crate) fn lookup_existing( - eg: &EGraph, - func: FunctionId, - key: &[Value], - index: &mut LookupIndex, -) -> Option { - let n_keys = eg.n_keys(func); - // Lazily build the key->outputs index for this function from live ∪ subsumed - // rows so repeated lookups within one iteration are O(1) instead of O(state) - // scans. A subsumed constructor row is still the current table row in the - // reference backend; looking it up must not mint a fresh visible row. - let idx = index.entry(func).or_insert_with(|| { - let live_len = eg.mirror.get(&func).map_or(0, |rows| rows.len()); - let subsumed_len = eg.subsumed.get(&func).map_or(0, |rows| rows.len()); - let mut values_by_key = HashMap::with_capacity(live_len + subsumed_len); - if let Some(set) = eg.mirror.get(&func) { - for row in set.iter() { - let key: Row = row[..n_keys].into(); - let values: Row = row[n_keys..].into(); - values_by_key.insert(key, values); - } - } - if let Some(set) = eg.subsumed.get(&func) { - for row in set.iter() { - let key: Row = row[..n_keys].into(); - let values: Row = row[n_keys..].into(); - values_by_key.entry(key).or_insert(values); - } - } - values_by_key - }); - let key: Row = key.iter().map(|value| value.rep()).collect(); - idx.get(&key).cloned() +/// Look up the current outputs of `func` for input `key` without creating a +/// row, through the persistent key index. A subsumed constructor row is still +/// the current table row in the reference backend; looking it up must not mint +/// a fresh visible row. +pub(crate) fn lookup_existing(eg: &EGraph, func: FunctionId, key: &[Value]) -> Option { + let key: Vec = key.iter().map(|value| value.rep()).collect(); + eg.index_lookup_live_first(func, &key) +} + +/// Service the term encoder's `set-if-empty` op against the mirror: return the +/// e-class (output col 0) of the existing `(view keys)` row, or insert +/// `(keys, default_vals)` — the args after the keys — and return the default +/// e-class. Writes immediately (like `lookup_or_create`) so repeated term +/// construction in one iteration dedups to the same e-class. +fn set_if_empty_apply(eg: &mut EGraph, op: &ViewOp, args: &[Value]) -> Result { + let view = *eg + .table_ids + .get(&op.view_name) + .ok_or_else(|| anyhow!("set-if-empty view `{}` is not registered", op.view_name))?; + let keys = &args[..op.n_keys]; + if let Some(values) = lookup_existing(eg, view, keys) { + return Ok(Value::new(values[0])); + } + let end = op.n_keys + op.out_arity; + let full: Row = args[..end].iter().map(|v| v.rep()).collect(); + eg.insert_live_row(view, full); + Ok(args[op.n_keys]) +} + +/// Service the term encoder's view-proof read against the mirror: the proof +/// column (output col 1) of the existing `(view keys)` row, or the `fallback` +/// arg (the one after the keys) when the key is absent. +fn view_proof_apply(eg: &mut EGraph, op: &ViewOp, args: &[Value]) -> Result { + let view = *eg + .table_ids + .get(&op.view_name) + .ok_or_else(|| anyhow!("view-proof view `{}` is not registered", op.view_name))?; + let keys = &args[..op.n_keys]; + let fallback = args[op.n_keys]; + Ok(match lookup_existing(eg, view, keys) { + Some(values) => Value::new(values[1]), + None => fallback, + }) } #[cfg(test)] diff --git a/egglog-experimental/dd/src/lib.rs b/egglog-experimental/dd/src/lib.rs index cd5fa752..866bff18 100644 --- a/egglog-experimental/dd/src/lib.rs +++ b/egglog-experimental/dd/src/lib.rs @@ -28,8 +28,8 @@ use egglog_ast::core::{GenericAtomTerm, GenericCoreAction}; use egglog_backend_trait::{ Backend, BaseValues, ColumnTy, ContainerMergeFn, ContainerValues, CounterId, DefaultVal, ExecutionState, ExternalFunction, ExternalFunctionId, FunctionConfig, FunctionId, - IterationReport, MergeAction, MergeFn, ReportLevel, RuleActionCall, RuleBodyCall, RuleId, - RuleSetRun, RuleSpec, RuleValue, RuleVar, ScanEntry, Value, + IterationReport, MergeAction, MergeFn, ReadMode, ReportLevel, RuleActionCall, RuleBodyCall, + RuleId, RuleSetRun, RuleSpec, RuleValue, RuleVar, ScanEntry, Value, }; use egglog_core_relations::Database; use egglog_numeric_id::NumericId; @@ -44,6 +44,25 @@ use compile::{validate_merge, visit_merge_read_dependencies, ReadKey, Row}; type LocatedValues = (Row, RowLocation); type RowReplacement = (Row, LocatedValues); +/// An append-only signed row event log for one relation read view, with a +/// drained-prefix offset so cursors stay absolute across truncation. +#[derive(Default)] +pub(crate) struct EventLog { + /// Absolute position of `events[0]` (everything before it was consumed by + /// every subscribed fused worker and drained). + pub(crate) base: usize, + /// Signed state transitions: `+1` row became visible in this view, `-1` it + /// left. Only real transitions are logged, so within any window a row's + /// last event sign tells its end-of-window presence. + pub(crate) events: Vec<(Row, i8)>, +} + +impl EventLog { + pub(crate) fn end(&self) -> usize { + self.base + self.events.len() + } +} + mod dd_workers { use hashbrown::HashMap; @@ -143,8 +162,11 @@ pub struct EGraph { /// A core-relations [`Database`] used purely as the base-value / primitive /// engine, so `Value`s are bit-for-bit identical to the reference backend. db: Database, - /// Monotonic fresh-id counter for `fresh_id` / `add_term`. - pub(crate) next_id: u32, + /// Monotonic fresh-id counter, shared by the native `FreshId` default + /// (`fresh_id_internal`) and the term encoder's `get-fresh!` primitive (via + /// [`Backend::eclass_id_counter`]) so the two id sources never collide. Lives + /// in `db` (survives `db.clone()`); `id_counter` is its handle. + pub(crate) id_counter: CounterId, /// Atom-less rules (`(rule () …)`) fire ONCE; an entry here marks a rule /// index as already fired. The DD dataflow has no input relation to drive an /// atom-less body, so this fired-marker is the one piece of seminaive @@ -155,20 +177,49 @@ pub struct EGraph { /// whole ruleset), keyed by the sorted live rule-index list. This is the /// join path the interpreter drives. pub(crate) dd_fused: DdWorkers, - /// Monotone version assigned whenever a row becomes visible in one of the - /// DD read views. This stands in for the reference backend's hidden - /// timestamp: removing and reinserting the same row gives it a fresh version, - /// so seminaive rules can fire on it again. - pub(crate) next_row_version: u64, - pub(crate) live_versions: HashMap>, - pub(crate) all_versions: HashMap>, - pub(crate) subsumed_versions: HashMap>, - /// Per-ruleset, per-function version snapshot last fed to the fused DD join. - pub(crate) dd_fused_fed_versions: HashMap, HashMap>>, + /// Persistent per-function `key -> present rows` index over live ∪ subsumed + /// rows, maintained incrementally by [`Self::record_row_event`]. Keys are + /// the leading `n_keys` columns; each entry lists a present row's value + /// columns and location, in insertion order (a key transiently holds more + /// than one entry when raw seed inserts collide before a merge transaction + /// normalizes them). This is what makes merge transactions, lookups, and + /// keyed removals O(keys touched) instead of O(table). + pub(crate) by_key: HashMap>>, + /// Per-view append-only row event logs (`+1` insert, `-1` remove), written + /// by [`Self::record_row_event`] for every view some live fused worker + /// reads. Each worker consumes its unread suffix through + /// [`Self::dd_fused_cursors`]; folding that window per row reproduces the + /// remove/insert (and remove-then-reinsert refire) batches the DD join + /// needs, in O(delta). Fully-consumed prefixes are drained. + pub(crate) event_logs: HashMap, + /// Per-ruleset, per-view absolute cursor (`EventLog::base`-relative) of the + /// events already fed to that ruleset's fused worker. + pub(crate) dd_fused_cursors: HashMap, HashMap>, /// Deferred error channel used by panic primitives. The embedded database's /// cloned external functions share this channel, matching the reference /// bridge's panic-function behavior. panic_message: Arc>>, + /// Relation name → `FunctionId`, populated by `add_table`. Lets the term + /// encoder's `set-if-empty` / view-proof ops (registered by view NAME before + /// the view table exists) resolve their view to a live relation at invoke + /// time. + pub(crate) table_ids: HashMap, + /// `set-if-empty` ops keyed by the `ExternalFunctionId` the frontend resolves + /// their call sites to. The interpreter services these against the `mirror` + /// instead of calling the (panic) db external function. + pub(crate) set_if_empty_ops: HashMap, + /// View-proof reader ops, keyed like `set_if_empty_ops`. + pub(crate) view_proof_ops: HashMap, +} + +/// A term-encoding view op (`set-if-empty` or view-proof) the DD interpreter +/// services against its `mirror`: the FD view table name plus its key/output +/// column counts. +#[derive(Clone)] +pub(crate) struct ViewOp { + pub(crate) view_name: String, + pub(crate) n_keys: usize, + pub(crate) out_arity: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -190,9 +241,12 @@ impl CurrentRow { } } +/// Transaction-local overlay over the persistent [`EGraph::by_key`] index: +/// only keys this transaction touched are materialized here; everything else +/// is read through the index on demand. #[derive(Default)] struct FunctionMergeState { - current: HashMap, + staged: HashMap, original: HashMap>, touched: Vec, } @@ -215,22 +269,27 @@ impl EGraph { // `register_type` is idempotent, so a later frontend registration is a // no-op that returns the same id. db.base_values_mut().register_type::<()>(); + // One counter feeds both `fresh_id_internal` and `get-fresh!`. Burn its + // initial 0 so the first minted id is 1, keeping 0 as a "null"/padding + // sentinel for the fixed-width DD rows. + let id_counter = db.add_counter(); + db.inc_counter(id_counter); EGraph { relations: Vec::new(), rules: Vec::new(), mirror: HashMap::new(), subsumed: HashMap::new(), db, - // Start at 1 so id 0 stays a "null"/padding sentinel. - next_id: 1, + id_counter, seen: HashMap::new(), dd_fused: DdWorkers::default(), - next_row_version: 1, - live_versions: HashMap::new(), - all_versions: HashMap::new(), - subsumed_versions: HashMap::new(), - dd_fused_fed_versions: HashMap::new(), + by_key: HashMap::new(), + event_logs: HashMap::new(), + dd_fused_cursors: HashMap::new(), panic_message: Default::default(), + table_ids: HashMap::new(), + set_if_empty_ops: HashMap::new(), + view_proof_ops: HashMap::new(), } } @@ -435,9 +494,7 @@ impl EGraph { } pub(crate) fn fresh_id_internal(&mut self) -> u32 { - let id = self.next_id; - self.next_id += 1; - id + self.db.inc_counter(self.id_counter) as u32 } /// Apply full-row sets in dependency-ordered waves. Merge-generated sets @@ -500,35 +557,6 @@ impl EGraph { inserted } - fn current_rows_by_key(&self, f: FunctionId, n_keys: usize) -> HashMap { - let live_len = self.mirror.get(&f).map(|set| set.len()).unwrap_or(0); - let subsumed_len = self.subsumed.get(&f).map(|set| set.len()).unwrap_or(0); - let mut cur = HashMap::with_capacity(live_len + subsumed_len); - for (location, store) in [ - (RowLocation::Live, &self.mirror), - (RowLocation::Subsumed, &self.subsumed), - ] { - if let Some(rows) = store.get(&f) { - for row in rows.iter() { - let values: Row = row[n_keys..].into(); - let key: Row = row[..n_keys].into(); - cur.entry(key) - .and_modify(|current: &mut CurrentRow| { - current.values = values.clone(); - current.location = location; - current.rows_for_key += 1; - }) - .or_insert(CurrentRow { - values, - location, - rows_for_key: 1, - }); - } - } - } - cur - } - fn replace_located_rows( &mut self, f: FunctionId, @@ -567,6 +595,30 @@ impl EGraph { keylen: usize, keys: &HashSet>, ) -> bool { + // Fast path: exact-key removals resolve each key through the + // persistent index — O(keys) instead of an O(table) retain scan. A + // non-key-length prefix (allowed by the delete action's shape) still + // scans. + if keylen == self.info(f).n_keys { + let mut changed = false; + for key in keys { + let Some(entries) = self.by_key.get(&f).and_then(|k| k.get(&key[..])) else { + continue; + }; + for (values, location) in entries.clone() { + let row = row_with_values(key, &values); + let (store, deltas) = match location { + RowLocation::Live => (&mut self.mirror, (-1, -1, 0)), + RowLocation::Subsumed => (&mut self.subsumed, (0, -1, -1)), + }; + if store.get_mut(&f).is_some_and(|rows| rows.remove(&row)) { + self.record_row_event(f, row, deltas.0, deltas.1, deltas.2); + changed = true; + } + } + } + return changed; + } let mut changed = false; let removed_live = remove_keys_from_store(&mut self.mirror, f, keylen, keys); changed |= !removed_live.is_empty(); @@ -589,22 +641,84 @@ impl EGraph { all_delta: isize, subsumed_delta: isize, ) { - let version = if live_delta > 0 || all_delta > 0 || subsumed_delta > 0 { - let version = self.next_row_version; - self.next_row_version += 1; - Some(version) - } else { - None - }; - update_version_map(&mut self.live_versions, func, &row, live_delta, version); - update_version_map(&mut self.all_versions, func, &row, all_delta, version); - update_version_map( - &mut self.subsumed_versions, - func, - &row, - subsumed_delta, - version, - ); + // Maintain the persistent key index. The live/subsumed deltas describe + // exactly which located entry appears or disappears (the all view is a + // union, not separate storage). + let n_keys = self.info(func).n_keys; + let key: Row = row[..n_keys].into(); + let values: Row = row[n_keys..].into(); + for (delta, location) in [ + (live_delta, RowLocation::Live), + (subsumed_delta, RowLocation::Subsumed), + ] { + match delta.cmp(&0) { + std::cmp::Ordering::Greater => { + self.by_key + .entry(func) + .or_default() + .entry(key.clone()) + .or_default() + .push((values.clone(), location)); + } + std::cmp::Ordering::Less => { + if let Some(keys) = self.by_key.get_mut(&func) { + if let Some(entries) = keys.get_mut(&key) { + if let Some(pos) = entries + .iter() + .position(|(v, loc)| *loc == location && *v == values) + { + entries.remove(pos); + } + if entries.is_empty() { + keys.remove(&key); + } + } + } + } + std::cmp::Ordering::Equal => {} + } + } + + // Append to the event log of every subscribed view this transition + // touches. Unsubscribed views (no live fused worker reads them) are not + // logged: a worker built later seeds from the full current state. + for (delta, mode) in [ + (live_delta, ReadMode::Live), + (all_delta, ReadMode::All), + (subsumed_delta, ReadMode::Subsumed), + ] { + if delta != 0 { + if let Some(log) = self.event_logs.get_mut(&ReadKey { func, mode }) { + log.events.push((row.clone(), delta.signum() as i8)); + } + } + } + } + + /// The current row for `key` in `func` (last inserted wins when raw seed + /// inserts transiently collide), with the number of rows present for the + /// key — the lazy replacement for whole-table merge-state snapshots. + fn index_current_row(&self, func: FunctionId, key: &[u32]) -> Option { + let entries = self.by_key.get(&func)?.get(key)?; + let (values, location) = entries.last()?; + Some(CurrentRow { + values: values.clone(), + location: *location, + rows_for_key: entries.len(), + }) + } + + /// The output columns for `key`, preferring live rows over subsumed ones (a + /// subsumed constructor row is still the current table row, so it must + /// resolve rather than mint a fresh one). + pub(crate) fn index_lookup_live_first(&self, func: FunctionId, key: &[u32]) -> Option { + let entries = self.by_key.get(&func)?.get(key)?; + entries + .iter() + .rev() + .find(|(_, loc)| *loc == RowLocation::Live) + .or_else(|| entries.last()) + .map(|(values, _)| values.clone()) } } @@ -623,7 +737,7 @@ struct MergeTransaction<'a> { impl<'a> MergeTransaction<'a> { fn new(eg: &'a mut EGraph, sets: Vec<(FunctionId, Row)>) -> Self { - let next_id_at_start = eg.next_id; + let next_id_at_start = eg.db.read_counter(eg.id_counter) as u32; Self { eg, pending: sets, @@ -638,86 +752,103 @@ impl<'a> MergeTransaction<'a> { fn run(mut self) -> Result { let result = self.run_inner(); if result.is_err() { - self.eg.next_id = self.next_id_at_start; + self.eg + .db + .set_counter(self.eg.id_counter, self.next_id_at_start as usize); } result } fn run_inner(&mut self) -> Result { - while !self.pending.is_empty() { - let mut wave = std::mem::take(&mut self.pending); - wave.sort_by_key(|(function, _)| (self.eg.info(*function).merge_level, function.rep())); - for (function, row) in wave { - self.apply_set(function, &row)?; + interpret::phase_timing::time("mt_apply", || -> Result<()> { + while !self.pending.is_empty() { + let mut wave = std::mem::take(&mut self.pending); + wave.sort_by_key(|(function, _)| { + (self.eg.info(*function).merge_level, function.rep()) + }); + for (function, row) in wave { + self.apply_set(function, &row)?; + } + self.pending = std::mem::take(&mut self.next_wave); } - self.pending = std::mem::take(&mut self.next_wave); - } + Ok(()) + })?; let states = std::mem::take(&mut self.states); - for function in std::mem::take(&mut self.state_order) { - let state = states - .get(&function) - .expect("merge state order must reference initialized state"); - let n_keys = self.eg.n_keys(function); - let mut replacements = Vec::new(); - for key in &state.touched { - let current = state - .current - .get(key) - .expect("a touched merge key must have a current row"); - let original = state.original[key].as_ref(); - let already_normalized = original.is_some_and(|old| { - old.rows_for_key == 1 - && old.location == current.location - && old.values == current.values - }); - if !already_normalized { - replacements.push((key.clone(), current.located())); + interpret::phase_timing::time("mt_land", || { + for function in std::mem::take(&mut self.state_order) { + let state = states + .get(&function) + .expect("merge state order must reference initialized state"); + let n_keys = self.eg.n_keys(function); + let mut replacements = Vec::new(); + for key in &state.touched { + let current = state + .staged + .get(key) + .expect("a touched merge key must have a staged row"); + let original = state.original[key].as_ref(); + let already_normalized = original.is_some_and(|old| { + old.rows_for_key == 1 + && old.location == current.location + && old.values == current.values + }); + if !already_normalized { + replacements.push((key.clone(), current.located())); + } } + interpret::phase_timing::note("mt_replacements", replacements.len()); + self.changed |= self.eg.replace_located_rows(function, n_keys, replacements); } - self.changed |= self.eg.replace_located_rows(function, n_keys, replacements); - } + }); - Ok(self.changed || self.eg.next_id != self.next_id_at_start) + Ok(self.changed + || self.eg.db.read_counter(self.eg.id_counter) as u32 != self.next_id_at_start) } - fn ensure_state(&mut self, function: FunctionId, n_keys: usize) { + fn ensure_state(&mut self, function: FunctionId) { if self.states.contains_key(&function) { return; } self.state_order.push(function); - self.states.insert( - function, - FunctionMergeState { - current: self.eg.current_rows_by_key(function, n_keys), - ..FunctionMergeState::default() - }, - ); + self.states + .insert(function, FunctionMergeState::default()); } fn current_row( &mut self, function: FunctionId, - n_keys: usize, + _n_keys: usize, key: &[u32], ) -> Option { - self.ensure_state(function, n_keys); - self.states[&function].current.get(key).cloned() + self.ensure_state(function); + if let Some(staged) = self.states[&function].staged.get(key) { + return Some(staged.clone()); + } + self.eg.index_current_row(function, key) } - fn set_current(&mut self, function: FunctionId, n_keys: usize, key: Row, current: CurrentRow) { - self.ensure_state(function, n_keys); + /// Stage `current` for `key`. `seen` is the caller's own `current_row` + /// result for this key: on the key's first touch the overlay was empty + /// then, so it doubles as the pre-transaction row without re-querying the + /// index. + fn set_current( + &mut self, + function: FunctionId, + key: Row, + current: CurrentRow, + seen: Option, + ) { + self.ensure_state(function); let state = self .states .get_mut(&function) .expect("merge state was initialized"); if !state.original.contains_key(&key) { - state - .original - .insert(key.clone(), state.current.get(&key).cloned()); + state.original.insert(key.clone(), seen); state.touched.push(key.clone()); } - state.current.insert(key, current); + state.staged.insert(key, current); } fn apply_set(&mut self, function: FunctionId, row: &[u32]) -> Result<()> { @@ -735,13 +866,13 @@ impl<'a> MergeTransaction<'a> { let Some(old) = self.current_row(function, n_keys, &key) else { self.set_current( function, - n_keys, key, CurrentRow { values: incoming, location: RowLocation::Live, rows_for_key: 1, }, + None, ); return Ok(()); }; @@ -786,15 +917,16 @@ impl<'a> MergeTransaction<'a> { values.into_boxed_slice() }; + let location = old.location; self.set_current( function, - n_keys, key, CurrentRow { values: merged, - location: old.location, + location, rows_for_key: 1, }, + Some(old), ); Ok(()) } @@ -878,11 +1010,18 @@ impl<'a> MergeTransaction<'a> { }), MergeFn::Const(value) => Ok(value.rep()), MergeFn::Primitive(id, arguments) => { - let arguments = self - .eval_args(arguments, owner, old, new, self_col, environment)? - .into_iter() - .map(Value::new) - .collect::>(); + let args = self.eval_args(arguments, owner, old, new, self_col, environment)?; + // A custom merge lowered into the FD view's `:merge` may build + // terms, so the term encoder's `set-if-empty` / view-proof ops can + // be invoked here too. Service them against the transaction's own + // view state (the db external function for them only panics). + if let Some(op) = self.eg.set_if_empty_ops.get(id).cloned() { + return self.set_if_empty_in_merge(&op, &args); + } + if let Some(op) = self.eg.view_proof_ops.get(id).cloned() { + return self.view_proof_in_merge(&op, &args); + } + let arguments = args.into_iter().map(Value::new).collect::>(); self.eg .eval_prim_internal(*id, &arguments)? .map(|value| value.rep()) @@ -946,16 +1085,64 @@ impl<'a> MergeTransaction<'a> { let values = vec![value].into_boxed_slice(); self.set_current( function, - n_keys, key.into(), CurrentRow { values: values.clone(), location: RowLocation::Live, rows_for_key: 1, }, + None, ); Ok(Some(values)) } + + /// Service a `set-if-empty` op invoked from inside a merge, against the + /// transaction's staged view state: return the e-class of the current + /// `(view keys)` row, or stage `(keys, default_vals)` and return the default + /// e-class. Mirrors [`crate::interpret`]'s action-time handler, but reads and + /// writes the transaction so same-transaction inserts and rollback apply. + fn set_if_empty_in_merge(&mut self, op: &ViewOp, args: &[u32]) -> Result { + let view = self.view_op_table(op)?; + let n_keys = op.n_keys; + let key: Row = args[..n_keys].into(); + if let Some(current) = self.current_row(view, n_keys, &key) { + return Ok(current.values[0]); + } + let values: Row = args[n_keys..n_keys + op.out_arity].into(); + let eclass = values[0]; + self.set_current( + view, + key, + CurrentRow { + values, + location: RowLocation::Live, + rows_for_key: 1, + }, + None, + ); + Ok(eclass) + } + + /// Service a view-proof read invoked from inside a merge: the proof column + /// (output col 1) of the current `(view keys)` row, or the `fallback` arg. + fn view_proof_in_merge(&mut self, op: &ViewOp, args: &[u32]) -> Result { + let view = self.view_op_table(op)?; + let n_keys = op.n_keys; + let key: Row = args[..n_keys].into(); + let fallback = args[n_keys]; + Ok(match self.current_row(view, n_keys, &key) { + Some(current) => current.values[1], + None => fallback, + }) + } + + fn view_op_table(&self, op: &ViewOp) -> Result { + self.eg + .table_ids + .get(&op.view_name) + .copied() + .ok_or_else(|| anyhow!("view op table `{}` is not registered", op.view_name)) + } } fn row_with_values(key: &[u32], values: &[u32]) -> Row { @@ -984,29 +1171,6 @@ fn remove_keys_from_store( removed } -fn update_version_map( - versions: &mut HashMap>, - func: FunctionId, - row: &Row, - delta: isize, - version: Option, -) { - match delta.cmp(&0) { - std::cmp::Ordering::Greater => { - versions.entry(func).or_default().insert( - row.clone(), - version.expect("positive row event needs a version"), - ); - } - std::cmp::Ordering::Less => { - if let Some(rows) = versions.get_mut(&func) { - rows.remove(row); - } - } - std::cmp::Ordering::Equal => {} - } -} - #[cfg(test)] #[allow(clippy::items_after_test_module)] mod tests { @@ -1488,12 +1652,12 @@ mod tests { false, ); eg.insert_live_row(f, row(&[1, 10, 20])); - let next_id = eg.next_id; + let next_id = eg.db.read_counter(eg.id_counter); let error = eg.apply_sets(vec![(f, row(&[1, 30, 40]))]).unwrap_err(); assert!(error.to_string().contains("illegal merge attempted")); - assert_eq!(eg.next_id, next_id); + assert_eq!(eg.db.read_counter(eg.id_counter), next_id); assert!(eg.mirror[&fresh].is_empty()); assert_eq!(eg.mirror[&f], HashSet::from([row(&[1, 10, 20])])); } @@ -1655,7 +1819,7 @@ mod tests { None, true, ); - eg.subsumed.entry(f).or_default().insert(row(&[1, 10, 20])); + eg.insert_located_row(f, RowLocation::Subsumed, row(&[1, 10, 20])); assert!(eg.apply_sets(vec![(f, row(&[1, 30, 40]))]).unwrap()); assert!(eg.mirror[&f].is_empty()); @@ -1666,7 +1830,7 @@ mod tests { fn merge_set_preserves_subsumed_status() { let mut eg = EGraph::new(); let f = id_function(&mut eg, "f", MergeFn::New); - eg.subsumed.entry(f).or_default().insert(row(&[1, 10])); + eg.insert_located_row(f, RowLocation::Subsumed, row(&[1, 10])); assert!(eg.apply_sets(vec![(f, row(&[1, 11]))]).unwrap()); @@ -1679,15 +1843,13 @@ mod tests { fn lookup_or_create_finds_subsumed_rows() { let mut eg = EGraph::new(); let f = id_function(&mut eg, "f", MergeFn::New); - eg.next_id = 100; - eg.subsumed.entry(f).or_default().insert(row(&[42, 7])); + eg.db.set_counter(eg.id_counter, 100); + eg.insert_located_row(f, RowLocation::Subsumed, row(&[42, 7])); - let mut lookup_index = HashMap::new(); - let value = - interpret::lookup_or_create(&mut eg, f, &[Value::new(42)], &mut lookup_index).unwrap(); + let value = interpret::lookup_or_create(&mut eg, f, &[Value::new(42)]).unwrap(); assert_eq!(value[0], 7); - assert_eq!(eg.next_id, 100); + assert_eq!(eg.db.read_counter(eg.id_counter), 100); assert!(eg.mirror[&f].is_empty()); } @@ -1695,8 +1857,8 @@ mod tests { fn merge_set_collapses_live_subsumed_key_duplicate() { let mut eg = EGraph::new(); let f = id_function(&mut eg, "f", MergeFn::New); - eg.mirror.entry(f).or_default().insert(row(&[1, 10])); - eg.subsumed.entry(f).or_default().insert(row(&[1, 11])); + eg.insert_live_row(f, row(&[1, 10])); + eg.insert_located_row(f, RowLocation::Subsumed, row(&[1, 11])); assert!(eg.apply_sets(vec![(f, row(&[1, 12]))]).unwrap()); @@ -1946,7 +2108,7 @@ mod tests { eg.insert_live_row(uf, row(&[75, 40, 0])); run_rules(&mut eg, &[rule]).unwrap(); assert!(!eg.dd_fused.is_empty()); - assert!(!eg.dd_fused_fed_versions.is_empty()); + assert!(!eg.dd_fused_cursors.is_empty()); let panic = Backend::new_panic(&mut eg, "cloned panic".to_owned()); let mut cloned = Backend::clone_boxed(&eg); @@ -1956,16 +2118,18 @@ mod tests { .expect("DD clone must retain its concrete backend type"); assert!(cloned.dd_fused.is_empty()); - assert!(cloned.dd_fused_fed_versions.is_empty()); + assert!(cloned.dd_fused_cursors.is_empty()); + assert!(cloned.event_logs.is_empty()); assert_eq!(cloned.relations.len(), eg.relations.len()); assert_eq!(cloned.rules.len(), eg.rules.len()); assert_eq!(cloned.mirror, eg.mirror); assert_eq!(cloned.subsumed, eg.subsumed); - assert_eq!(cloned.next_id, eg.next_id); - assert_eq!(cloned.next_row_version, eg.next_row_version); - assert_eq!(cloned.live_versions, eg.live_versions); - assert_eq!(cloned.all_versions, eg.all_versions); - assert_eq!(cloned.subsumed_versions, eg.subsumed_versions); + assert_eq!(cloned.by_key, eg.by_key); + assert_eq!(cloned.id_counter, eg.id_counter); + assert_eq!( + cloned.db.read_counter(cloned.id_counter), + eg.db.read_counter(eg.id_counter) + ); let outcome = catch_unwind(AssertUnwindSafe(|| { cloned.eval_prim_internal(panic, &[Value::new(7)]) @@ -1980,7 +2144,7 @@ mod tests { assert!(cloned.mirror[&uf].contains(&row(&[90, 4, 0]))); assert!(!eg.mirror[&uf].contains(&row(&[90, 4, 0]))); assert!(!cloned.dd_fused.is_empty()); - assert!(!cloned.dd_fused_fed_versions.is_empty()); + assert!(!cloned.dd_fused_cursors.is_empty()); } #[test] @@ -2162,6 +2326,7 @@ impl Backend for EGraph { }); merge_level = merge_level.max(dependency.merge_level + 1); }); + self.table_ids.insert(config.name.clone(), id); self.relations.push(RelationInfo { name: config.name, arity, @@ -2257,13 +2422,15 @@ impl Backend for EGraph { return None; } let key = key.iter().map(|value| value.rep()).collect::>(); - let find = |rows: Option<&HashSet>| { - rows? - .iter() - .find(|row| row[..key.len()] == key[..]) - .map(|row| row[..info.arity].iter().copied().map(Value::new).collect()) - }; - find(self.mirror.get(&func)).or_else(|| find(self.subsumed.get(&func))) + let values = self.index_lookup_live_first(func, &key)?; + Some( + key.iter() + .chain(values.iter()) + .take(info.arity) + .copied() + .map(Value::new) + .collect(), + ) } fn lookup_id(&self, func: FunctionId, key: &[Value]) -> Option { @@ -2278,6 +2445,12 @@ impl Backend for EGraph { Some(self.db.add_counter()) } + fn eclass_id_counter(&self) -> Option { + // Same counter `fresh_id_internal` mints from, so the term encoder's + // `get-fresh!` ids and native `FreshId`-default ids share one id space. + Some(self.id_counter) + } + fn container_merge_fn(&self, _container_type: TypeId) -> Option { // The supported proof/term subset interns container values but does not // rely on merging distinct ids for one rebuilt value. Keep that subset's @@ -2300,6 +2473,34 @@ impl Backend for EGraph { Ok(id) } + fn fresh_eclass_id(&mut self) -> Value { + Value::new(self.fresh_id_internal()) + } + + fn add_values(&mut self, values: Vec<(FunctionId, Vec)>) { + // Route through the merge-aware path (like the reference backend's staged + // flush) so that same-key/different-value rows are collapsed by each + // function's `:merge` — FD-view congruence unions colliding e-classes, and + // a `:no-merge` conflict is rejected — rather than coexisting as raw mirror + // rows. + let sets: Vec<(FunctionId, Row)> = values + .into_iter() + .map(|(func, row)| (func, row.iter().map(|v| v.rep()).collect())) + .collect(); + if let Err(err) = self.apply_sets(sets) { + // `add_values` cannot return an error; stash it to surface on the next + // `run_rules` (the reference backend likewise reports a conflicting + // merge lazily rather than at load time). + let mut pending = self + .panic_message + .lock() + .expect("DD panic-message side channel must not be poisoned"); + if pending.is_none() { + *pending = Some(err.to_string()); + } + } + } + fn free_rule(&mut self, id: RuleId) { if let Some(slot) = self.rules.get_mut(id.rep() as usize) { *slot = None; @@ -2308,8 +2509,15 @@ impl Backend for EGraph { // Any fused ruleset that included this rule is now stale: drop it so // it is rebuilt (without the freed rule) on the next `run_rules`. self.dd_fused.retain(|key| !key.contains(&i)); - self.dd_fused_fed_versions - .retain(|key, _| !key.contains(&i)); + self.dd_fused_cursors.retain(|key, _| !key.contains(&i)); + // Drop event logs no remaining worker subscribes to (a later worker + // seeds from the full current state instead of replaying events). + let subscribed: HashSet = self + .dd_fused_cursors + .values() + .flat_map(|reads| reads.keys().copied()) + .collect(); + self.event_logs.retain(|read, _| subscribed.contains(read)); } } @@ -2385,6 +2593,48 @@ impl Backend for EGraph { ))) } + fn register_set_if_empty( + &mut self, + view_name: String, + n_keys: usize, + out_arity: usize, + ) -> ExternalFunctionId { + // The interpreter intercepts this id and services it against the mirror + // (see `interpret::apply_head`); the registered db function only fires if + // that interception is ever missed, so make it a loud error. + let id = Backend::new_panic( + self, + format!("set-if-empty for `{view_name}` reached the db path; DD must intercept it"), + ); + self.set_if_empty_ops.insert( + id, + ViewOp { + view_name, + n_keys, + out_arity, + }, + ); + id + } + + fn register_view_proof(&mut self, view_name: String, n_keys: usize) -> ExternalFunctionId { + let id = Backend::new_panic( + self, + format!("view-proof for `{view_name}` reached the db path; DD must intercept it"), + ); + self.view_proof_ops.insert( + id, + ViewOp { + view_name, + n_keys, + // A view-proof reader never inserts, so out_arity is unused; the + // view always has (eclass, proof) outputs. + out_arity: 2, + }, + ); + id + } + // -- capability flags --------------------------------------------------- fn requires_term_encoding(&self) -> bool { @@ -2418,24 +2668,26 @@ impl Backend for EGraph { fn clone_boxed(&self) -> Box { // Timely workers are transient and cannot be cloned. The authoritative - // relation/rule/database/mirror/version state is copied, while workers - // and their fed-version snapshots start empty. The next `run_rules` - // lazily rebuilds the fused dataflow and feeds the cloned current state. + // relation/rule/database/mirror/index state is copied, while workers, + // their cursors, and the event logs start empty. The next `run_rules` + // lazily rebuilds the fused dataflow and seeds it from the cloned + // current state. Box::new(Self { relations: self.relations.clone(), rules: self.rules.clone(), mirror: self.mirror.clone(), subsumed: self.subsumed.clone(), db: self.db.clone(), - next_id: self.next_id, + id_counter: self.id_counter, seen: self.seen.clone(), dd_fused: DdWorkers::default(), - next_row_version: self.next_row_version, - live_versions: self.live_versions.clone(), - all_versions: self.all_versions.clone(), - subsumed_versions: self.subsumed_versions.clone(), - dd_fused_fed_versions: HashMap::new(), + by_key: self.by_key.clone(), + event_logs: HashMap::new(), + dd_fused_cursors: HashMap::new(), panic_message: Arc::clone(&self.panic_message), + table_ids: self.table_ids.clone(), + set_if_empty_ops: self.set_if_empty_ops.clone(), + view_proof_ops: self.view_proof_ops.clone(), }) } diff --git a/egglog-experimental/dd/tests/files.rs b/egglog-experimental/dd/tests/files.rs index 412fbf1b..495f7979 100644 --- a/egglog-experimental/dd/tests/files.rs +++ b/egglog-experimental/dd/tests/files.rs @@ -66,16 +66,24 @@ const DEBUG_SUBSET: &[&str] = &[ /// - container rebuild read primitives (registered through egglog's /// `ActionRegistry`; DD has direct container storage but no registry /// execution state for read primitives over its term-encoded mirror): -/// `container-proofs`, `datatypes`, `nested-container-dirty-propagation`, -/// `repro-querybug3`. +/// `container-proofs`, `container-fail`, `datatypes`, +/// `nested-container-dirty-propagation`, `hardboiled_conv1d_32`. +/// - `input` from an external CSV whose path is relative to the corpus dir; the +/// DD harness runs from the `dd` crate root, so the file cannot be found: +/// `string_quotes`. const KNOWN_UNSUPPORTED: &[(&str, &str)] = &[ ("container-proofs.egg", "requires a backend action registry"), + ("container-fail.egg", "requires a backend action registry"), ("datatypes.egg", "requires a backend action registry"), ( "nested-container-dirty-propagation.egg", "requires a backend action registry", ), - ("repro-querybug3.egg", "requires a backend action registry"), + ( + "hardboiled_conv1d_32.egg", + "requires a backend action registry", + ), + ("string_quotes.egg", "string_quotes.csv"), ]; /// Run `program` on DD and render its outputs with the SAME normalization diff --git a/egglog-experimental/src/set_cost.rs b/egglog-experimental/src/set_cost.rs index c66f326f..5a1e591b 100644 --- a/egglog-experimental/src/set_cost.rs +++ b/egglog-experimental/src/set_cost.rs @@ -125,6 +125,7 @@ impl Macro> for SetCostDeclarations { term_constructor: None, unextractable: false, identity_vals: None, + cost: None, }); } } @@ -159,6 +160,7 @@ fn generate_cost_table_commands_from_variants(variants: &[Variant]) -> Vec>() diff --git a/egglog-experimental/tests/eggcc_2mm_proof.rs b/egglog-experimental/tests/eggcc_2mm_proof.rs index 36b63eaf..e4a3703c 100644 --- a/egglog-experimental/tests/eggcc_2mm_proof.rs +++ b/egglog-experimental/tests/eggcc_2mm_proof.rs @@ -45,8 +45,14 @@ fn eggcc_2mm_bounded_export_uses_container_helpers() { ); } + // `:no-merge` is unsupported by the term/proof encoding, so the bounded export + // rewrites its no-merge functions to `:merge old` to stay proof-supported. assert!( - non_comment_program.contains(":no-merge"), - "bounded eggcc export should preserve native no-merge declarations" + non_comment_program.contains(":merge old"), + "bounded eggcc export should use `:merge old` for its former no-merge functions" + ); + assert!( + !non_comment_program.contains(":no-merge"), + "`:no-merge` is unsupported by the encoding; the bounded export must not use it" ); } diff --git a/egglog-experimental/tests/fixtures/eggcc-2mm-pass1.egg b/egglog-experimental/tests/fixtures/eggcc-2mm-pass1.egg index 6e18a37f..41b7d1d6 100644 --- a/egglog-experimental/tests/fixtures/eggcc-2mm-pass1.egg +++ b/egglog-experimental/tests/fixtures/eggcc-2mm-pass1.egg @@ -2,7 +2,9 @@ ; egraphs-good/eggcc#796 (https://github.com/egraphs-good/eggcc/pull/796). ; The fixture was checked into egraphs-good/egglog-experimental#56 ; (https://github.com/egraphs-good/egglog-experimental/pull/56). Its original -; :no-merge declarations are preserved now that proof mode supports them. +; no-merge declarations are rewritten to `:merge old` here: the term/proof +; encoding does not support no-merge functions, and these length/succ functions +; are deterministic so keeping the old value on a (non-occurring) conflict is sound. ; Prologue (datatype Expr) @@ -93,7 +95,7 @@ (constructor TLConcat (TypeList TypeList) TypeList :unextractable) (rewrite (TLConcat (TNil) r) r :ruleset type-helpers) (rewrite (TLConcat (TCons hd tl) r) (TCons hd (TLConcat tl r)) :ruleset type-helpers) -(function TypeList-length (TypeList) i64 :no-merge) +(function TypeList-length (TypeList) i64 :merge old) (constructor TypeList-ith (TypeList i64) BaseType :unextractable) (rule () ((set (TypeList-length (TNil)) 0)) :ruleset type-helpers) (rule ((= lst (TCons hd tl)) (= len (TypeList-length tl))) ((set (TypeList-length lst) (+ 1 len))) :ruleset type-helpers) @@ -218,7 +220,7 @@ (rule ((TupleT tylist) (PureTypeList tylist)) ((PureType (TupleT tylist))) :ruleset type-analysis) (rule ((TNil)) ((PureTypeList (TNil))) :ruleset type-analysis) (rule ((TCons hd tl) (PureBaseType hd) (PureTypeList tl)) ((PureTypeList (TCons hd tl))) :ruleset type-analysis) -(function ListExpr-length (ListExpr) i64 :no-merge) +(function ListExpr-length (ListExpr) i64 :merge old) (constructor ListExpr-ith (ListExpr i64) Expr :unextractable) (constructor ListExpr-suffix (ListExpr i64) ListExpr :unextractable) (constructor Append (ListExpr Expr) ListExpr :unextractable) @@ -227,7 +229,7 @@ (rule ((= (ListExpr-suffix list n) (Nil))) ((set (ListExpr-length list) n)) :ruleset always-run) (rewrite (Append (Cons a b) e) (Cons a (Append b e)) :ruleset always-run) (rewrite (Append (Nil) e) (Cons e (Nil)) :ruleset always-run) -(function tuple-length (Expr) i64 :no-merge) +(function tuple-length (Expr) i64 :merge old) (rule ((HasType expr (TupleT tl)) (= len (TypeList-length tl))) ((set (tuple-length expr) len)) :ruleset always-run) (relation leading-Expr (Expr)) (relation leading-Expr-list (ListExpr)) @@ -649,7 +651,7 @@ (constructor AddIntInterval (IntInterval IntInterval) IntInterval) (rewrite (AddIntInterval (MkIntInterval lo1 hi1) (MkIntInterval lo2 hi2)) (MkIntInterval (AddIntOrInfinity lo1 lo2) (AddIntOrInfinity hi1 hi2)) :ruleset always-run) (datatype List (Nil-List) (Cons-List i64 IntInterval List)) -(function Length-List (List) i64 :no-merge) +(function Length-List (List) i64 :merge old) (rule ((= x (Nil-List))) ((set (Length-List x) 0)) :ruleset always-run) (rule ((= x (Cons-List hd0 hd1 tl)) (= l (Length-List tl))) ((set (Length-List x) (+ l 1))) :ruleset always-run) (rule ((= x (Nil-List))) ((set (Length-List x) 0)) :ruleset memory-helpers) @@ -715,7 +717,7 @@ (relation PointsNowhere-PtrPointees (PtrPointees)) (rule ((= f (PointsTo x)) (IsEmpty-List x)) ((PointsNowhere-PtrPointees f)) :ruleset always-run) (datatype List (Nil-List) (Cons-List PtrPointees List)) -(function Length-List (List) i64 :no-merge) +(function Length-List (List) i64 :merge old) (rule ((= x (Nil-List))) ((set (Length-List x) 0)) :ruleset always-run) (rule ((= x (Cons-List hd0 tl)) (= l (Length-List tl))) ((set (Length-List x) (+ l 1))) :ruleset always-run) (rule ((= x (Nil-List))) ((set (Length-List x) 0)) :ruleset memory-helpers) @@ -753,7 +755,7 @@ (rewrite (ExprSet-union (ES set1) (ES set2)) (ES (set-union set1 set2)) :ruleset memory-helpers) (constructor ExprSet-insert (ExprSet Expr) ExprSet) (rewrite (ExprSet-insert (ES set1) x) (ES (set-insert set1 x)) :ruleset memory-helpers) -(function ExprSet-length (ExprSet) i64 :no-merge) +(function ExprSet-length (ExprSet) i64 :merge old) (rule ((ES set1)) ((set (ExprSet-length (ES set1)) (set-length set1))) :ruleset memory-helpers) (datatype Pointees (TuplePointsTo List) (PtrPointsTo PtrPointees)) (constructor UnwrapPtrPointsTo (Pointees) PtrPointees) @@ -829,7 +831,7 @@ (rewrite (PointsToCells (Alloc id sz state ty) aps) (TuplePointsTo (Cons-List (PointsTo (Cons-List id (MkIntInterval (I 0) (I 0)) (Nil-List))) (Cons-List (PointsTo (Nil-List)) (Nil-List)))) :ruleset memory-helpers) (constructor PointsToCellsAtIter (Pointees Expr Expr i64) Pointees) (rule ((= e (DoWhile inputs pred-body)) (PointsToCells e aps)) ((union (PointsToCellsAtIter aps inputs pred-body 0) (PointsToCells inputs aps)) (union (PointsToCellsAtIter aps inputs pred-body 1) (UnionPointees (PointsToCellsAtIter aps inputs pred-body 0) (PointeesDropFirst (PointsToCells pred-body (PointsToCellsAtIter aps inputs pred-body 0)))))) :ruleset memory-helpers) -(function succ (i64) i64 :no-merge) +(function succ (i64) i64 :merge old) (rule ((PointsToCellsAtIter aps inputs pred-body i)) ((set (succ i) (+ i 1))) :ruleset memory-helpers) (rule ((= pointees0 (PointsToCellsAtIter aps inputs pred-body i)) (= pointees1 (PointsToCellsAtIter aps inputs pred-body (succ i))) (Resolved-Pointees pointees0) (Resolved-Pointees pointees1) (!= pointees0 pointees1)) ((union (PointsToCellsAtIter aps inputs pred-body (+ i 2)) (UnionPointees pointees1 (PointeesDropFirst (PointsToCells pred-body pointees1))))) :ruleset memory) (rule ((= pointees (PointsToCellsAtIter aps inputs pred-body i)) (= pointees (PointsToCellsAtIter aps inputs pred-body (succ i)))) ((union (PointsToCells (DoWhile inputs pred-body) aps) pointees)) :ruleset memory) diff --git a/egglog-experimental/tests/snapshots/files__proofs__with_ruleset_proof_testing.snap b/egglog-experimental/tests/snapshots/files__proofs__with_ruleset_proof_testing.snap index 11c7673d..33f22066 100644 --- a/egglog-experimental/tests/snapshots/files__proofs__with_ruleset_proof_testing.snap +++ b/egglog-experimental/tests/snapshots/files__proofs__with_ruleset_proof_testing.snap @@ -11,11 +11,8 @@ expression: snapshot (premises) (substitution)) (let t0 (Add (Num 2) (Num 3))) -(let t1 (Add (Num 3) (Num 2))) -(Sym - (= t0 t1) - (Rule - (= t1 t0) - (name "(rewrite (Add a b) (Add b a) :ruleset optimization)") - (premises (Fiat (= t0 t0))) - (substitution (a (Num 2)) (b (Num 3)) (@rewrite_var__ t0)))) +(Rule + (= t0 (Add (Num 3) (Num 2))) + (name "(rewrite (Add a b) (Add b a) :ruleset optimization)") + (premises (Fiat (= t0 t0))) + (substitution (a (Num 2)) (b (Num 3)) (@rewrite_var__ t0))) diff --git a/egglog/CHANGELOG.md b/egglog/CHANGELOG.md index d4b8f67e..861bbc20 100644 --- a/egglog/CHANGELOG.md +++ b/egglog/CHANGELOG.md @@ -2,11 +2,24 @@ ## [Unreleased] - ReleaseDate +- The term/proof encoding supports `:no-merge` functions with a primitive or `Unit` output (encoded as an FD view declared native `:no-merge` with an identity-column guard on the output), but no longer supports `:no-merge` functions with an eq-sort output (whose conflict check needs union-find leaders). An eq-sort `:no-merge` program is reported unsupported and must run on the native backend, or give the function a `:merge` (e.g. `:merge old`). This removes the rule/`current`-helper machinery the encoding previously used to emulate `:no-merge`. +- `(fail +)` now accepts multiple commands, running them in order and succeeding if any one fails (previously it wrapped a single command). Desugaring, global removal, and proof encoding keep the whole expansion of a wrapped command inside the `fail`, so `fail` now works over commands that expand to several — including `(fail (set …))` under the term/proof encoding. +- In the term/proof encoding, load `(input …)` for custom functions (with or without `:merge`, including `:no-merge` `Unit`-output ones) natively via `EGraph::native_input`, the same path already used for constructors and relations. This removes the per-input bodyless "loader rule" (and its fresh ruleset) that custom-function inputs used to compile to. +- Make proof extraction deterministic so proof-mode snapshot tests no longer flake on backends with nondeterministic row order (the differential-dataflow backend). +- Speed up freeing external functions, which was quadratic in the number of short-lived one-shot rules created (e.g. under the term/proof encoding). +- Speed up `core-relations`' `merge_all` by resetting only the tables that changed during the call instead of every table. +- Desugar global variables as functions instead of constructor + `union` in the term/proof encoding, and skip rebuilding after non-`union` top-level actions (removing a per-definition rebuild cost). - Fix user-defined primitives (registered through the Rust API after construction) being reported as unbound under term encoding / proofs: primitive registration now also reaches the term-encoding typechecker, so the encoder can typecheck the encoded program. Previously callers had to manually register the primitive on `proof_state.original_typechecking` as well. - **Pluggable backend SPI.** `EGraph::with_backend(Box)` lets a third party drive the egglog frontend with their own backend (see the `egglog-backend-trait` crate and the `egglog-experimental-dd` example). A backend without a native union-find declares `Backend::requires_term_encoding()`; `EGraph::with_term_encoding()` opts such an e-graph into the term-encoding pipeline (congruence and rebuild lower to rules over `@uf` tables), and running a term-encoding-only backend without it now errors with `Error::BackendRequiresTermEncoding` instead of silently dropping `union`s. +- Route the term/proof encoding's `get-fresh!` (id minting) and `set-if-empty` (view canonicalization) primitives through the backend SPI (`Backend::register_get_fresh` / `register_set_if_empty` / `register_view_proof`), so a backend can service them against its own storage instead of reaching into core-relations tables. The differential-dataflow backend implements them over its host-side mirror and now runs eq-sort programs under term/proof encoding. - Add `make nightly` and `scripts/nightly_bench.py`, a hyperfine-based benchmark harness that measures every `tests/**/*.egg` program at 1/2/4/8 threads and (where supported) in proof-testing mode, caps each run at a 2-minute timeout, skips sub-50ms programs, and emits an HTML dashboard (one row per benchmark, one column per configuration) for nightly.cs.washington.edu. The dashboard uses [eval-live](https://github.com/oflatt/eval-live) for interactive filtering and sorting. - Rework the term/proof encoding's union-find and congruence maintenance, substantially reducing proof-mode time and memory. +- In the term/proof encoding, run a custom function's `:merge` in its + functional-dependency view's own `:merge` (like constructor congruence) instead + of a separate rule plus a `current` helper table. This computes the merge once + rather than twice, so encoded runs no longer mint over-merged extra term rows, + and the merged value is justified by a proper merge-function proof. - **Tuple-output functions.** A function may declare more than one output sort, e.g. `(function interval (Math) (i64 i64) :merge (values (max old0 new0) (min old1 new1)))`. Such a function stores its outputs as separate value columns; the functional dependency is diff --git a/egglog/core-relations/src/free_join/mod.rs b/egglog/core-relations/src/free_join/mod.rs index 6e42452e..db82666e 100644 --- a/egglog/core-relations/src/free_join/mod.rs +++ b/egglog/core-relations/src/free_join/mod.rs @@ -281,6 +281,9 @@ impl Counters { // NB: we may want to experiment with Ordering::Relaxed here. self.0[ctr].fetch_add(1, Ordering::Release) } + pub(crate) fn set(&self, ctr: CounterId, value: usize) { + self.0[ctr].store(value, Ordering::Release); + } } /// A collection of tables and indexes over them. @@ -510,6 +513,13 @@ impl Database { self.counters.read(counter) } + /// Overwrite the given counter's value. Used (by the differential-dataflow + /// backend) to roll a counter back to a previously read value, e.g. reclaiming + /// ids staged by an aborted action. + pub fn set_counter(&self, counter: CounterId, value: usize) { + self.counters.set(counter, value) + } + /// A helper for merging all pending updates. Used to write to the database after updates have /// been staged. Returns true if any tuples were added. /// @@ -520,9 +530,18 @@ impl Database { let mut ever_changed = false; let do_parallel = parallelize_db_level_op(self.total_size_estimate); let mut to_merge = IndexSet::default(); + // Tables modified during this call (accumulated from the notification list + // here and inside `merge_simple`). Only these need their cached indexes + // reset at the end: an unmodified table's index is still valid — its + // version is unchanged, so `Index::refresh` would be a no-op. Resetting + // *every* table instead is O(all tables) per call, which is quadratic when + // many small tables each trigger a merge (e.g. a long run of global-`let` + // definitions in the term/proof encoding, one view table apiece). + let mut touched: IndexSet = IndexSet::default(); loop { to_merge.clear(); let to_merge_vec = self.notification_list.reset(); + touched.extend(to_merge_vec.iter().copied()); // `merge_simple` is faster but ignores read/write-dependency ordering, so it is only // sound when no dirty table's merge reads another table (e.g. a `Construct`/`Function` // lookup inside a `:merge`). With a read dependency, an unordered merge could observe a @@ -533,7 +552,7 @@ impl Database { .iter() .any(|table| self.deps.has_read_deps(*table)) { - ever_changed |= self.merge_simple(to_merge_vec); + ever_changed |= self.merge_simple(to_merge_vec, &mut touched); break; } for table in to_merge_vec { @@ -565,7 +584,13 @@ impl Database { // Then initialize read dependencies (this two-phase structure is why we have an // Option in the tables_merging map). for table in stratum.intersection(&to_merge).copied() { - tables_merging[table].0 = Some(self.tables.unwrap_val(table)); + let val = self.tables.unwrap_val(table); + // Maintain `total_size_estimate` incrementally (subtract the + // pre-merge length now, add the post-merge length on drain + // below), so the reset loop no longer re-sums every table. + self.total_size_estimate = + self.total_size_estimate.wrapping_sub(val.table.len()); + tables_merging[table].0 = Some(val); } let db = self.read_only_view(); changed |= if do_parallel { @@ -588,34 +613,47 @@ impl Database { .unwrap_or(false) }; for (id, (table, _)) in tables_merging.drain() { - self.tables.insert(id, table.unwrap()); + let val = table.unwrap(); + self.total_size_estimate = + self.total_size_estimate.wrapping_add(val.table.len()); + self.tables.insert(id, val); } } ever_changed |= changed; } - // Reset all indexes to force an update on the next access. - let mut size_estimate = 0; - for (_, info) in self.tables.iter_mut() { - info.column_indexes.update(|_, ti| { - Arc::get_mut(ti).unwrap().reset(); - }); - info.indexes.update(|_, ti| { - Arc::get_mut(ti).unwrap().reset(); - }); - size_estimate += info.table.len(); + // Reset the cached indexes of the tables modified during this call so they + // refresh on next access. Unmodified tables keep their still-valid cached + // indexes. `total_size_estimate` was maintained incrementally at each merge + // (above and in `merge_simple`), so we no longer re-sum every table here. + for table in touched.iter().copied() { + if let Some(info) = self.tables.get_mut(table) { + info.column_indexes.update(|_, ti| { + Arc::get_mut(ti).unwrap().reset(); + }); + info.indexes.update(|_, ti| { + Arc::get_mut(ti).unwrap().reset(); + }); + } } - self.total_size_estimate = size_estimate; ever_changed } /// A "fast path" merge method that is not optimized for parallelism and does not respect read /// and write dependencies. This ends up being faster than the full "strata-aware" option in /// the body of `merge_all`. - fn merge_simple(&mut self, mut to_merge: SmallVec<[TableId; 4]>) -> bool { + fn merge_simple( + &mut self, + mut to_merge: SmallVec<[TableId; 4]>, + touched: &mut IndexSet, + ) -> bool { let mut changed = false; while !to_merge.is_empty() { for table_id in to_merge.iter().copied() { let mut info = self.tables.unwrap_val(table_id); + // Maintain `total_size_estimate` incrementally (see `merge_all`'s + // reset loop, which no longer re-sums every table). + self.total_size_estimate = + self.total_size_estimate.wrapping_sub(info.table.len()); // Pre-seed the table's OWN buffer so a self-referential merge — one that stages a // write back into its own table (e.g. the term encoder's `@UF` recursive // parent-union) — can stage it. The table has been `unwrap_val`'d out of @@ -627,9 +665,12 @@ impl Database { bufs.insert(table_id, info.table.new_buffer()); let mut es = ExecutionState::new(self.read_only_view(), bufs); changed |= info.table.merge(&mut es).added || es.changed; + self.total_size_estimate = + self.total_size_estimate.wrapping_add(info.table.len()); self.tables.insert(table_id, info); } to_merge = self.notification_list.reset(); + touched.extend(to_merge.iter().copied()); } changed } diff --git a/egglog/egglog-backend-trait/Cargo.toml b/egglog/egglog-backend-trait/Cargo.toml index 36158c90..897ef744 100644 --- a/egglog/egglog-backend-trait/Cargo.toml +++ b/egglog/egglog-backend-trait/Cargo.toml @@ -12,4 +12,5 @@ anyhow = { workspace = true } egglog-bridge = { workspace = true } egglog-ast = { workspace = true } egglog-core-relations = { workspace = true } +egglog-numeric-id = { workspace = true } egglog-reports = { workspace = true } diff --git a/egglog/egglog-backend-trait/src/backend_impl.rs b/egglog/egglog-backend-trait/src/backend_impl.rs index 569a4ad3..0492a6d8 100644 --- a/egglog/egglog-backend-trait/src/backend_impl.rs +++ b/egglog/egglog-backend-trait/src/backend_impl.rs @@ -195,6 +195,14 @@ impl Backend for EGraph { build_rule(self, rule) } + fn fresh_eclass_id(&mut self) -> Value { + EGraph::fresh_id(self) + } + + fn add_values(&mut self, values: Vec<(FunctionId, Vec)>) { + EGraph::add_values(self, values); + } + fn free_rule(&mut self, id: RuleId) { EGraph::free_rule(self, id); } @@ -222,6 +230,19 @@ impl Backend for EGraph { EGraph::new_panic(self, message) } + fn register_set_if_empty( + &mut self, + view_name: String, + n_keys: usize, + out_arity: usize, + ) -> ExternalFunctionId { + EGraph::register_set_if_empty(self, view_name, n_keys, out_arity) + } + + fn register_view_proof(&mut self, view_name: String, n_keys: usize) -> ExternalFunctionId { + EGraph::register_view_proof(self, view_name, n_keys) + } + fn set_report_level(&mut self, level: ReportLevel) { EGraph::set_report_level(self, level); } @@ -238,6 +259,10 @@ impl Backend for EGraph { Some(EGraph::action_registry(self)) } + fn eclass_id_counter(&self) -> Option { + Some(EGraph::id_counter(self)) + } + fn supports_containers(&self) -> bool { true } diff --git a/egglog/egglog-backend-trait/src/lib.rs b/egglog/egglog-backend-trait/src/lib.rs index ec6b5fe1..c88e502a 100644 --- a/egglog/egglog-backend-trait/src/lib.rs +++ b/egglog/egglog-backend-trait/src/lib.rs @@ -87,6 +87,7 @@ use std::any::{Any, TypeId}; use std::sync::{Arc, RwLock}; use anyhow::Result; +use egglog_numeric_id::NumericId; mod backend_impl; @@ -204,6 +205,14 @@ pub type ContainerMergeFn = /// /// See the crate docs for which methods are required vs. optional and how the /// ergonomic sugar on `dyn Backend` relates to the `_dyn` methods here. +/// +/// Under the term/proof encoding the frontend lowers e-graph constructors to +/// relation tables plus functional-dependency view functions, and `union` to a +/// `:merge` on a union-find function. A backend therefore needs no dedicated +/// `union` or `constructor` operation — generic tables, rules, and `:merge` +/// suffice. This holds when evaluating actions too: a `union` action becomes a +/// `:merge` write and a constructor application becomes a table insert, so the +/// backend never sees union/constructor as primitive operations. pub trait Backend: Send + Sync { // -- table lifecycle ---------------------------------------------------- @@ -268,6 +277,13 @@ pub trait Backend: Send + Sync { None } + /// The counter that mints fresh eq-class ids, for a backend whose ids come from a counter + /// (the reference bridge). Backends with deterministic / structural ids return `None`. + /// Exposed so the term/proof encoding's `get-fresh!` primitive can mint fresh ids. + fn eclass_id_counter(&self) -> Option { + None + } + /// Select the merge policy for a registered container type. Backends that /// advertise container support outside the reference bridge must provide /// this alongside a container registry and id counter. @@ -275,6 +291,26 @@ pub trait Backend: Send + Sync { None } + // -- native fact loading (`(input …)`) ---------------------------------- + // + // The frontend loads `(input …)` facts by minting ids and inserting the + // encoded term/view (and, in proof mode, AST/proof) rows directly, rather + // than compiling and running a loader rule. Each backend services these + // against its own storage (db buffers for the reference bridge, the + // host-side mirror for Differential Dataflow), so input loading never falls + // back to rule compilation. + + /// Mint a fresh eq-class id from the backend's shared counter (used for term, + /// AST, and proof ids). Panics on a backend without a counter. + fn fresh_eclass_id(&mut self) -> Value; + + /// Insert a batch of logical rows and flush. Each `(func, row)` gives a + /// function id and its row as keys followed by all value columns (no + /// timestamp/subsumption — the backend fills those in). Duplicate view keys + /// are resolved by the view's `:merge` on flush, so callers plain-insert + /// rather than get-or-insert. + fn add_values(&mut self, values: Vec<(FunctionId, Vec)>); + // -- execution state (object-safe; see `with_execution_state` sugar) ----- /// Run `f` against a fresh execution state and return whether it staged any @@ -321,6 +357,59 @@ pub trait Backend: Send + Sync { /// [`Backend::run_rules`] return the provided message as an error. fn new_panic(&mut self, message: String) -> ExternalFunctionId; + // -- term-encoding mint/canonicalize ops -------------------------------- + // + // The term encoder represents terms as relation rows minted with fresh ids + // and canonicalized against per-constructor "FD view" tables. These three + // ops let each backend service that minting/canonicalization against its + // own storage (db tables for the reference bridge; a host-side mirror for a + // relational backend), so the encoding does not reach into one backend's + // internals directly. + + /// Register the `get-fresh!` mint op for one eq-sort. Returns the + /// [`ExternalFunctionId`] its mint sites (`(@get-fresh-!)`) resolve + /// to. The default mints an impure `() -> id` value from the backend's + /// [`Backend::eclass_id_counter`], so it works for any counter-based + /// backend. Called only when [`Backend::eclass_id_counter`] is `Some`. + fn register_get_fresh(&mut self) -> ExternalFunctionId { + let counter = self + .eclass_id_counter() + .expect("register_get_fresh requires an eq-class id counter"); + self.register_external_func(Box::new(egglog_core_relations::make_external_func( + move |state: &mut ExecutionState, _args: &[Value]| { + Some(Value::from_usize(state.inc_counter(counter))) + }, + ))) + } + + /// Register the `set-if-empty` canonicalize op for the FD view table named + /// `view_name` (`n_keys` key columns, `out_arity` output columns + /// `(eclass, …)`). Returns the [`ExternalFunctionId`] its call sites resolve + /// to. Semantics at invoke: look up `(view keys)`; if a row exists return its + /// first output (the eclass); otherwise insert `(keys, default_vals)` — the + /// trailing `out_arity` args — and return `default_vals[0]`. The default + /// registers a panic, so a backend that cannot service it fails with a clear + /// message rather than silently. + fn register_set_if_empty( + &mut self, + view_name: String, + _n_keys: usize, + _out_arity: usize, + ) -> ExternalFunctionId { + self.new_panic(format!( + "this backend does not support set-if-empty for view `{view_name}`" + )) + } + + /// Register the view-proof reader for the FD view named `view_name` + /// (`n_keys` key columns): `(keys, fallback) -> proof`, returning output + /// column 1 for `keys` or `fallback` when the key is absent. Default panics. + fn register_view_proof(&mut self, view_name: String, _n_keys: usize) -> ExternalFunctionId { + self.new_panic(format!( + "this backend does not support view-proof reads for view `{view_name}`" + )) + } + // -- diagnostics -------------------------------------------------------- /// Set the verbosity of the per-iteration timing report. diff --git a/egglog/egglog-bridge/src/lib.rs b/egglog/egglog-bridge/src/lib.rs index 3ab715a2..cfe49493 100644 --- a/egglog/egglog-bridge/src/lib.rs +++ b/egglog/egglog-bridge/src/lib.rs @@ -20,13 +20,14 @@ use crate::core_relations::{ BaseValue, BaseValueId, BaseValues, ColumnId, Constraint, ContainerValue, ContainerValues, CounterId, Database, DisplacedTable, ExecutionState, ExternalFunction, ExternalFunctionId, MergeVal, Offset, PlanStrategy, SortedWritesTable, TableId, TaggedRowBuffer, Value, - WrappedTable, + WrappedTable, make_external_func, }; use crate::numeric_id::{DenseIdMap, DenseIdMapWithReuse, NumericId, define_id}; use egglog_core_relations as core_relations; use egglog_numeric_id as numeric_id; use egglog_reports::{IterationReport, ReportLevel, RuleSetReport}; use hashbrown::HashMap; +use std::collections::BTreeMap; use indexmap::IndexSet; use log::info; use once_cell::sync::Lazy; @@ -126,6 +127,18 @@ pub struct EGraph { /// also serve as a debugging tool in the case that the number of panic messages grows without /// bound. panic_funcs: HashMap, + /// Reverse index `id -> message` for [`EGraph::panic_funcs`]. Lets + /// [`EGraph::free_external_func`] find a cached panic's entry — or determine + /// that a func is not a cached panic at all — in O(1), instead of scanning + /// the whole (id-unindexed) `panic_funcs` map on every free. That scan made + /// freeing one-shot action rules O(number of cached panics), which grows with + /// the program, so a long run of one-shot actions was quadratic. + // `BTreeMap` (not `HashMap`) on purpose: it introduces no new randomly-seeded + // hasher, so the seed sequence of the other hash tables is unchanged. Some + // backends' row iteration order (which order-dependent proof extraction reads) + // depends on that sequence, so a `HashMap` here would needlessly shift it. + // Lookups are by a small integer id, so a `BTreeMap` is more than fast enough. + panic_func_ids: BTreeMap, report_level: ReportLevel, /// Live registry of name-indexed action handles. Shared (via /// `Arc>`) with state wrappers and primitive callbacks @@ -163,11 +176,13 @@ impl Default for EGraph { // same message reuses the id. let panic_message: SideChannel = Default::default(); let mut panic_funcs: HashMap = Default::default(); + let mut panic_func_ids: BTreeMap = Default::default(); let default_panic_msg = "primitive panicked".to_string(); let default_panic_id = db.add_external_function(Box::new(Panic( default_panic_msg.clone(), panic_message.clone(), ))); + panic_func_ids.insert(default_panic_id, default_panic_msg.clone()); panic_funcs.insert( default_panic_msg, CachedPanic { @@ -194,6 +209,7 @@ impl Default for EGraph { funcs: Default::default(), panic_message, panic_funcs, + panic_func_ids, report_level: Default::default(), action_registry, } @@ -312,19 +328,70 @@ impl EGraph { self.db.add_external_function(func) } + /// Register the term encoder's `set-if-empty` canonicalize op for the FD + /// view table named `view_name` (`n_keys` key columns), returning the + /// [`ExternalFunctionId`] its mint sites resolve to. At invoke: look up + /// `(view keys)`; if a row exists return its first output (the eclass); + /// otherwise insert `(keys, trailing-default-columns)` and return the first + /// default column. Serviced over this backend's db view table. + pub fn register_set_if_empty( + &mut self, + view_name: String, + n_keys: usize, + _out_arity: usize, + ) -> ExternalFunctionId { + let registry = self.action_registry.clone(); + self.register_external_func(Box::new(make_external_func( + move |state: &mut ExecutionState, args: &[Value]| { + let registry = registry.read().unwrap(); + let action = registry.lookup_table(&view_name)?.clone(); + let keys = &args[..n_keys]; + if let Some(vals) = action.lookup_values(state, keys) { + // Already canonicalized: reuse the committed eclass and skip + // the insert, so the fresh id never enters the table. + return Some(vals[0]); + } + action.insert(state, args.iter().copied()); + Some(args[n_keys]) + }, + ))) + } + + /// Register the term encoder's view-proof reader for the FD view named + /// `view_name` (`n_keys` key columns): `(keys, fallback) -> proof`, returning + /// output column 1 for `keys` or `fallback` when the key is absent. + pub fn register_view_proof(&mut self, view_name: String, n_keys: usize) -> ExternalFunctionId { + let registry = self.action_registry.clone(); + self.register_external_func(Box::new(make_external_func( + move |state: &mut ExecutionState, args: &[Value]| { + let registry = registry.read().unwrap(); + let action = registry.lookup_table(&view_name)?.clone(); + let fallback = args[n_keys]; + Some(match action.lookup_values(state, &args[..n_keys]) { + Some(vals) => vals[1], + None => fallback, + }) + }, + ))) + } + pub fn free_external_func(&mut self, func: ExternalFunctionId) { + // A cached panic with more than one reference is kept alive (just + // decrement); one at its last reference — or any func that is not a + // cached panic — is freed from the database. The reverse index makes the + // "is this a cached panic, and which entry?" question O(1); previously we + // scanned all of `panic_funcs` on every call (see `panic_func_ids`). let mut free = true; - self.panic_funcs.retain(|_, cached| { - if cached.id != func { - true - } else if cached.references > 1 { - cached.references -= 1; - free = false; - true - } else { - false + if let Some(message) = self.panic_func_ids.get(&func).cloned() + && let Some(cached) = self.panic_funcs.get_mut(&message) { + if cached.references > 1 { + cached.references -= 1; + free = false; + } else { + self.panic_funcs.remove(&message); + self.panic_func_ids.remove(&func); + } } - }); if free { self.db.free_external_function(func); } @@ -335,6 +402,13 @@ impl EGraph { Value::from_usize(self.db.inc_counter(self.id_counter)) } + /// The global counter that mints fresh eq-class ids (backing [`fresh_id`](Self::fresh_id) + /// and `DefaultVal::FreshId`). Exposed so a primitive can mint ids in the same space during + /// rule execution — used by the term/proof encoding's `get-fresh!` primitive. + pub fn id_counter(&self) -> CounterId { + self.id_counter + } + /// Look up the canonical value for `val` in the union-find. /// /// If the value has never been inserted into the union-find, `val` is returned. @@ -2078,6 +2152,7 @@ impl EGraph { } let panic = Panic(message.clone(), self.panic_message.clone()); let id = self.db.add_external_function(Box::new(panic)); + self.panic_func_ids.insert(id, message.clone()); self.panic_funcs .insert(message, CachedPanic { id, references: 1 }); id diff --git a/egglog/src/ast/check_shadowing.rs b/egglog/src/ast/check_shadowing.rs index 7e808dff..66175896 100644 --- a/egglog/src/ast/check_shadowing.rs +++ b/egglog/src/ast/check_shadowing.rs @@ -70,9 +70,12 @@ impl Names { let mut inner = self.clone(); inner.check_shadowing_query(query) } - ResolvedNCommand::Fail(_span, command) => { + ResolvedNCommand::Fail(_span, commands) => { let mut inner = self.clone(); - inner.check_shadowing(command) + for command in commands { + inner.check_shadowing(command)?; + } + Ok(()) } ResolvedNCommand::Extract(..) => Ok(()), ResolvedNCommand::RunSchedule(..) => Ok(()), diff --git a/egglog/src/ast/desugar.rs b/egglog/src/ast/desugar.rs index ce3ad7fc..70745325 100644 --- a/egglog/src/ast/desugar.rs +++ b/egglog/src/ast/desugar.rs @@ -21,12 +21,14 @@ pub(crate) fn desugar_command( term_constructor, unextractable, identity_vals, + cost, } => { let mut fdecl = FunctionDecl::function(span, name, schema, merge); fdecl.internal_hidden = hidden; fdecl.internal_let = let_binding; fdecl.term_constructor = term_constructor; fdecl.identity_vals = identity_vals; + fdecl.cost = cost; // Functions with term_constructor are view tables that should be // extractable unless explicitly marked unextractable if fdecl.term_constructor.is_some() { @@ -195,12 +197,14 @@ pub(crate) fn desugar_command( Command::Pop(span, num) => { vec![NCommand::Pop(span, num)] } - Command::Fail(span, cmd) => { - let mut desugared = desugar_command(*cmd, parser, proof_testing)?; - - let last = desugared.pop().unwrap(); - desugared.push(NCommand::Fail(span, Box::new(last))); - return Ok(desugared); + Command::Fail(span, cmds) => { + // Desugar every wrapped command and wrap the whole flattened result in + // one `fail`, so the assertion covers all of them (not just the last). + let mut desugared = vec![]; + for cmd in cmds { + desugared.extend(desugar_command(cmd, parser, proof_testing)?); + } + return Ok(vec![NCommand::Fail(span, desugared)]); } Command::Input { span, name, file } => { vec![NCommand::Input { span, name, file }] diff --git a/egglog/src/ast/mod.rs b/egglog/src/ast/mod.rs index 77fdfe6a..ff0f5490 100644 --- a/egglog/src/ast/mod.rs +++ b/egglog/src/ast/mod.rs @@ -2,7 +2,6 @@ pub mod check_shadowing; pub mod desugar; mod expr; mod parse; -pub mod proof_global_remover; pub mod remove_globals; use std::cmp::max; @@ -51,6 +50,11 @@ pub struct ProofConstructorNames { pub trans: String, pub sym: String, pub normalize: String, + /// The `Fiat` justification constructor. Recorded here so that + /// [`EGraph::native_input`] can recover it (to mint base-fact proofs for + /// `(input …)` rows) when replaying an encoded program in a fresh e-graph, + /// where the encoding-time `proof_names` maps are unavailable. + pub fiat: String, } #[derive(Clone, Debug)] @@ -89,10 +93,12 @@ where span: Span, name: String, presort_and_args: Option<(String, Vec>)>, - /// The union-find `(constructor, optional function-index)` table names - /// for this sort: `UF_` (scanned by extraction's `find_canonical`) - /// and the optional `UF_f` index (single-key leader lookup). - uf: Option<(String, Option)>, + /// The union-find table names for this sort: + /// `(constructor, optional function-index, optional aux)` — `UF_` + /// (scanned by extraction's `find_canonical`), the optional `UF_f` + /// index (single-key leader lookup), and the optional proof-mode + /// `UF_Aux_` (`:internal-uf-aux`, read by container rebuild). + uf: Option<(String, Option, Option)>, /// The name of the proof function for this sort. /// Set by proof desugaring to record where proofs are stored for this sort. proof_func: Option, @@ -135,7 +141,11 @@ where }, Push(usize), Pop(Span, usize), - Fail(Span, Box>), + /// Assert that at least one of the wrapped commands fails. The commands run + /// in order; the first error is swallowed (the `fail` succeeds), and if none + /// error the `fail` itself errors. A `Vec` because desugaring / proof encoding + /// can expand one source command into several. + Fail(Span, Vec>), Input { span: Span, name: String, @@ -191,6 +201,7 @@ where term_constructor: f.term_constructor.clone(), unextractable: f.unextractable, identity_vals: f.identity_vals, + cost: f.cost, }, }, GenericNCommand::AddRuleset(span, name) => { @@ -227,9 +238,10 @@ where }, GenericNCommand::Push(n) => GenericCommand::Push(*n), GenericNCommand::Pop(span, n) => GenericCommand::Pop(span.clone(), *n), - GenericNCommand::Fail(span, cmd) => { - GenericCommand::Fail(span.clone(), Box::new(cmd.to_command())) - } + GenericNCommand::Fail(span, cmds) => GenericCommand::Fail( + span.clone(), + cmds.iter().map(|cmd| cmd.to_command()).collect(), + ), GenericNCommand::Input { span, name, file } => GenericCommand::Input { span: span.clone(), name: name.clone(), @@ -255,9 +267,12 @@ where GenericNCommand::RunSchedule(schedule) => { GenericNCommand::RunSchedule(schedule.visit_queries(f)) } - GenericNCommand::Fail(span, cmd) => { - GenericNCommand::Fail(span, Box::new(cmd.visit_queries(f))) - } + GenericNCommand::Fail(span, cmds) => GenericNCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.visit_queries(&mut *f)) + .collect(), + ), GenericNCommand::Sort { .. } | GenericNCommand::Function(..) | GenericNCommand::AddRuleset(..) @@ -339,9 +354,12 @@ where }, GenericNCommand::Push(n) => GenericNCommand::Push(n), GenericNCommand::Pop(span, n) => GenericNCommand::Pop(span, n), - GenericNCommand::Fail(span, cmd) => { - GenericNCommand::Fail(span, Box::new(cmd.visit_exprs(f))) - } + GenericNCommand::Fail(span, cmds) => GenericNCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.visit_exprs(&mut *f)) + .collect(), + ), GenericNCommand::Input { span, name, file } => { GenericNCommand::Input { span, name, file } } @@ -602,9 +620,9 @@ where span: Span, name: String, presort_and_args: Option<(String, Vec)>, - /// The union-find `(constructor, optional function-index)` table names - /// for this sort (see [`GenericNCommand::Sort`]). - uf: Option<(String, Option)>, + /// The union-find `(constructor, optional function-index, optional aux)` + /// table names for this sort (see [`GenericNCommand::Sort`]). + uf: Option<(String, Option, Option)>, /// The name of the proof function for this sort. /// Set by proof desugaring to record where proofs are stored for this sort. proof_func: Option, @@ -772,6 +790,10 @@ where /// leaves them unchanged is skipped and the existing row kept. Only /// valid for merges that are idempotent on equal inputs. identity_vals: Option, + /// Extraction head cost, from `:internal-cost`. Used by view tables (whose + /// term table is a relation that can't carry a cost) to record the user + /// operation's cost for the extractor. + cost: Option, }, /// Using the `ruleset` command, defines a new @@ -987,8 +1009,8 @@ where /// `pop` the current egraph, restoring the previous one. /// The argument specifies how many egraphs to pop. Pop(Span, usize), - /// Assert that a command fails with an error. - Fail(Span, Box>), + /// Assert that at least one of the wrapped commands fails with an error. + Fail(Span, Vec>), /// Include another egglog file directly as text and run it. Include(Span, String), /// User-defined command. @@ -1028,11 +1050,14 @@ where .. } => { write!(f, "(sort {name}")?; - if let Some((uf_ctor, uf_index)) = uf { + if let Some((uf_ctor, uf_index, uf_aux)) = uf { write!(f, " :internal-uf {uf_ctor}")?; if let Some(uf_index) = uf_index { write!(f, " {uf_index}")?; } + if let Some(uf_aux) = uf_aux { + write!(f, " :internal-uf-aux {uf_aux}")?; + } } if let Some(pf) = proof_func { write!(f, " :internal-proof-func {pf}")?; @@ -1040,8 +1065,8 @@ where if let Some(pc) = proof_constructors { write!( f, - " :internal-proof-names {} {} {} {}", - pc.congr, pc.trans, pc.sym, pc.normalize + " :internal-proof-names {} {} {} {} {}", + pc.congr, pc.trans, pc.sym, pc.normalize, pc.fiat )?; } write!(f, ")") @@ -1072,6 +1097,7 @@ where term_constructor, unextractable, identity_vals, + cost, } => { write!(f, "(function {name} {schema}")?; if let Some(merge) = &merge { @@ -1094,6 +1120,9 @@ where if let Some(k) = identity_vals { write!(f, " :internal-identity-vals {k}")?; } + if let Some(c) = cost { + write!(f, " :internal-cost {c}")?; + } write!(f, ")") } GenericCommand::Constructor { @@ -1191,7 +1220,7 @@ where file, exprs, } => write!(f, "(output {file:?} {})", ListDisplay(exprs, " ")), - GenericCommand::Fail(_span, cmd) => write!(f, "(fail {cmd})"), + GenericCommand::Fail(_span, cmds) => write!(f, "(fail {})", ListDisplay(cmds, " ")), GenericCommand::Include(_span, file) => write!(f, "(include {file:?})"), GenericCommand::Datatypes { span: _, datatypes } => { let datatypes: Vec<_> = datatypes @@ -1831,7 +1860,9 @@ where span, name: fun(name), presort_and_args, - uf: uf.map(|(ctor, index)| (fun(ctor), index.map(&mut *fun))), + uf: uf.map(|(ctor, index, aux)| { + (fun(ctor), index.map(&mut *fun), aux.map(&mut *fun)) + }), proof_func: proof_func.map(&mut *fun), container_rebuild, proof_constructors, @@ -1925,6 +1956,7 @@ where term_constructor, unextractable, identity_vals, + cost, } => GenericCommand::Function { span, name: fun(name), @@ -1938,6 +1970,7 @@ where term_constructor: term_constructor.map(&mut *fun), unextractable, identity_vals, + cost, }, GenericCommand::AddRuleset(span, name) => GenericCommand::AddRuleset(span, fun(name)), GenericCommand::UnstableCombinedRuleset(span, name, others) => { @@ -1995,9 +2028,12 @@ where } GenericCommand::Push(n) => GenericCommand::Push(n), GenericCommand::Pop(span, n) => GenericCommand::Pop(span, n), - GenericCommand::Fail(span, cmd) => { - GenericCommand::Fail(span, Box::new(cmd.map_string_symbols(fun))) - } + GenericCommand::Fail(span, cmds) => GenericCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.map_string_symbols(&mut *fun)) + .collect(), + ), GenericCommand::Include(span, file) => GenericCommand::Include(span, file), GenericCommand::UserDefined(span, name, exprs) => { GenericCommand::UserDefined(span, name, exprs) @@ -2021,6 +2057,7 @@ where term_constructor, unextractable, identity_vals, + cost, } => GenericCommand::Function { span, name, @@ -2031,6 +2068,7 @@ where term_constructor, unextractable, identity_vals, + cost, }, GenericCommand::Rule { rule } => GenericCommand::Rule { rule: rule.visit_exprs(f), @@ -2084,9 +2122,12 @@ where GenericCommand::RunSchedule(schedule) => { GenericCommand::RunSchedule(schedule.visit_exprs(f)) } - GenericCommand::Fail(span, cmd) => { - GenericCommand::Fail(span, Box::new(cmd.visit_exprs(f))) - } + GenericCommand::Fail(span, cmds) => GenericCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.visit_exprs(&mut *f)) + .collect(), + ), // All other commands don't contain expressions cmd => cmd, } @@ -2166,6 +2207,7 @@ where term_constructor, unextractable, identity_vals, + cost, } => GenericCommand::Function { span, name, @@ -2176,6 +2218,7 @@ where term_constructor, unextractable, identity_vals, + cost, }, GenericCommand::AddRuleset(span, name) => GenericCommand::AddRuleset(span, name), GenericCommand::UnstableCombinedRuleset(span, name, others) => { @@ -2238,9 +2281,12 @@ where }, GenericCommand::Push(n) => GenericCommand::Push(n), GenericCommand::Pop(span, n) => GenericCommand::Pop(span, n), - GenericCommand::Fail(span, cmd) => { - GenericCommand::Fail(span, Box::new(cmd.map_symbols(head, leaf))) - } + GenericCommand::Fail(span, cmds) => GenericCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.map_symbols(&mut *head, &mut *leaf)) + .collect(), + ), GenericCommand::Include(span, file) => GenericCommand::Include(span, file), GenericCommand::UserDefined(span, name, exprs) => { GenericCommand::UserDefined(span, name, exprs) @@ -2264,9 +2310,12 @@ where rule: rule.visit_actions(f), }, GenericCommand::Action(action) => GenericCommand::Action(f(action)), - GenericCommand::Fail(span, cmd) => { - GenericCommand::Fail(span, Box::new(cmd.visit_actions(f))) - } + GenericCommand::Fail(span, cmds) => GenericCommand::Fail( + span, + cmds.into_iter() + .map(|cmd| cmd.visit_actions(&mut *f)) + .collect(), + ), other => other, } } diff --git a/egglog/src/ast/parse.rs b/egglog/src/ast/parse.rs index 5433a2d5..08246825 100644 --- a/egglog/src/ast/parse.rs +++ b/egglog/src/ast/parse.rs @@ -432,9 +432,10 @@ impl Parser { }] } [name, rest @ ..] => { - // Parse :internal-uf / :internal-proof-func and the - // :internal-proof-names global proof-constructor record. - let mut uf = None; + // Parse :internal-uf / :internal-uf-aux / :internal-proof-func + // and the :internal-proof-names global proof-constructor record. + let mut uf: Option<(String, Option)> = None; + let mut uf_aux: Option = None; let mut proof_func = None; let mut proof_constructors = None; for (key, val) in self.parse_options(rest)? { @@ -448,27 +449,34 @@ impl Parser { Some(uf_index.expect_atom("uf index function name")?), )); } + (":internal-uf-aux", [aux]) => { + uf_aux = Some(aux.expect_atom("uf aux function name")?); + } (":internal-proof-func", [pf]) => { proof_func = Some(pf.expect_atom("internal-proof-func function name")?); } - (":internal-proof-names", [congr, trans, sym, normalize]) => { + (":internal-proof-names", [congr, trans, sym, normalize, fiat]) => { proof_constructors = Some(ProofConstructorNames { congr: congr.expect_atom("congr constructor")?, trans: trans.expect_atom("trans constructor")?, sym: sym.expect_atom("sym constructor")?, normalize: normalize .expect_atom("container-normalize constructor")?, + fiat: fiat.expect_atom("fiat constructor")?, }); } _ => { return error!( span, - "usages:\n(sort )\n(sort :internal-uf [])\n(sort :internal-proof-func )\n(sort :internal-proof-names )\n(sort ( *))" + "usages:\n(sort )\n(sort :internal-uf [] [:internal-uf-aux ])\n(sort :internal-proof-func )\n(sort :internal-proof-names )\n(sort ( *))" ); } } } + // Fold `:internal-uf-aux` into the uf tuple (always paired + // with `:internal-uf` in generated code). + let uf = uf.map(|(ctor, index)| (ctor, index, uf_aux)); vec![Command::Sort { span, name: self.parse_name(name, "sort name")?, @@ -508,6 +516,7 @@ impl Parser { let mut term_constructor = None; let mut unextractable = false; let mut identity_vals = None; + let mut cost = None; for (key, val) in self.parse_options(rest)? { match (key, val) { (":no-merge", []) => { @@ -568,6 +577,7 @@ impl Parser { identity_vals = Some(k.expect_uint::("identity value column count")?) } + (":internal-cost", [c]) => cost = Some(c.expect_uint("cost")?), _ => return error!(span, "could not parse function options"), } } @@ -589,6 +599,7 @@ impl Parser { term_constructor, unextractable, identity_vals, + cost, span, }] } @@ -968,16 +979,16 @@ impl Parser { [file] => vec![Command::Include(span, file.expect_string("file name")?)], _ => return error!(span, "usage: (include )"), }, - "fail" => match tail { - [subcommand] => { - let mut cs = self.parse_command(subcommand)?; - if cs.len() != 1 { - todo!("extend Fail to work with multiple parsed commands") - } - vec![Command::Fail(span, Box::new(cs.remove(0)))] + "fail" => { + if tail.is_empty() { + return error!(span, "usage: (fail +)"); } - _ => return error!(span, "usage: (fail )"), - }, + let mut cs = vec![]; + for subcommand in tail { + cs.extend(self.parse_command(subcommand)?); + } + vec![Command::Fail(span, cs)] + } _ => self .parse_action(sexp)? .into_iter() diff --git a/egglog/src/ast/proof_global_remover.rs b/egglog/src/ast/proof_global_remover.rs deleted file mode 100644 index 0388e69c..00000000 --- a/egglog/src/ast/proof_global_remover.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Remove global variables from the program by translating -//! them into constructors, making proof generation easier. -//! Does not support primitive-valued globals. - -use crate::ast::{ - FunctionSubtype, GenericNCommand, ResolvedAction, ResolvedActions, ResolvedExprExt, - ResolvedFunctionDecl, ResolvedNCommand, Schema, -}; -use crate::*; -use crate::{core::ResolvedCall, typechecking::FuncType}; -use egglog_ast::generic_ast::{GenericAction, GenericExpr, GenericRule}; - -/// Removes all globals from a program. -/// No top level lets are allowed after this pass, -/// nor any variable that references a global. -/// Adds new functions for global variables -/// and replaces references to globals with -/// references to the new functions. -/// e.g. -/// ```ignore -/// (let x (Add 1 2)) -/// ``` -/// becomes -/// ```ignore -/// (function x () Math) -/// (union (x) (Add 1 2)) -/// ``` -pub(crate) fn remove_globals( - prog: Vec, - _fresh: &mut SymbolGen, -) -> Vec { - prog.into_iter().flat_map(remove_globals_cmd).collect() -} - -fn resolved_var_to_call(var: &ResolvedVar) -> ResolvedCall { - assert!( - var.is_global_ref, - "resolved_var_to_call called on non-global var" - ); - ResolvedCall::Func(FuncType { - name: var.name.clone(), - subtype: FunctionSubtype::Constructor, - input: vec![], - outputs: vec![var.sort.clone()], - }) -} - -/// TODO (yz) it would be better to implement replace_global_var -/// as a function from ResolvedVar to ResolvedExpr -/// and use it as an argument to `subst` instead of `visit_expr`, -/// but we have not implemented `subst` for command. -fn replace_global_vars(expr: ResolvedExpr) -> ResolvedExpr { - match expr.get_global_var() { - Some(resolved_var) => { - GenericExpr::Call(expr.span(), resolved_var_to_call(&resolved_var), vec![]) - } - None => expr, - } -} - -fn remove_globals_expr(expr: ResolvedExpr) -> ResolvedExpr { - expr.visit_exprs(&mut replace_global_vars) -} - -fn remove_globals_action(action: ResolvedAction) -> ResolvedAction { - action.visit_exprs(&mut replace_global_vars) -} - -fn remove_globals_cmd(cmd: ResolvedNCommand) -> Vec { - match cmd { - GenericNCommand::CoreAction(action) => match action { - GenericAction::Let(span, name, expr) => { - let ty = expr.output_type(); - if !ty.is_eq_sort() { - panic!("Global variable {} has non-eq sort {}", name, ty.name()); - } - - let resolved_call = ResolvedCall::Func(FuncType { - name: name.name.clone(), - subtype: FunctionSubtype::Constructor, - input: vec![], - outputs: vec![ty.clone()], - }); - let func_decl = ResolvedFunctionDecl { - name: name.name, - subtype: FunctionSubtype::Constructor, - schema: Schema { - input: vec![], - outputs: vec![ty.name().to_owned()], - }, - resolved_schema: resolved_call.clone(), - merge: None, - cost: None, - unextractable: true, - internal_hidden: false, - internal_let: true, - span: span.clone(), - term_constructor: None, - identity_vals: None, - }; - vec![ - GenericNCommand::Function(func_decl), - GenericNCommand::CoreAction(GenericAction::Union( - span.clone(), - ResolvedExpr::Call(span.clone(), resolved_call, vec![]), - remove_globals_expr(expr), - )), - ] - } - _ => vec![GenericNCommand::CoreAction(remove_globals_action(action))], - }, - GenericNCommand::NormRule { rule } => { - let new_rule = GenericRule { - span: rule.span, - body: rule - .body - .iter() - .map(|fact| fact.clone().visit_exprs(&mut replace_global_vars)) - .collect(), - head: ResolvedActions::new( - rule.head - .iter() - .map(|action| action.clone().visit_exprs(&mut replace_global_vars)) - .collect(), - ), - name: rule.name.clone(), - ruleset: rule.ruleset.clone(), - eval_mode: rule.eval_mode, - no_decomp: rule.no_decomp, - include_subsumed: rule.include_subsumed, - }; - vec![GenericNCommand::NormRule { rule: new_rule }] - } - GenericNCommand::Fail(span, cmd) => { - let mut removed = remove_globals_cmd(*cmd); - let last = removed - .pop() - .expect("remove_globals_cmd returned empty result"); - let boxed_last = Box::new(last); - removed.push(GenericNCommand::Fail(span, boxed_last)); - removed - } - _ => vec![cmd.visit_exprs(&mut replace_global_vars)], - } -} diff --git a/egglog/src/ast/remove_globals.rs b/egglog/src/ast/remove_globals.rs index 9c8f889c..3d393617 100644 --- a/egglog/src/ast/remove_globals.rs +++ b/egglog/src/ast/remove_globals.rs @@ -185,14 +185,15 @@ impl GlobalRemover<'_> { }; vec![GenericNCommand::NormRule { rule: new_rule }] } - // Handle the corner case where a global command is wrap in (fail ) - GenericNCommand::Fail(span, cmd) => { - let mut removed = self.remove_globals_cmd(*cmd); - let last = removed.pop().unwrap(); - let boxed_last = Box::new(last); - let new_command = GenericNCommand::Fail(span, boxed_last); - removed.push(new_command); - removed + // Handle the corner case where a global command is wrapped in (fail). + // Remove globals from every wrapped command and keep the whole flattened + // result inside the `fail` (not just the last command). + GenericNCommand::Fail(span, cmds) => { + let mut removed = vec![]; + for cmd in cmds { + removed.extend(self.remove_globals_cmd(cmd)); + } + vec![GenericNCommand::Fail(span, removed)] } _ => vec![cmd.visit_exprs(&mut replace_global_vars)], } diff --git a/egglog/src/extract.rs b/egglog/src/extract.rs index fe9e5955..437754b9 100644 --- a/egglog/src/extract.rs +++ b/egglog/src/extract.rs @@ -711,33 +711,46 @@ pub(crate) fn find_canonical(egraph: &EGraph, value: Value, sort: &ArcSort) -> V } impl Function { - /// Returns the extraction head cost for this table. - /// View tables inherit the cost of their referenced hidden term constructor. - pub(crate) fn extraction_head_cost(&self, egraph: &EGraph) -> DefaultCost { - if let Some(term_constructor) = &self.decl.term_constructor { - egraph - .functions - .get(term_constructor) - .and_then(|func| func.decl.cost) - .unwrap_or(DefaultCost::unit()) - } else { - self.decl.cost.unwrap_or(DefaultCost::unit()) - } + /// Returns the extraction head cost for this table. A view table carries its + /// term operation's cost via `:internal-cost` (the term table is a relation + /// that can't hold `:cost`); an ordinary constructor carries its own `:cost`. + /// Either way it is `decl.cost`. + pub(crate) fn extraction_head_cost(&self, _egraph: &EGraph) -> DefaultCost { + self.decl.cost.unwrap_or(DefaultCost::unit()) } /// Whether this is the functional-dependency view `(children) -> (eclass, {Unit|Proof})`, /// where the e-class is the first output column rather than the last input column. - fn is_fd_view(&self) -> bool { + pub(crate) fn is_fd_view(&self) -> bool { self.decl.term_constructor.is_some() && self.schema.outputs.len() > 1 } + /// A term or proof relation created by the term/proof encoding: an + /// internal-hidden function-to-`Unit` where the minted id is the last input + /// column and the earlier inputs are the term's children. (Distinct from + /// views, which carry `term_constructor` and have a non-`Unit` output.) Such + /// a relation is reconstructed like an old-form view: last input = output id. + pub(crate) fn is_relation_term(&self) -> bool { + self.decl.subtype == FunctionSubtype::Custom + && self.decl.internal_hidden + && self.decl.term_constructor.is_none() + && self.schema.outputs.len() == 1 + && self.schema.outputs[0].name() == "Unit" + } + + /// True when the id is the last input column (old-form views and encoding + /// relations), rather than a real output column. + fn id_is_last_input(&self) -> bool { + (self.decl.term_constructor.is_some() && !self.is_fd_view()) || self.is_relation_term() + } + /// For view tables (with term_constructor), the effective output sort is the last input column /// (old form) or the first output column (FD tuple view). For regular tables, it's the output. /// This is used by extraction to determine which sort a table produces values for. pub(crate) fn extraction_output_sort(&self) -> &ArcSort { if self.is_fd_view() { self.schema.output() - } else if self.decl.term_constructor.is_some() { + } else if self.id_is_last_input() { self.schema.input.last().unwrap() } else { self.schema.output() @@ -748,7 +761,7 @@ impl Function { /// For old-form view tables, this excludes the last input column (the e-class); FD tuple views /// key on children only, so all inputs are children. pub(crate) fn extraction_num_children(&self) -> usize { - if self.decl.term_constructor.is_some() && !self.is_fd_view() { + if self.id_is_last_input() { self.schema.input.len() - 1 } else { self.schema.input.len() @@ -768,8 +781,9 @@ impl Function { /// For view tables, the e-class is the last input column (second-to-last in the row). /// For regular tables, it's the last column (the actual output). pub(crate) fn extraction_output_index(&self) -> usize { - if self.decl.term_constructor.is_some() && !self.is_fd_view() { - // Old-form view: row is [children..., eclass, view_sort]; eclass at input.len() - 1. + if self.id_is_last_input() { + // Old-form view / encoding relation: row is [children..., id, ...]; + // the id is at input.len() - 1. self.schema.input.len() - 1 } else { // Regular table: [inputs..., output]. FD view: [children..., eclass, proof]; the eclass diff --git a/egglog/src/lib.rs b/egglog/src/lib.rs index 189a3677..d40d18b3 100644 --- a/egglog/src/lib.rs +++ b/egglog/src/lib.rs @@ -539,10 +539,31 @@ impl EGraph { proofs::proof_encoding_helpers::OrientProof::max(), Some(orient_proof_validator(false)), ); + // `select-eq test cand if-eq else`: keeps a custom FD-view merge's proof + // column stable (reuse a premise proof on an unchanged output). See + // [`crate::proofs::proof_encoding_helpers::SelectEqProof`]. + let select_eq_validator: PrimitiveValidator = + Arc::new(|_: &mut TermDag, args: &[TermId]| -> Option { + let [test, cand, if_eq, els] = args else { + return None; + }; + Some(if test == cand { *if_eq } else { *els }) + }); + eg.add_pure_primitive( + proofs::proof_encoding_helpers::SelectEqProof, + Some(select_eq_validator), + ); eg.rulesets .insert("".into(), Ruleset::Rules(Default::default())); + // The generic `get-fresh!` mint primitive is registered on every e-graph + // (a no-op without an eclass-id counter). Doing it here — rather than + // per-eq-sort — means it is present whenever the *encoded* program is run, + // including when the already-desugared program is replayed in a plain + // e-graph (e.g. the desugar proof-testing path). + crate::proofs::proof_fresh::register_get_fresh(&mut eg); + eg } } @@ -575,10 +596,19 @@ impl EGraph { /// commands. pub fn new_with_term_encoding() -> Self { let mut egraph = EGraph::default(); - egraph.proof_state.original_typechecking = Some(Box::new(egraph.clone())); + let typechecker = egraph.clone(); + egraph.enable_term_encoding(typechecker); egraph } + /// Enable the term/proof encoding pipeline with `typechecker` as the head of + /// the re-typechecking chain. (`get-fresh!` is already registered on every + /// e-graph by [`Self::with_backend`], so no per-`Sort` re-registration is + /// needed.) + fn enable_term_encoding(&mut self, typechecker: EGraph) { + self.proof_state.original_typechecking = Some(Box::new(typechecker)); + } + /// Create a new e-graph with proof generation enabled. pub fn new_with_proofs() -> Self { let mut egraph = EGraph::new_with_term_encoding(); @@ -591,7 +621,8 @@ impl EGraph { /// This method is to support the current CLI implementation with egglog-experimental (https://github.com/egraphs-good/egglog/issues/768) #[doc(hidden)] pub fn with_term_encoding_enabled(mut self) -> Self { - self.proof_state.original_typechecking = Some(Box::new(self.clone())); + let typechecker = self.clone(); + self.enable_term_encoding(typechecker); self } @@ -603,7 +634,7 @@ impl EGraph { /// union-find. Re-typechecking after the encoder runs uses a default /// (bridge-backed) e-graph, so this backend need not implement typechecking. pub fn with_term_encoding(mut self) -> Self { - self.proof_state.original_typechecking = Some(Box::new(EGraph::default())); + self.enable_term_encoding(EGraph::default()); self } @@ -611,7 +642,7 @@ impl EGraph { /// bridge-backed e-graph for parsing/typechecking before instrumentation. #[doc(hidden)] pub fn with_term_encoding_typechecker(mut self, typechecker: EGraph) -> Self { - self.proof_state.original_typechecking = Some(Box::new(typechecker)); + self.enable_term_encoding(typechecker); self } @@ -1959,10 +1990,15 @@ impl EGraph { .. } => { // Restore the sort's UF metadata into proof_state. - if let Some((uf_ctor, _uf_index)) = uf { + if let Some((uf_ctor, _uf_index, uf_aux)) = uf { self.proof_state .uf_parent .insert(name.clone(), uf_ctor.clone()); + if let Some(uf_aux) = uf_aux { + self.proof_state + .uf_aux_parent + .insert(name.clone(), uf_aux.clone()); + } } // If the sort has a :internal-proof-func field, store the mapping for proof lookup. // This annotation is set by proof instrumentation and consumed here. @@ -1980,6 +2016,9 @@ impl EGraph { names.eq_trans_constructor = pc.trans; names.eq_sym_constructor = pc.sym; names.container_normalize_constructor = pc.normalize; + // Recovered so `native_input` can build `(input …)` base-fact + // proofs when replaying an encoded program in a fresh e-graph. + names.fiat_constructor = pc.fiat; } log::info!("Declared sort {name}.") } @@ -2122,16 +2161,35 @@ impl EGraph { })?; return Ok(vec![res]); } - ResolvedNCommand::Fail(span, c) => { - let result = self.run_command(*c); - if let Err(e) = result { - log::info!("Command failed as expected: {e}"); - } else { + ResolvedNCommand::Fail(span, cmds) => { + // Run the wrapped commands in order; the first error is the expected + // failure. If none error, the `fail` assertion itself fails. + let mut any_failed = false; + for c in cmds { + if let Err(e) = self.run_command(c) { + log::info!("Command failed as expected: {e}"); + any_failed = true; + break; + } + } + if !any_failed { return Err(Error::ExpectFail(span)); } } ResolvedNCommand::Input { span, name, file } => { - self.input_file(span, &name, file)?; + // An encoded program (term/proof mode, or a replayed desugared + // program) keeps `(input …)` targeting the encoded *term relation*, + // loaded natively into the encoded tables; a plain program targets a + // user relation/constructor loaded by the relation loader. + if self + .functions + .get(&name) + .is_some_and(|f| f.is_relation_term()) + { + self.native_input(span, &name, file)?; + } else { + self.input_file(span, &name, file)?; + } } ResolvedNCommand::Output { span, file, exprs } => { let mut filename = self.fact_directory.clone().unwrap_or_default(); @@ -2201,9 +2259,6 @@ impl EGraph { span: &Span, file: &str, ) -> Result>, Error> { - let mut filename = fact_directory.map_or_else(PathBuf::new, PathBuf::from); - filename.push(file); - for sort in &function_type.input { match sort.name() { "i64" | "f64" | "String" => {} @@ -2219,20 +2274,35 @@ impl EGraph { } } - log::info!("Opening file '{filename:?}'..."); - let contents = std::fs::read_to_string(&filename) - .map_err(|error| Error::IoError(filename, error, span.clone()))?; let mut row_schema = function_type.input.clone(); // Relations desugar to constructors, so their implicit output is not a TSV column. if function_type.subtype == FunctionSubtype::Custom { row_schema.extend(function_type.outputs.iter().cloned()); } + Self::read_input_rows(fact_directory, &row_schema, span, file) + } + + /// Read a TSV `file` into literal rows matching `row_schema` (one column per + /// sort). A `Unit` column contributes `Literal::Unit` without consuming a + /// field; `i64`/`f64`/`String` columns are parsed from the next field. + fn read_input_rows( + fact_directory: Option<&std::path::Path>, + row_schema: &[ArcSort], + span: &Span, + file: &str, + ) -> Result>, Error> { + let mut filename = fact_directory.map_or_else(PathBuf::new, PathBuf::from); + filename.push(file); + + log::info!("Opening file '{filename:?}'..."); + let contents = std::fs::read_to_string(&filename) + .map_err(|error| Error::IoError(filename, error, span.clone()))?; let mut rows = Vec::with_capacity(contents.lines().count()); for line in contents.lines() { let mut fields = line.split('\t').map(str::trim); let mut row = Vec::with_capacity(row_schema.len()); - for sort in &row_schema { + for sort in row_schema { if sort.name() == "Unit" { row.push(Literal::Unit); continue; @@ -2251,7 +2321,7 @@ impl EGraph { .map(Literal::Float) .map_err(|_| Error::InputFileFormatError(file.to_owned()))?, "String" => Literal::String(raw.to_owned()), - _ => unreachable!(), + name => panic!("Unsupported type {name} for input"), }; row.push(literal); } @@ -2333,6 +2403,150 @@ impl EGraph { Ok(()) } + /// Load `(input …)` facts natively into the term/proof encoding's tables. For + /// each row we mint a term id (and, when the encoding carries proofs, its AST + + /// fiat-proof ids) and insert the encoded term-relation, view, and proof rows + /// directly via the backend SPI — no compiled loader rule. Rows are + /// plain-inserted (no get-or-insert): a duplicate view key is resolved by the + /// view's `:merge`. The proof checker keeps using the per-row top-level fiat + /// actions (`desugared_before_proofs`); this just materializes the same table + /// state, so it works identically on any backend that services `add_values`. + /// + /// Everything is derived from the encoded schema + annotations (never the + /// pre-encoding `FuncType`), so it also works when a desugared program is + /// replayed in a fresh e-graph. `func_name` names the encoded *term relation*; + /// its view is found by the view's `:internal-term-constructor` back-reference. + /// The encoded shape is read off the two schemas: + /// * constructor / relation (FD view, `term_inputs == view_inputs + 1`) — term + /// row `(F children… term-id) Unit`, FD view `(children…) -> (term-id, + /// proof)`, and the term id's `Proof` row. + /// * custom `:merge` (FD view, `term_inputs == view_inputs + 2`) / `:no-merge` + /// (non-FD all-column view) — term row `(f children… output term-id) Unit` + /// and view row `(children… output proof)`; the proof lives only in the view + /// (a custom's fresh term sort has no `Proof`). A `:no-merge` custom + /// also mirrors the output into its hidden `current` helper. + fn native_input(&mut self, span: Span, func_name: &str, file: String) -> Result<(), Error> { + // The encoded term relation keeps the user's original name. Its last input + // column is the minted term id; the columns before it are the CSV base + // columns (children, plus a custom function's output value). + let term = self + .functions + .get(func_name) + .unwrap_or_else(|| panic!("Unrecognized function name {func_name}")); + let f_id = term.backend_id; + let term_input = term.schema.input.clone(); + let n_term_input = term_input.len(); + let term_id_sort = term_input[n_term_input - 1].name().to_string(); + let csv_sorts: Vec = term_input[..n_term_input - 1].to_vec(); + + // Locate the view by its `:internal-term-constructor` back-reference (as + // extraction / print-size do) and read the encoded shape off it. + let view = self + .functions + .values() + .find(|g| g.decl.term_constructor.as_deref() == Some(func_name)) + .unwrap_or_else(|| panic!("no encoded view for {func_name}")); + let view_id = view.backend_id; + let view_n_inputs = view.schema.input.len(); + // Proofs are on for this relation iff the view's proof column (its last + // output) is not `Unit`; term-encoding-only mode uses `Unit` there. + let proofs = view.schema.outputs.last().unwrap().name() != "Unit"; + // Constructor iff its FD view keys on all children and the term relation + // adds exactly the term id (a custom `:merge` FD view adds an output column + // too; a `:no-merge` custom's view is the non-FD all-column form). + let is_constructor = view.is_fd_view() && n_term_input == view_n_inputs + 1; + + let rows = Self::read_input_rows(self.fact_directory.as_deref(), &csv_sorts, &span, &file)?; + let unit_val = self.backend.base_values().get(()); + // Convert literals to values up front (ends the `&backend` borrow before minting). + let value_rows: Vec> = rows + .iter() + .map(|row| { + row.iter() + .map(|lit| match lit { + Literal::Int(v) => self.backend.base_values().get(*v), + Literal::Float(v) => self + .backend + .base_values() + .get::(core_relations::Boxed::new(*v)), + Literal::String(v) => self.backend.base_values().get::(v.clone().into()), + Literal::Unit => unit_val, + Literal::Bool(_) => unreachable!(), + }) + .collect() + }) + .collect(); + + // Proof tables, recovered from replay-safe annotations rather than the + // encoding-time `proof_names` maps: `Fiat` from the `Proof` sort's + // `:internal-proof-names`, the term-id sort's AST constructor by its + // signature `( ) -> Unit`, and (constructors only) `Proof` + // from the term-id sort's `:internal-proof-func`. + let proof_tables = proofs.then(|| { + let fiat = self.proof_state.proof_names.fiat_constructor.clone(); + let fiat_fn = &self.functions[&fiat]; + let fiat_id = fiat_fn.backend_id; + let ast_sort = fiat_fn.schema.input[0].name().to_string(); + let ast_id = self + .functions + .values() + .find(|g| { + g.decl.internal_hidden + && g.schema.input.len() == 2 + && g.schema.input[0].name() == term_id_sort + && g.schema.input[1].name() == ast_sort + && g.schema.output().name() == "Unit" + }) + .unwrap_or_else(|| panic!("no AST constructor for sort {term_id_sort}")) + .backend_id; + let proof_func_id = is_constructor.then(|| { + let pf = self.proof_state.proof_func_parent[&term_id_sort].clone(); + self.functions[&pf].backend_id + }); + (ast_id, fiat_id, proof_func_id) + }); + + let num_facts = value_rows.len(); + let mut batch: Vec<(egglog_bridge::FunctionId, Vec)> = Vec::new(); + for value_row in value_rows { + let fv = self.backend.fresh_eclass_id(); + // Term-relation row: CSV columns (children [+ output]) + term id + Unit. + let mut frow = value_row.clone(); + frow.push(fv); + frow.push(unit_val); + batch.push((f_id, frow)); + + let view_proof = if let Some((ast_id, fiat_id, proof_func_id)) = proof_tables { + // Fiat proof of the base fact: `@Fiat(ast(fv), ast(fv))`. + let a1 = self.backend.fresh_eclass_id(); + batch.push((ast_id, vec![fv, a1, unit_val])); + let a2 = self.backend.fresh_eclass_id(); + batch.push((ast_id, vec![fv, a2, unit_val])); + let pf = self.backend.fresh_eclass_id(); + batch.push((fiat_id, vec![a1, a2, pf, unit_val])); + if let Some(proof_func_id) = proof_func_id { + batch.push((proof_func_id, vec![fv, pf])); + } + pf + } else { + unit_val + }; + + // View row. A constructor's FD view value-0 is the minted term id; a + // custom view stores the base output (already in `value_row`). The + // proof column follows (`Unit` when the encoding carries no proofs). + let mut vrow = value_row; + if is_constructor { + vrow.push(fv); + } + vrow.push(view_proof); + batch.push((view_id, vrow)); + } + self.backend.add_values(batch); + log::info!("Natively loaded {num_facts} facts into {func_name} from '{file}'."); + Ok(()) + } + /// Returns true if proofs are enabled. pub fn are_proofs_enabled(&self) -> bool { self.proof_state.proofs_enabled @@ -2386,15 +2600,33 @@ impl EGraph { desugared_before_proofs: vec![], }) } else { - // Input expansion needs resolved schemas. Lower it once here so the - // encoded execution and proof checker consume the same fiat actions. - let resolved_before_proofs = - ProofInstrumentor::lower_inputs(self, resolved_before_proofs)?; - // Now remove globals for actual execution (but NOT from desugared_commands) - let typechecked_no_globals = proof_global_remover::remove_globals( - resolved_before_proofs.clone(), - &mut self.parser.symbol_gen, - ); + // The proof checker consumes the per-row top-level fiat actions. + let per_row_before_proofs = + ProofInstrumentor::lower_inputs(self, resolved_before_proofs.clone())?; + // Execution keeps every `(input …)` as an `Input` command, loaded + // natively at run time by `EGraph::native_input` straight into the + // encoded tables (no loader rule, no per-mint global function). + // Function-style global desugaring (same pass native/off mode uses): + // each global `(let x …)` becomes an `:internal-let` no-arg function + // `set` to its value, with RHS uses looked up in the query. Replaces the + // old constructor+`union` desugaring so the top level uses `set` (no + // union), and the global is a function with a view like any other. + let typechecked_no_globals = + remove_globals::remove_globals(resolved_before_proofs, &mut self.parser.symbol_gen); + // The term encoder runs before the encoded program is typechecked, so it + // can't rely on the later typecheck to populate `global_sorts`. Register + // the new global functions' sorts eagerly so `is_global` recognizes them + // while encoding. + for command in &typechecked_no_globals { + if let GenericNCommand::Function(fdecl) = command + && fdecl.internal_let + && let Some(output_sort) = self.type_info.sorts.get(fdecl.schema.output()) + { + self.type_info + .global_sorts + .insert(fdecl.name.clone(), output_sort.clone()); + } + } for command in &typechecked_no_globals { self.names.check_shadowing(command)?; } @@ -2411,7 +2643,8 @@ impl EGraph { // Now typecheck using self, adding term type information. let desugared_typechecked = self.typecheck_program(&desugared)?; - // remove globals again, but this time allow primitive globals + // Remove the globals the term encoding itself introduced (its minted + // `let`s), the same way source-level globals were removed above. let desugared_typechecked = remove_globals::remove_globals( desugared_typechecked, &mut self.parser.symbol_gen, @@ -2421,7 +2654,7 @@ impl EGraph { } Ok(ResolvedNCommands { desugared: new_typechecked, - desugared_before_proofs: resolved_before_proofs, + desugared_before_proofs: per_row_before_proofs, }) } } diff --git a/egglog/src/prelude.rs b/egglog/src/prelude.rs index 6d74b9dd..676fa7a7 100644 --- a/egglog/src/prelude.rs +++ b/egglog/src/prelude.rs @@ -781,6 +781,7 @@ pub fn add_function( term_constructor: None, unextractable: false, identity_vals: None, + cost: None, }]) } diff --git a/egglog/src/proofs/mod.rs b/egglog/src/proofs/mod.rs index 037d42a9..3dc25b96 100644 --- a/egglog/src/proofs/mod.rs +++ b/egglog/src/proofs/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod proof_encoding_helpers; pub(crate) mod proof_extraction; pub(crate) mod proof_extractor; pub(crate) mod proof_format; +pub(crate) mod proof_fresh; pub(crate) mod proof_normal_form; pub(crate) mod proof_simplification; pub(crate) mod proof_tests; diff --git a/egglog/src/proofs/proof_checker.rs b/egglog/src/proofs/proof_checker.rs index 5720117c..fe4749a1 100644 --- a/egglog/src/proofs/proof_checker.rs +++ b/egglog/src/proofs/proof_checker.rs @@ -165,7 +165,7 @@ pub(crate) fn process_actions( /// Returns Ok((TermId, propositions)) if successful, where propositions include /// all reflexive equalities for the term and its subterms. /// Returns Err(()) if evaluation fails. -fn eval_expr_with_subst( +pub(crate) fn eval_expr_with_subst( rule_name: &str, expr: &ResolvedExpr, dag: &mut TermDag, diff --git a/egglog/src/proofs/proof_container_rebuild.rs b/egglog/src/proofs/proof_container_rebuild.rs index d4ce3445..d7e17166 100644 --- a/egglog/src/proofs/proof_container_rebuild.rs +++ b/egglog/src/proofs/proof_container_rebuild.rs @@ -9,6 +9,26 @@ use crate::exec_state::{Internal, RegistrySealed}; use crate::*; +use egglog_backend_trait::CounterId; +use egglog_bridge::TableAction; +use egglog_numeric_id::NumericId; + +/// Mint a fresh proof id and assert the relation row `( args… out ())`, +/// returning `out`. Proof constructors are relations `(@C args… out)`, so a proof +/// node is built by minting its id and inserting the row (the id is the last +/// input column, the `Unit` output is `()`). +fn mint_proof_row( + state: &mut FullState, + action: &TableAction, + id_counter: CounterId, + args: &[Value], +) -> Value { + let out = Value::from_usize(state.raw_exec_state().inc_counter(id_counter)); + let unit = state.base_values().get::<()>(()); + let row: Vec = args.iter().copied().chain([out, unit]).collect(); + action.insert(state.raw_exec_state(), row.into_iter()); + out +} /// Register a container sort's rebuild primitives from its /// [`ContainerRebuildSpec`]. Called when a container Sort command carrying an @@ -22,22 +42,31 @@ pub(crate) fn register_container_rebuild_from_spec( let Some(container_sort) = eg.get_sort_by_name(sort_name).cloned() else { return; }; - // Each element eq-sort's single UF table, recovered from proof_state (filled - // by the element sorts' `:internal-uf` on re-parse) rather than the spec. + // Each element eq-sort's single UF (and, in proof mode, aux UF) table, + // recovered from proof_state (filled by the element sorts' `:internal-uf` / + // `:internal-uf-aux` on re-parse) rather than the spec. let mut uf_names = HashMap::default(); collect_element_uf_names(eg, &container_sort, &mut uf_names); + let mut aux_names = HashMap::default(); + collect_element_aux_names(eg, &container_sort, &mut aux_names); eg.add_read_primitive( ContainerRebuild { name: spec.internal_rebuild_prim.clone(), container_sort: container_sort.clone(), uf_names: uf_names.clone(), + aux_names: aux_names.clone(), proof_mode: spec.internal_rebuild_proof_prim.is_some(), }, None, ); if let Some(proof_prim) = &spec.internal_rebuild_proof_prim { + // Proof nodes are minted from the backend's id counter (proof constructors + // are relations). A backend without a counter can't run these proofs. + let Some(id_counter) = eg.backend.eclass_id_counter() else { + return; + }; // Each container's `Proof` table (this sort + nested containers), // recovered from proof_state (filled by `:internal-proof-func`). let mut cproof_names = HashMap::default(); @@ -58,11 +87,13 @@ pub(crate) fn register_container_rebuild_from_spec( container_sort, proof_sort, uf_names, + aux_names, cproof_names, congr_name, trans_name, sym_name, container_normalize_name, + id_counter, }, None, ); @@ -83,6 +114,21 @@ fn collect_element_uf_names(eg: &EGraph, sort: &ArcSort, out: &mut HashMap) { + for elem in sort.inner_sorts() { + if elem.is_eq_sort() { + if let Some(aux) = eg.proof_state.uf_aux_parent.get(elem.name()) { + out.insert(elem.name().to_string(), aux.clone()); + } + } else if elem.is_eq_container_sort() { + collect_element_aux_names(eg, &elem, out); + } + } +} + /// The `Proof` table for `sort` and every nested container sort, from /// `proof_state.proof_func_parent` (filled by `:internal-proof-func`). fn collect_container_proof_names(eg: &EGraph, sort: &ArcSort, out: &mut HashMap) { @@ -124,6 +170,7 @@ fn rebuild_container_value_rec( sort: &ArcSort, value: Value, uf_names: &HashMap, + aux_names: &HashMap, proof_mode: bool, ) -> Option { let elements = { @@ -133,11 +180,18 @@ fn rebuild_container_value_rec( let mut leaders: HashMap = HashMap::default(); for (esort, eval) in &elements { let new = if esort.is_eq_sort() { - lookup_uf_row(state, uf_names, esort, *eval, proof_mode) - .map(|(leader, _)| leader) - .unwrap_or(*eval) + // Chain: a natural element resolves through `UF-Aux` to its canonical + // id, which then resolves through the main `UF` to its leader. + let mut cur = *eval; + if let Some((canonical, _)) = lookup_aux_row(state, aux_names, esort, cur) { + cur = canonical; + } + if let Some((leader, _)) = lookup_uf_row(state, uf_names, esort, cur, proof_mode) { + cur = leader; + } + cur } else if esort.is_eq_container_sort() { - rebuild_container_value_rec(state, esort, *eval, uf_names, proof_mode)? + rebuild_container_value_rec(state, esort, *eval, uf_names, aux_names, proof_mode)? } else { *eval }; @@ -169,6 +223,28 @@ where Some((values[0], proof_mode.then(|| values[1]))) } +/// Look up a natural element's `UF_Aux_` row: `natural -> (canonical, +/// connector)`, where `connector` proves `natural = canonical`. Containers are +/// built over natural element ids (so their term-proof extracts the syntactic +/// shape); this is how the rebuild recovers the canonical element. Returns +/// `None` outside proof mode (no aux table for the sort) or when the element is +/// not a recorded natural. The table name comes from `aux_names` (recovered from +/// the element sort's `:internal-uf-aux` on re-parse). +fn lookup_aux_row<'a, 'db: 'a, S>( + state: &S, + aux_names: &HashMap, + esort: &ArcSort, + eval: Value, +) -> Option<(Value, Value)> +where + S: RegistrySealed<'a, 'db>, +{ + let aux_name = aux_names.get(esort.name())?; + let action = state.registry().lookup_table(aux_name)?; + let values = action.lookup_values(state.es(), &[eval])?; + Some((values[0], values[1])) +} + /// A term-encoding primitive that canonicalizes a container value's elements to /// their union-find leaders (recursing through nested containers). Registered /// per container sort by `ensure_container_rebuild` and @@ -180,6 +256,8 @@ struct ContainerRebuild { container_sort: ArcSort, /// element-sort name -> single `UF_` table name (all reachable eq-sorts) uf_names: HashMap, + /// element-sort name -> `UF_Aux_` table name (proof mode; empty otherwise) + aux_names: HashMap, /// Whether the single UF row has a second proof value column. proof_mode: bool, } @@ -206,6 +284,7 @@ impl ReadPrim for ContainerRebuild { &self.container_sort, args[0], &self.uf_names, + &self.aux_names, self.proof_mode, ) } @@ -224,6 +303,8 @@ struct ContainerRebuildProof { proof_sort: ArcSort, /// element-sort name -> single `UF_` table name (all reachable eq-sorts) uf_names: HashMap, + /// element-sort name -> `UF_Aux_` table name (all reachable eq-sorts) + aux_names: HashMap, /// container-sort name -> `Proof` table name (all reachable containers) cproof_names: HashMap, /// `Congr` / `Trans` / `Sym` / `ContainerNormalize` proof constructor names @@ -231,6 +312,10 @@ struct ContainerRebuildProof { trans_name: String, sym_name: String, container_normalize_name: String, + /// Counter for minting fresh proof ids: the proof constructors are relations + /// (`(@C args out)`), so a proof node is created by minting `out` and + /// inserting the row, rather than a constructor's lookup-or-insert. + id_counter: egglog_backend_trait::CounterId, } impl Primitive for ContainerRebuildProof { @@ -281,12 +366,35 @@ fn rebuild_container_proof_rec( let mut child_proofs: Vec<(usize, Value)> = vec![]; for (j, (esort, eval)) in elements.iter().enumerate() { if esort.is_eq_sort() { - if let Some((leader, Some(proof))) = - lookup_uf_row(state, &prim.uf_names, esort, *eval, true) - && leader != *eval + // Chain the two hops and compose their proofs: `natural --connector--> + // canonical --uf_proof--> leader`. Either hop may be absent. + let mut cur = *eval; + let mut proof: Option = None; + if let Some((canonical, connector)) = lookup_aux_row(state, &prim.aux_names, esort, cur) { - leaders.insert(*eval, leader); - child_proofs.push((j, proof)); + cur = canonical; + proof = Some(connector); + } + if let Some((leader, Some(uf_proof))) = + lookup_uf_row(state, &prim.uf_names, esort, cur, true) + { + proof = Some(match proof { + Some(connector) => { + let trans_action = state.registry().lookup_table(&prim.trans_name)?.clone(); + mint_proof_row( + state, + &trans_action, + prim.id_counter, + &[connector, uf_proof], + ) + } + None => uf_proof, + }); + cur = leader; + } + if cur != *eval { + leaders.insert(*eval, cur); + child_proofs.push((j, proof.expect("changed element must carry a proof"))); } } else if esort.is_eq_container_sort() { let (rebuilt_child, child_proof) = @@ -313,8 +421,12 @@ fn rebuild_container_proof_rec( let mut current = base; for (j, proof) in child_proofs { let j_val = state.base_values().get::(j as i64); - current = - congr_action.lookup_or_insert(state.raw_exec_state(), &[current, j_val, proof])?; + current = mint_proof_row( + state, + &congr_action, + prim.id_counter, + &[current, j_val, proof], + ); } // Bridge the (possibly non-canonical) `raw` term to the canonical `rebuilt` @@ -327,7 +439,7 @@ fn rebuild_container_proof_rec( .registry() .lookup_table(&prim.container_normalize_name)? .clone(); - current = normalize_action.lookup_or_insert(state.raw_exec_state(), &[current])?; + current = mint_proof_row(state, &normalize_action, prim.id_counter, &[current]); // Anchor a reflexive proof on the rebuilt value for future rebuilds. if rebuilt != value { @@ -338,8 +450,8 @@ fn rebuild_container_proof_rec( .lookup_table(prim.cproof_names.get(sort.name())?)? .clone(); // Sym(current): rebuilt = value; Trans(Sym(current), current): rebuilt = rebuilt. - let sym_p = sym_action.lookup_or_insert(state.raw_exec_state(), &[current])?; - let refl = trans_action.lookup_or_insert(state.raw_exec_state(), &[sym_p, current])?; + let sym_p = mint_proof_row(state, &sym_action, prim.id_counter, &[current]); + let refl = mint_proof_row(state, &trans_action, prim.id_counter, &[sym_p, current]); cproof_action.insert(state.raw_exec_state(), [rebuilt, refl].into_iter()); } diff --git a/egglog/src/proofs/proof_encoding.md b/egglog/src/proofs/proof_encoding.md index 18ad8e16..0e6f610b 100644 --- a/egglog/src/proofs/proof_encoding.md +++ b/egglog/src/proofs/proof_encoding.md @@ -1,32 +1,34 @@ Rewrites an egglog program to use an encoding for equality tracking, optionally including proof tracking. -# Term Encoding - -The job of the term encoding is to *remove all calls to union* in the egglog program. -This makes proof production easier, since all equality reasoning is explicit and - can be instrumented with proof tracking. -The term encoding adds an explicit union-find structure per sort, and maintains it via - rules that run during scheduled maintenance. -The union-find for a sort is a function `UF_` that maps each term to its - parent; a term with no entry is its own representative. -Unioning two terms is a `set` making one the parent of the other; the function's `:merge` - resolves the case where the term already had a different parent. -For efficiency, every constructor becomes two tables: - a term table that stores the actual terms, and a view table mapping canonicalized - children to the e-class representative (the leader term). -The encoding uses the same shapes with and without proof tracking: - union-find and view rows carry a proof column, which is `()` (of sort `Unit`) - when proofs are off. -The term encoding enables proof tracking, done at the - same time in this file. -The encoding keeps the operational semantics equivalent to the standard encoding (for the -subset of commands that are currently supported). +# Overview -The transformation is triggered when an `EGraph` is created with -[`EGraph::new_with_term_encoding`](crate::EGraph::new_with_term_encoding) or -converted via [`EGraph::with_term_encoding_enabled`](crate::EGraph::with_term_encoding_enabled). +The job of the term encoding is to *remove all calls to union* in the egglog +program. This makes proof production easier, since all equality reasoning is +explicit and can be instrumented with proof tracking. The encoding replaces +egglog's built-in congruence and rebuilding with an explicit, per-sort +union-find and view tables maintained by ordinary rules. -Consider a tiny program that defines a pure arithmetic helper and checks a fact about it: +The transformation is triggered when an `EGraph` is created with +[`EGraph::new_with_term_encoding`](crate::EGraph::new_with_term_encoding) (no +proofs), [`EGraph::new_with_proofs`](crate::EGraph::new_with_proofs), or by +converting an existing one via +[`EGraph::with_term_encoding_enabled`](crate::EGraph::with_term_encoding_enabled). +The same shapes are used with and without proofs: union-find and view rows carry +a proof column that is `()` (of sort `Unit`) when proofs are off, and a real +`Proof` when they are on. + +The rest of this document is organized as: + +- **[Data structures](#data-structures)** — the per-sort union-find and the + per-constructor term relation and view that every command reads and writes. +- **[Actions](#actions)** — how term construction, `union`, and `delete` lower, + including how a nested term is built over canonical ids with proofs. +- **[Queries](#queries)** — how rule bodies and `check`/`prove` read the views. +- **[Rebuilding](#rebuilding)** — the maintenance rules that keep views and the + union-find canonical, and the schedule that runs them. +- **[Globals](#globals)** and **[Containers](#containers)**. + +We use a running example throughout: ```text (sort Math) @@ -37,34 +39,15 @@ Consider a tiny program that defines a pure arithmetic helper and checks a fact :name "commutativity") (run 1) (check (= (Add 1 2) (Add 2 1))) - (delete (Add 1 2)) ``` -Lowering the program with the term encoding expands to a bunch of new egglog, which we'll show (most of) in pieces. - -```text -(ruleset parent) -(ruleset rebuilding) -(ruleset rebuilding_cleanup) -(ruleset delete_subsume_ruleset) -``` - -*The new rulesets* orchestrate path compression on the per-sort union-find (`parent`), -rebuild-time congruence (`rebuilding` + `rebuilding_cleanup`), and deferred deletions/subsumptions (`delete_subsume_ruleset`). +Internal names generated by the encoding are shown here without their `__` +prefix (e.g. `AddView`, not `__AddView`), and fresh ids are given readable names. -```text -(run-schedule - (seq - (saturate - rebuilding_cleanup ;; clean up merged rows - (saturate parent) ;; flatten union-find chains via path compression - rebuilding) ;; find new equalities via congruence - delete_subsume_ruleset)) ;; process deletions/subsumptions -``` +# Data structures -*In-between* the original program's commands, the term encoding - runs these rulesets to maintain egglog's invariants. +## Union-find ```text (sort Math :internal-uf UF_Math) @@ -72,39 +55,30 @@ rebuild-time congruence (`rebuilding` + `rebuilding_cleanup`), and deferred dele :merge ((set (UF_Math (ordering-max old0 new0)) (values (ordering-min old0 new0) ())) (values (ordering-min old0 new0) ())) :unextractable :internal-hidden :internal-identity-vals 1) -(rule ((= (values b pb) (UF_Math a)) - (= (values c pc) (UF_Math b)) - (!= b c)) - ((set (UF_Math a) (values c ()))) - :ruleset parent :name "uf_path_compress") ``` -*The union-find* for each sort is the function `UF_`, mapping each term to its parent - (plus the proof column, `()` here). -A term with no row is its own representative, so `UF_` acts as an identity-on-miss lookup. -To union `a` and `b`, the encoding runs - `(set (UF_ (ordering-max a b)) (values (ordering-min a b) ()))`. -If the key already had a different parent, the `:merge` action block runs: - the key keeps the smaller of the two parents (the merge result), - and the `set` in the block unions the larger parent with the smaller one, - since both are equal to the key. -The `:internal-identity-vals 1` annotation marks the parent column as the row's identity: - a merge whose parent is unchanged keeps the existing row without running the block. -Without it, re-setting an existing edge would run the block and stage the same union - again, forever. - -*Union-find rules:* -The only maintenance rule is path compression (in the `parent` ruleset), which flattens - `a -> b -> c` chains to `a -> c`. -We use the `ordering-max` and `ordering-min` egglog primitives - to define an arbitrary ordering on terms based on insertion order, - so that we can deterministically choose which term becomes the parent - in the union-find structure. +The union-find for each sort is the function `UF_`, mapping each term to +its parent (plus the proof column, `()` here). A term with no row is its own +representative, so `UF_` is an identity-on-miss lookup. To union `a` and +`b`, the encoding runs +`(set (UF_ (ordering-max a b)) (values (ordering-min a b) ()))`. If the key +already had a different parent, the `:merge` block keeps the smaller of the two +parents and `set`s the larger parent's edge to the smaller one (both are equal to +the key). `ordering-max`/`ordering-min` impose an arbitrary but deterministic +order (by insertion) so the parent choice is stable. + +`:internal-identity-vals 1` marks the parent column as the row's identity: a +merge whose parent is unchanged keeps the existing row without running the block, +so re-setting an existing edge does not re-stage the same union forever. + +## Term relation and view +Each constructor expands to a **term relation** (`Add`), a **view** (`AddView`), +and deferred-deletion helpers: ```text (sort view) -(constructor Add (i64 i64) Math :unextractable :internal-hidden) +(function Add (i64 i64 Math) Unit :no-merge :unextractable :internal-hidden) (function AddView (i64 i64) (Math Unit) :merge ((set (UF_Math (ordering-max old0 new0)) (values (ordering-min old0 new0) ())) (values (ordering-min old0 new0) ())) @@ -113,80 +87,220 @@ We use the `ordering-max` and `ordering-min` egglog primitives (constructor to_subsume_Add (i64 i64) view :internal-hidden) ``` -Each constructor in the original program is expanded to - a term table (`Add`), a view table (`AddView`), and helpers for deferred deletion/subsumption - (`to_delete_Add`, `to_subsume_Add`). -The view table maps a term's canonicalized children to `(values eclass proof)`: - the representative term for its e-class, plus the proof column. -A canonicalized term has representative terms for its children. -Two view rows conflicting on the same children are congruent, so the view's `:merge` - resolves congruence directly: it keeps the smaller e-class and unions the two - e-classes in `UF_` — no congruence rule is needed. -The view tables are kept up to date during rebuilding. +The term relation `Add(child0, child1, eclass)` stores every application as a +row whose last column is the term's own id (minted with `get-fresh!`); nothing is +ever removed from it, which lets proofs refer to terms even after they leave the +e-graph. The **view** is a functional dependency `children -> (eclass, proof)` +mapping a term's *canonicalized* children to its e-class representative. Two view +rows that collide on the same children are congruent, so the view's `:merge` +resolves congruence directly — it keeps the smaller e-class and unions the two in +`UF_`, so no separate congruence rule is needed. All queries read the view; +the term relation is write-only after creation. + +## Proof tables (proof mode) + +With proofs enabled, the encoding first emits a header defining the proof format +(corresponding to [`RawProof`](crate::proofs::RawProof); see +`proof_encoding_helpers.rs`): the `Proof`, `Ast`, and `ProofList` sorts and the +proof constructors `Rule`, `Fiat`, `Trans`, `Sym`, `Congr`, `Merge…`, +`ContainerNormalize`, `Eval`, plus `AstMath` (one `Ast` per sort) and: ```text -(rule ((= (values v4 v5) (AddView c0 c1)) - (= (values v6 v7) (UF_Math v4)) - (!= v4 v6)) - ((set (AddView c0 c1) (values v6 ()))) - :ruleset rebuilding :name "rebuild_rule" :internal-include-subsumed) +(function MathProof (Math) Proof :merge old :unextractable :internal-hidden) ``` -For each constructor, we add rebuild rules that keep the view canonical, - *fanned out* one per eq-sort column. -`Add`'s `i64` children are not eq-sorts, so its only rebuild rule updates the e-class: - when the e-class has a `UF_` parent, the rule re-sets the row with the leader - (the view's `:merge` keeps the smaller). -A rule for an eq-sort child instead re-keys the row: it `set`s the view at the - canonicalized children and deletes the stale row. -Because `UF_` has no row for a canonical term (identity-on-miss), a column already at its - leader simply doesn't match, so no self-loops or default lookups are needed. +`MathProof` records, for each term `t`, a proof of the proposition `t = t` +(oldest kept). The union-find and view keep the same shape, but their proof +column now carries a real `Proof`: ```text -(function v3 () Math :no-merge :unextractable :internal-let) -(set (v3) (Add 1 2)) -(set (AddView 1 2) (values (v3) ())) +(function UF_Math (Math) (Math Proof) + :merge ((let hi_pf_ (proof-of-max old0 old1 new0 new1)) + (let lo_pf_ (proof-of-min old0 old1 new0 new1)) + (set (UF_Math (ordering-max old0 new0)) + (values (ordering-min old0 new0) (Trans (Sym hi_pf_) lo_pf_))) + (values (ordering-min old0 new0) lo_pf_)) + :unextractable :internal-hidden :internal-identity-vals 1) ``` -Above is the desugaring for `(Add 1 2)`. -We add to both view and term tables whenever we evaluate - a constructor or function application. -The new term needs no `UF_` entry: with identity-on-miss, a term with no row is already - its own representative. -It's straightforward except for global variables. -Since global variables are not allowed after this pass, - we use functions with no arguments to represent them - (see globals section below). +If term `k` has parent `p`, `(UF_Math k)` returns `(values p proof)` where `proof` +proves `k = p` (the key on the left). `proof-of-min`/`proof-of-max` pair the +proof with the smaller/larger parent; the displaced edge stores +`Trans (Sym hi_pf_) lo_pf_` (proving `larger = smaller`). The view's proof column +instead proves `eclass = f(children)` (the eclass on the left), so its `:merge` +composes `Trans hi_pf_ (Sym lo_pf_)` — flipped relative to the union-find's. +# Actions + +## Building a term + +Evaluating a constructor application adds to both the term relation and the view. +Top-level `(Add 1 2)` lowers to (globals `x`, `y` name the two ids; see +[Globals](#globals)): + +```text +(set (x) (get-fresh! "Math")) ;; mint a fresh id for the new term +(set (Add 1 2 (x)) ()) ;; the term-relation row +(set (y) (set-if-empty-AddView! 1 2 (x) ())) ;; intern into the view; (y) is canonical +``` + +`get-fresh!` mints a new id; `set-if-empty-!` interns the application into +its view and returns the view's **existing** e-class if the term was already +there, so `(y)` is always the canonical id. The new term needs no `UF_` +row — identity-on-miss makes it its own representative. + +## Union in a rule + +The commutativity rule builds `(Add b a)` and unions it with `(Add a b)`: ```text -(rule ((= (values v5 v6) (AddView a b))) - ((let v7 (Add a b)) - (set (AddView a b) (values v7 ())) - (let v8 (Add b a)) - (set (AddView b a) (values v8 ())) - (set (UF_Math (ordering-max v7 v8)) (values (ordering-min v7 v8) ()))) +(rule ((= (values e p) (AddView a b))) + ((let ab (get-fresh! "Math")) + (set (Add a b ab) ()) + (let ab_canon (set-if-empty-AddView! a b ab ())) + (let ba (get-fresh! "Math")) + (set (Add b a ba) ()) + (let ba_canon (set-if-empty-AddView! b a ba ())) + (set (UF_Math (ordering-max ab_canon ba_canon)) + (values (ordering-min ab_canon ba_canon) ()))) :name "commutativity") ``` -Here we have the instrumented commutativity rule. -The query uses the view table to find the canonical e-node. -The actions add to the term and view tables, then add an equality to the union-find. -We add the equality with a `set` on `UF_`, using the `ordering-max` and - `ordering-min` egglog primitives to deterministically choose the parent. +The body query matches an `Add` row and binds `a` and `b` to its **canonical** +children (a view read; see [Queries](#queries)). The head then builds the two +terms and unions them: + +- **Build a term.** `(let ab (get-fresh! "Math"))` mints a fresh id and + `(set (Add a b ab) ())` records the term-relation row for that id. `ab` is that + specific new node. +- **Canonicalize it.** `(set-if-empty-AddView! a b ab ())` interns the + application into the view keyed by its children `a b`. If a row for `(Add a b)` + already exists it returns that row's e-class; otherwise it installs `ab`. Either + way `ab_canon` is the *canonical* e-class for `(Add a b)`, so nothing downstream + ever sees the raw `ab`. +- **Union.** `(set (UF_Math (ordering-max ab_canon ba_canon)) (values (ordering-min …) ()))` + points the larger of the two canonical e-classes at the smaller, the union-find + convention from [Data structures](#union-find). `ordering-max`/`min` make the + choice deterministic. + +So even the term-mode encoding already mints-then-canonicalizes every built term; +proofs (next) just thread an equality alongside each of those steps. + +## Building nested terms with proofs + +With proofs on, building a *nested* term additionally threads a proof from the +term as the rule built it to its canonical form, so the final `union` can record +a correct equality proof. Trace this rule: + +```text +(rule ((Seed a b c rewrite_var)) + ((union rewrite_var (Neg (Add a (Add b c)))))) +``` + +The head first **flattens** to one constructor application per step: + +```text +(let d (Add b c)) +(let e (Add a d)) +(let f (Neg e)) +(union rewrite_var f) +``` + +The body match binds `a b c rewrite_var` and yields the rule's premise proof, +collected into a one-element list `prems = (PCons body_proof (PNil))`; every proof +minted below is justified by `(Rule rule_name prems lhs rhs)`. Term, AST, and +proof nodes are all relations, so a new node is a fresh id plus a row `set` +(written inline below — e.g. `(Rule …)` means "mint a proof id and `set` its +`Rule` row"). + +For each subterm we track up to three ids and the proofs between them: + +- **`e`** — the *natural* term, built with its children's as-built ids. +- **`e'`** — the same term over **canonical children** (each child replaced by its + representative). It differs from `e` only when a child moved, and the proof + `e_to_e'` is a `Congr` rewriting those children. +- **`e''`** — the view's **representative** for `e'`, returned by `set-if-empty`; + the view supplies the proof `e'_to_e''`. + +The connector handed up to the parent is `e_to_e''` (their composition). The +natural node `e` is deliberately never interned, so its `Rule` proof keeps +pointing at the shape the rule head wrote. + +### Line 1 — `(let d (Add b c))` + +`b` and `c` are already canonical (they came from the body match), so there is no +child to rewrite: `d' = d`, and the connector is just the view proof. + +```text +;; natural d = (Add b c), with its `d = d` rule proof +(let d (get-fresh! "Math")) +(set (Add b c d) ()) +(let d_prf (Rule rule_name prems (AstMath d) (AstMath d))) +(set (MathProof d) d_prf) + +;; intern (Add b c); d' is the representative the view returns +(let d' (set-if-empty-AddView! b c d d_prf)) +(let d'_prf (view-proof-AddView b c …)) ;; proves `d' = (Add b c)` +(let d_to_d' (Trans d_prf (Sym d'_prf))) ;; `d = d'` +``` + +### Line 2 — `(let e (Add a d))` + +`d` was canonicalized to `d'`, so the natural `e = (Add a d)` is rewritten to +`e' = (Add a d')` with a `Congr` at child index 1, then interned. + +```text +;; natural e = (Add a d), over d's as-built id +(let e (get-fresh! "Math")) +(set (Add a d e) ()) +(let e_prf (Rule rule_name prems (AstMath e) (AstMath e))) +(set (MathProof e) e_prf) + +;; e' = (Add a d'): rewrite child 1 (d -> d') +(let e_to_e' (Congr e_prf 1 d_to_d')) ;; `e = e'` + +;; intern (Add a d'); e'' is the representative +(let e'' (set-if-empty-AddView! a d' e' e_to_e')) +(let e'_to_e'' (view-proof-AddView a d' …)) ;; proves `e'' = (Add a d')` +(let e_to_e'' (Trans e_to_e' e'_to_e'')) ;; connector `e = e''` +``` + +### Line 3 — `(let f (Neg e))` + +Same shape with one child: rewrite `e -> e''` at index 0. +```text +(let f (get-fresh! "Math")) +(set (Neg e f) ()) +(let f_prf (Rule rule_name prems (AstMath f) (AstMath f))) +(set (MathProof f) f_prf) + +(let f_to_f' (Congr f_prf 0 e_to_e'')) ;; f' = (Neg e''), `f = f'` + +(let f'' (set-if-empty-NegView! e'' f' f_to_f')) +(let f'_to_f'' (view-proof-NegView e'' …)) ;; proves `f'' = (Neg e'')` +(let f_to_f'' (Trans f_to_f' f'_to_f'')) ;; connector `f = f''` +``` +### The union — `(union rewrite_var f)` +The rule justifies `rewrite_var = f` directly (over the natural `f`); composing +with the `f = f''` connector gives `rewrite_var = f''`. The edge is oriented to +the union-find's `larger -> smaller` convention with `proof-of-max`/`proof-of-min`. ```text -(check (= (values v9 v10) (AddView 1 2)) - (= (values v11 v12) (AddView 2 1)) - (= v9 v11)) +(let rw_to_f (Rule rule_name prems (AstMath rewrite_var) (AstMath f))) +(let rw_to_f'' (Trans rw_to_f f_to_f'')) ;; `rewrite_var = f''` +(set (UF_Math (ordering-max rewrite_var f'')) + (values (ordering-min rewrite_var f'') )) ``` -All queries use the view tables, including check commands. -This query checks that the e-class representatives for `(Add 1 2)` and `(Add 2 1)` are equal, - ensuring they share the same e-class. +The discipline is the same at every level: build the natural term, `Congr` each +child that moved to its representative, intern with `set-if-empty` to get the +representative id, and hand a `natural = representative` connector up to the +parent. Only representative ids ever reach the view and union-find. + +## Delete and subsume ```text (rule ((to_delete_Add c0 c1) @@ -199,154 +313,125 @@ This query checks that the e-class representatives for `(Add 1 2)` and `(Add 2 1 ((subsume (AddView c0 c1))) :ruleset delete_subsume_ruleset :name "delete_rule_subsume") -(to_delete_Add 1 2) +(to_delete_Add 1 2) ;; lowering of (delete (Add 1 2)) ``` -Finally, deletions and subsumptions are deferred via helper tables. -For every constructor, we add a `to_delete_` and `to_subsume_` table. -When a deletion or subsumption is requested, we add to these tables. -During rebuilding, we process these tables to actually delete or subsume the requested terms. -View functions support subsumption (via the `:internal-term-constructor` annotation). -We only need to delete or subsume from the view tables, - since the term tables are not used for queries. -This has the added benefit of allowing us to keep terms around - for proof tracking even after they are deleted from the e-graph. +Deletions and subsumptions are deferred: `(delete (Add 1 2))` records +`(to_delete_Add 1 2)`, and the `delete_subsume_ruleset` (run during maintenance) +removes the view row. Only the view is deleted/subsumed — the term relation is +never queried, so keeping its rows lets proofs still refer to deleted terms. +# Queries -# Globals +All queries — rule bodies, `check`, and `prove` — read the **view**, never the +term relation. A view read binds both the e-class and the proof column: -*Before the term encoding*, egglog desugars all global - variables to constructors with the `proof_global_remover.rs` pass. -This makes the encoding simpler and makes it so the backend - need not worry about globals. -The above program doesn't have any global variables, so it stays the same. -A different program like this one: ```text -(sort Math) -(constructor Add (i64 i64) Math) -(let g1 (Add 1 2)) -(rule ((= g1 (Add 2 3)) - ((Add 3 4)))) +(= (values e p) (AddView a b)) ``` -Would desugar to this before term encoding: +A nested term flattens into one view read per subterm, joined on shared e-class +variables. The `check` in the running example expands to: + ```text -(sort Math) -(constructor Add (i64 i64) Math) -(constructor g1 () Math) -(union (g1) (Add 1 2)) -(rule ((= (g1) (Add 2 3))) - ((Add 3 4))) +(check (= (values e1 p1) (AddView 1 2)) + (= (values e2 p2) (AddView 2 1)) + (= e1 e2)) ``` +This checks that the representatives of `(Add 1 2)` and `(Add 2 1)` are the same +e-class. In proof mode the read also binds the view's proof (`p1`, `p2`); a plain +`check` discards it, while a rule body or `prove` uses it: the fact's proof is the +view's proof composed with a `Congr` for each child that carries its own subproof, +mirroring the construction side. +# Rebuilding -# Proof Tracking - -During term encoding, if proof tracking is enabled, - we also instrument the program to track proofs of equalities. -We'll continue with our example from above, showing the additions - for proof tracking. - -Original program snippet is +Between the original program's commands, the encoding runs maintenance rules that +restore the invariants egglog normally maintains during rebuilding: ```text -(sort Math) -(constructor Add (i64 i64) Math) -(Add 1 2) -(rule ((Add a b)) - ((union (Add a b) (Add b a))) - :name "commutativity") -(run 1) -(check (= (Add 1 2) (Add 2 1))) +(ruleset parent) ;; path compression on the union-find +(ruleset rebuilding) ;; re-canonicalize view rows; resolve congruence +(ruleset rebuilding_cleanup) ;; drop rows merged away +(ruleset delete_subsume_ruleset) + +(run-schedule + (seq + (saturate + (run rebuilding_cleanup) + (saturate (run parent)) + (run rebuilding)) + (run delete_subsume_ruleset))) ``` +## Path compression -The encoding with proof tracking adds a proof header before the rest of the program. -The header defines the proof format corresponding to [`RawProof`](crate::proofs::RawProof) in Rust. -See the proof header in `proof_encoding_helpers.rs` for details. +The only union-find rule flattens `a -> b -> c` chains to `a -> c` (composing the +two edge proofs with `Trans` in proof mode): ```text -(function MathProof (Math) Proof :merge old :unextractable :internal-hidden) +(rule ((= (values b pb) (UF_Math a)) + (= (values c pc) (UF_Math b)) + (!= b c)) + ((set (UF_Math a) (values c ()))) + :ruleset parent :name "uf_path_compress") ``` -Every sort gets a proof table storing - a proof for that term. -The proof proves a proposition `t = t` for - input term `t`. -We store the oldest proof currently. +## Keeping the view canonical -When proof tracking is enabled, the union-find keeps the same shape, but its proof -column carries a real `Proof` instead of `()`: +For each constructor, the encoding fans out one rebuild rule per rebuildable +column. `Add`'s `i64` children are not eq-sorts, so its only rule re-canonicalizes +the **e-class** column: when the e-class has a `UF_` parent, re-`set` the row +with the leader (the view's `:merge` keeps the smaller). ```text -(function UF_Math (Math) (Math Proof) - :merge ((let hi_pf_ (proof-of-max old0 old1 new0 new1)) - (let lo_pf_ (proof-of-min old0 old1 new0 new1)) - (set (UF_Math (ordering-max old0 new0)) - (values (ordering-min old0 new0) (Trans (Sym hi_pf_) lo_pf_))) - (values (ordering-min old0 new0) lo_pf_)) - :unextractable :internal-hidden :internal-identity-vals 1) +(rule ((= (values e pe) (AddView c0 c1)) + (= (values leader ple) (UF_Math e)) + (!= e leader)) + ((set (AddView c0 c1) (values leader ()))) + :ruleset rebuilding :name "rebuild_rule" :internal-include-subsumed) ``` -If term `k` has parent `p`, `(UF_Math k)` returns `(values p proof)` where `proof` -proves `k = p` (the key on the left). The `:merge` is the term-mode merge with the -proofs riding along: `proof-of-min`/`proof-of-max` return the proof paired with the -smaller/larger parent, the displaced edge stores `Trans (Sym hi_pf_) lo_pf_` -(proving `larger parent = smaller parent`), and the smaller parent keeps its own -proof. Path compression flattens chains via `Trans`. +A rule for an **eq-sort child** instead re-keys the row: it `set`s the view at the +canonicalized children and deletes the stale row (proof mode composes a `Congr` at +that child index). Because `UF_` has no row for a canonical term +(identity-on-miss), a column already at its leader simply does not match, so no +self-loops or default lookups are needed. Subsumption markers +(`to_subsume_`) are likewise re-keyed to their leaders so a subsumed +row stays subsumed after its children move. +# Globals -Similarly, the constructor view's proof column carries a proof of the row itself: +*Before the term encoding*, egglog desugars all global variables to nullary +functions with the `remove_globals.rs` pass, so the backend need not treat them +specially. A program like: ```text -(function AddView (i64 i64) (Math Proof) - :merge ((let hi_pf_ (proof-of-max old0 old1 new0 new1)) - (let lo_pf_ (proof-of-min old0 old1 new0 new1)) - (set (UF_Math (ordering-max old0 new0)) - (values (ordering-min old0 new0) (Trans hi_pf_ (Sym lo_pf_)))) - (values (ordering-min old0 new0) lo_pf_)) - :internal-term-constructor Add :internal-identity-vals 1) +(sort Math) +(constructor Add (i64 i64) Math) +(let g1 (Add 1 2)) +(rule ((= g1 (Add 2 3))) + ((Add 3 4))) ``` -The `proof` in `(values eclass proof)` proves `eclass = f(children)` (the eclass on -the left), which is why the `Trans`/`Sym` composition is flipped relative to the -union-find's. - +desugars to: ```text -(rule (;; query the view for its eclass and proof (proof that eclass = (Add a b)) - (= (values v11 v12) (AddView a b))) - (;; proof list, one per line of the original query - (let v13 (PCons v12 (PNil))) - - (let v14 (Add a b)) - ;; Proof that Add a b = Add a b - (let v15 (Rule "commutativity" v13 (AstMath v14) (AstMath v14))) - ;; Set the proof for Add a b - (set (MathProof v14) v15) - ;; Update the FD view: children -> (eclass, proof) - (set (AddView a b) (values v14 v15)) - - (let v16 (Add b a)) - ;; Proof that Add b a = Add b a - (let v17 (Rule "commutativity" v13 (AstMath v16) (AstMath v16))) - (set (MathProof v16) v17) - (set (AddView b a) (values v16 v17)) - - ;; Union (Add a b) and (Add b a), storing a proof of their equality. - (set (UF_Math (ordering-max v14 v16)) - (values (ordering-min v14 v16) - (Rule "commutativity" v13 (AstMath (ordering-max v14 v16)) (AstMath (ordering-min v14 v16)))))) - :name "commutativity") +(sort Math) +(constructor Add (i64 i64) Math) +(function g1 () Math :internal-let) +(set (g1) (Add 1 2)) +(rule ((= (g1) (Add 2 3))) + ((Add 3 4))) ``` -Instrumented rules with proof tracking query the view function directly - (since the proof is its output column), then construct proofs for each action. -The structure is the same as term mode — view and UF updates both use `set` — - but the values stored carry `Proof` terms instead of `()`. -For nested terms, congruence proofs are built to ensure - the proof terms match the original queries. +The global is a nullary `:internal-let` function `set` to its value (no `union` +at the top level), so it gets a view and rebuild rules like any other function; +references to `g1` become the lookup `(g1)`. The `(Add 1 2)` and `(Add 3 4)` +constructions above are the same [term construction](#building-a-term) shown +earlier — the term-building sites in the encoding wrap their minted ids in such +`:internal-let` functions. # Containers diff --git a/egglog/src/proofs/proof_encoding.rs b/egglog/src/proofs/proof_encoding.rs index ce0d1f83..6fcc6be0 100644 --- a/egglog/src/proofs/proof_encoding.rs +++ b/egglog/src/proofs/proof_encoding.rs @@ -4,10 +4,35 @@ use crate::proofs::proof_encoding_helpers::{EncodingNames, Justification}; use crate::typechecking::FuncType; use crate::*; +/// Term-construction side channel (proof mode): maps a built term's canonical +/// e-class var to `(natural e-class var, connector proof var)`, where the +/// connector proves `natural = canonical`. A parent term reads its children's +/// entries to build the natural term and its `Congr` connector; the root `union` +/// and a global's `global_value_proof` read it to anchor on the natural form. +/// +/// Scoped to a single generated program — a rule, a top-level action, or a +/// custom function's merge body — so each such scope threads a fresh, local map +/// through the action/term builders rather than sharing one long-lived field. +pub(crate) type NatConn = HashMap)>; + +/// Which FD-view value column [`ProofInstrumentor::fd_value_rebuild_rule`] rebuilds. +enum ValueRebuild { + /// The value is the term's e-class (constructors and globals): re-`set` and let + /// the congruence `:merge` keep the min. + Eclass, + /// A custom `:merge` function's eq-sort output at child index `out_idx`: + /// delete-then-reinsert so the re-`set` doesn't re-run the user merge. + CustomOutput { out_idx: usize }, +} + // TODO refactor so that encoding state is optional on the e-graph, ProofNames not optional on EncodingState. Then we don't have to clone proof names everywhere. #[derive(Clone)] pub(crate) struct EncodingState { pub uf_parent: HashMap, + /// Maps eq-sort name -> its auxiliary union-find `UF_Aux_` (natural + /// e-class id -> canonical dedup id + connector proof). Set from the + /// `:internal-uf-aux` annotation; read by container rebuild for elements. + pub uf_aux_parent: HashMap, /// Maps sort name -> proof function name (set from :internal-proof-func annotation). pub proof_func_parent: HashMap, /// Maps container sort name -> the name of its registered container-rebuild @@ -17,11 +42,6 @@ pub(crate) struct EncodingState { /// Maps container sort name -> the name of its registered proof-producing /// container-rebuild primitive (`ContainerRebuildProof`). Proof mode only. pub container_rebuild_proof_name: HashMap, - /// Function name -> (hidden current-value function, input arity). The - /// current function uses the original eager backend merge, so cleanup can - /// discard stale proof-view candidates whenever the current value already - /// has a proof witness. - pub merge_current: HashMap, pub term_header_added: bool, // TODO this is very ugly- we should separate out a typechecking struct // since we didn't need an entire e-graph @@ -40,10 +60,10 @@ impl EncodingState { pub(crate) fn new(symbol_gen: &mut SymbolGen) -> Self { Self { uf_parent: HashMap::default(), + uf_aux_parent: HashMap::default(), proof_func_parent: HashMap::default(), container_rebuild_name: HashMap::default(), container_rebuild_proof_name: HashMap::default(), - merge_current: HashMap::default(), term_header_added: false, original_typechecking: None, proofs_enabled: false, @@ -88,12 +108,16 @@ impl<'a> ProofInstrumentor<'a> { } /// Mark two things as equal, adding proof if proofs are enabled. + /// Emits any proof-relation mints onto `stmts` and returns the `(set @UF ...)` + /// action; the caller must push the mints (already on `stmts`) before it. pub(crate) fn union( &mut self, + stmts: &mut Vec, type_name: &str, lhs: &str, rhs: &str, justification: &Justification, + nat_conn: &NatConn, ) -> String { let uf_name = self.uf_name(type_name); let smaller = format!("(ordering-min {lhs} {rhs})"); @@ -101,29 +125,131 @@ impl<'a> ProofInstrumentor<'a> { // `@UF : (S) -> (S, {Unit|Proof})` is keyed by the larger endpoint; its // `:merge` resolves conflicting parents. The second column carries a proof // `larger = smaller` (`()` in term mode). - let proof = if !self.egraph.proof_state.proofs_enabled { - "()".to_string() - } else { - let to_ast_constructor = self - .proof_names() - .sort_to_ast_constructor - .get(type_name) - .unwrap(); - let rule_constructor = &self.proof_names().rule_constructor; - let fiat_constructor = &self.proof_names().fiat_constructor; + if !self.egraph.proof_state.proofs_enabled { + return format!("(set ({uf_name} {larger}) (values {smaller} ()))"); + } + + let to_ast_constructor = self + .proof_names() + .sort_to_ast_constructor + .get(type_name) + .unwrap() + .clone(); + let proof_sort = self.proof_sort(); + let ast_sort = self.proof_names().ast_sort.clone(); + let rule_constructor = self.proof_names().rule_constructor.clone(); + let fiat_constructor = self.proof_names().fiat_constructor.clone(); + + // Natural id + connector (`natural = deduped`) for each operand, if it was + // a canonicalized constructor term. Leaves / body matches have neither. + let lhs_info = nat_conn.get(lhs).cloned(); + let rhs_info = nat_conn.get(rhs).cloned(); + let lhs_conn = lhs_info.as_ref().and_then(|(_, c)| c.clone()); + let rhs_conn = rhs_info.as_ref().and_then(|(_, c)| c.clone()); + + // Neither operand was a canonicalized constructor term (no connector), so + // both e-classes' ASTs are stable: build the edge proof directly over them. + if lhs_conn.is_none() && rhs_conn.is_none() { + let proof = match justification { + Justification::Rule(rule_name, proof_list) => { + let a_larger = self.mint(stmts, &to_ast_constructor, &larger, &ast_sort); + let a_smaller = self.mint(stmts, &to_ast_constructor, &smaller, &ast_sort); + self.mint( + stmts, + &rule_constructor, + &format!("{rule_name} {proof_list} {a_larger} {a_smaller}"), + &proof_sort, + ) + } + Justification::Fiat => { + let a_larger = self.mint(stmts, &to_ast_constructor, &larger, &ast_sort); + let a_smaller = self.mint(stmts, &to_ast_constructor, &smaller, &ast_sort); + self.mint( + stmts, + &fiat_constructor, + &format!("{a_larger} {a_smaller}"), + &proof_sort, + ) + } + Justification::MergeIdx(..) | Justification::MergeRow(..) => panic!( + "Merge functions do not include union actions, so proof should not be by merge" + ), + }; + return format!("(set ({uf_name} {larger}) (values {smaller} {proof}))"); + } + + // A canonicalized operand's deduped e-class may already be unioned with a + // differently-shaped term, so its AST floats. Build the base equality over + // the *natural* forms (ASTs pinned to the enode the rule built), then route + // each deduped e-class to a shared natural form and orient the edge to + // `larger = smaller` with proof-of-max/min. + let nat_of = |info: &Option<(String, Option)>, dedup: &str| { + info.as_ref() + .map(|(n, _)| n.clone()) + .unwrap_or_else(|| dedup.to_string()) + }; + let lhs_nat = nat_of(&lhs_info, lhs); + let rhs_nat = nat_of(&rhs_info, rhs); + + let base_proof = { + let a_lhs = self.mint(stmts, &to_ast_constructor, &lhs_nat, &ast_sort); + let a_rhs = self.mint(stmts, &to_ast_constructor, &rhs_nat, &ast_sort); match justification { - Justification::Rule(rule_name, proof_list) => format!( - "({rule_constructor} {rule_name} {proof_list} ({to_ast_constructor} {larger}) ({to_ast_constructor} {smaller}))" + Justification::Rule(rule_name, proof_list) => self.mint( + stmts, + &rule_constructor, + &format!("{rule_name} {proof_list} {a_lhs} {a_rhs}"), + &proof_sort, ), - Justification::Fiat => format!( - "({fiat_constructor} ({to_ast_constructor} {larger}) ({to_ast_constructor} {smaller}))" + Justification::Fiat => self.mint( + stmts, + &fiat_constructor, + &format!("{a_lhs} {a_rhs}"), + &proof_sort, ), - Justification::Merge(_func_name, _proof1, _proof2) => panic!( + Justification::MergeIdx(..) | Justification::MergeRow(..) => panic!( "Merge functions do not include union actions, so proof should not be by merge" ), } }; - format!("(set ({uf_name} {larger}) (values {smaller} {proof}))") + + let sym = self.proof_names().eq_sym_constructor.clone(); + let trans = self.proof_names().eq_trans_constructor.clone(); + // Route both operands to a shared *natural* form, then orient to the + // `larger = smaller` UF edge with proof-of-max/min. The shared form is the + // canonicalized side's natural (pinned AST), so the Trans goes through it + // rather than through the deduped e-class. + let (lhs_to_shared, rhs_to_shared) = if let Some(rc) = &rhs_conn { + let lhs_to = if let Some(lc) = &lhs_conn { + let sym_lc = self.mint(stmts, &sym, lc, &proof_sort); + self.mint( + stmts, + &trans, + &format!("{sym_lc} {base_proof}"), + &proof_sort, + ) + } else { + base_proof.clone() + }; + let rhs_to = self.mint(stmts, &sym, rc, &proof_sort); + (lhs_to, rhs_to) + } else { + let lc = lhs_conn.as_ref().unwrap(); + let lhs_to = self.mint(stmts, &sym, lc, &proof_sort); + let rhs_to = self.mint(stmts, &sym, &base_proof, &proof_sort); + (lhs_to, rhs_to) + }; + let max_pf = self.fresh_var(); + stmts.push(format!( + "(let {max_pf} (proof-of-max {lhs} {lhs_to_shared} {rhs} {rhs_to_shared}))" + )); + let min_pf = self.fresh_var(); + stmts.push(format!( + "(let {min_pf} (proof-of-min {lhs} {lhs_to_shared} {rhs} {rhs_to_shared}))" + )); + let sym_min = self.mint(stmts, &sym, &min_pf, &proof_sort); + let edge = self.mint(stmts, &trans, &format!("{max_pf} {sym_min}"), &proof_sort); + format!("(set ({uf_name} {larger}) (values {smaller} {edge}))") } /// The parent table is the database representation of a union-find datastructure. @@ -169,9 +295,14 @@ impl<'a> ProofInstrumentor<'a> { let proof_tables = if proofs { let term_proof_name = self.term_proof_name(sort_name); let add_to_ast_code = self.add_to_ast(sort_name); + // `UF_Aux_`: natural -> (canonical, connector proof). Written + // only for container elements; the container rebuild reads it. `:merge + // old` keeps the first edge (each natural is minted once). + let aux_name = self.uf_aux_name(sort_name); format!( "{add_to_ast_code} - (function {term_proof_name} ({sort_name}) {proof_type} :merge old :internal-hidden)" + (function {term_proof_name} ({sort_name}) {proof_type} :merge old :internal-hidden) + (function {aux_name} ({sort_name}) ({sort_name} {proof_type}) :merge (values old0 old1) :internal-hidden)" ) } else { String::new() @@ -183,11 +314,18 @@ impl<'a> ProofInstrumentor<'a> { let uf_merge = if proofs { let trans = self.proof_names().eq_trans_constructor.clone(); let sym = self.proof_names().eq_sym_constructor.clone(); + let proof_sort = self.proof_sort(); + let mut mints = vec![]; + let sym_pf = self.mint(&mut mints, &sym, "hi_pf_", &proof_sort); + let displaced_pf = + self.mint(&mut mints, &trans, &format!("{sym_pf} lo_pf_"), &proof_sort); + let mints_str = mints.join("\n "); format!( "((let hi_pf_ (proof-of-max old0 old1 new0 new1)) (let lo_pf_ (proof-of-min old0 old1 new0 new1)) + {mints_str} (set ({uf_name} (ordering-max old0 new0)) - (values (ordering-min old0 new0) ({trans} ({sym} hi_pf_) lo_pf_))) + (values (ordering-min old0 new0) {displaced_pf})) (values (ordering-min old0 new0) lo_pf_))" ) } else { @@ -197,11 +335,14 @@ impl<'a> ProofInstrumentor<'a> { ) }; // path compression: a->b (pb: a=b), b->c (pc: b=c) => a->c (Trans pb pc: a=c) - let compressed_proof = if proofs { + let (compressed_proof_lets, compressed_proof) = if proofs { let trans = self.proof_names().eq_trans_constructor.clone(); - format!("({trans} {pb} {pc})") + let proof_sort = self.proof_sort(); + let mut mints = vec![]; + let pf = self.mint(&mut mints, &trans, &format!("{pb} {pc}"), &proof_sort); + (mints.join("\n "), pf) } else { - "()".to_string() + (String::new(), "()".to_string()) }; let code = format!( @@ -210,7 +351,8 @@ impl<'a> ProofInstrumentor<'a> { (rule ((= (values {b} {pb}) ({uf_name} {a})) (= (values {c} {pc}) ({uf_name} {b})) (!= {b} {c})) - ((set ({uf_name} {a}) (values {c} {compressed_proof}))) + ({compressed_proof_lets} + (set ({uf_name} {a}) (values {c} {compressed_proof}))) :ruleset {path_compress_ruleset_name} :name \"{fresh_name}\") " @@ -235,10 +377,10 @@ impl<'a> ProofInstrumentor<'a> { let delete_subsume_ruleset = self.proof_names().delete_subsume_ruleset_name.clone(); let fresh_name = self.egraph.parser.symbol_gen.fresh("delete_rule"); - // A constructor's FD tuple view is keyed by children only, so match its value - // tuple to delete/subsume by key (the bridge re-reads every value column when - // subsuming a tuple-output view). - if fdecl.subtype == FunctionSubtype::Constructor { + // An FD tuple view (constructors + custom functions with a `:merge`) is keyed + // by children only, so match its value tuple to delete/subsume by key (the + // bridge re-reads every value column when subsuming a tuple-output view). + if self.is_fd_view(fdecl) { let e = self.fresh_var(); let pf = self.fresh_var(); let e2 = self.fresh_var(); @@ -275,210 +417,82 @@ impl<'a> ProofInstrumentor<'a> { ) } - /// Generate rules that run a merge function for a custom function. - /// One rule runs the merge function when two different values are present for the same children. - /// Another rule cleans up old values, necessary because the newly merged value may be equal to one of the old values. - fn handle_merge_fn( - &mut self, - fdecl: &ResolvedFunctionDecl, - child_names: &[String], - child_names_str: &str, - _view_name: &str, - rebuilding_ruleset: &str, - ) -> String { - let name = &fdecl.name; - - let merge_fn = &fdecl - .merge - .as_ref() - .unwrap_or_else(|| panic!("Proofs don't support :no-merge")); + /// Whether `fdecl`'s view uses the FD pair-valued shape `(children) -> + /// (values output proof)`, keyed on children only. Every encoded function now + /// does: constructors and globals (congruence `:merge`), custom `:merge` + /// functions (the user merge), and primitive/`Unit`-output `:no-merge` customs + /// (native `:no-merge` + `:internal-identity-vals 1`). Eq-sort-output + /// `:no-merge` — the only non-FD shape — is rejected before encoding, so this + /// is true for every function that reaches the encoder. + fn is_fd_view(&self, fdecl: &ResolvedFunctionDecl) -> bool { + fdecl.subtype == FunctionSubtype::Constructor || fdecl.subtype == FunctionSubtype::Custom + } - let current_name = self - .egraph - .parser - .symbol_gen - .fresh(&format!("{name}Current")); - self.egraph - .proof_state - .merge_current - .insert(name.clone(), (current_name.clone(), child_names.len())); + /// A global is a `:internal-let` function; in the encoding it is treated like a + /// nullary constructor (FD view, congruence merge, readable value+proof) rather + /// than a `:no-merge` custom function. + fn is_encoded_global(&self, fdecl: &ResolvedFunctionDecl) -> bool { + fdecl.internal_let + } - let fresh_name = self.egraph.parser.symbol_gen.fresh("merge_rule"); - let cleanup_name = self.egraph.parser.symbol_gen.fresh("merge_cleanup"); - let current_cleanup_name = self.egraph.parser.symbol_gen.fresh("merge_current_cleanup"); + /// Whether the function's output value *is* its e-class, so the term relation + /// needs no separate output column and the view is the congruence FD + /// `(children) -> (eclass, proof)`. Holds for constructors and encoded globals. + fn output_is_eclass(&self, fdecl: &ResolvedFunctionDecl) -> bool { + fdecl.subtype == FunctionSubtype::Constructor || self.is_encoded_global(fdecl) + } - let p1_fresh = self.egraph.parser.symbol_gen.fresh("p1"); - let p2_fresh = self.egraph.parser.symbol_gen.fresh("p2"); - let view_name = self.view_name(&fdecl.name); - let rebuilding_cleanup_ruleset = self.proof_names().rebuilding_cleanup_ruleset_name.clone(); - let input_sorts = ListDisplay(&fdecl.schema.input, " "); - let proof_query = if self.egraph.proof_state.proofs_enabled { - // View is a function with proof output; bind proof variables - format!( - "(= {p1_fresh} ({view_name} {child_names_str} old)) - (= {p2_fresh} ({view_name} {child_names_str} new)) - " - ) - } else { - // View is a function with Unit output; no need to bind the output - "".to_string() - }; - let proof_var = if self.egraph.proof_state.proofs_enabled { - self.fresh_var() + /// The `:merge` expression for a custom function's FD pair-valued view + /// `(children) -> (values output proof)`. On a children-key collision it runs + /// the user's merge body ONCE (unlike a constructor's congruence, it performs + /// no `@UF` union): `old`/`new` bind to the two colliding output columns + /// (`old0`/`new0`) and the carried view proofs to `old1`/`new1`. The result is + /// `(values merged rowproof)`, where `merged` is the (canonically-minted) merge + /// body and `rowproof` is a children-free `MergeRow` (`()` in term mode). + /// + /// Doing the merge here, rather than in a separate rule + `current` helper, + /// avoids computing it twice (which minted over-merged extra term rows). + fn custom_view_merge(&mut self, fdecl: &ResolvedFunctionDecl) -> String { + // `nat_conn` is scoped to this merge body; the body mints subterms via + // `add_term_and_view`, which threads it. + let mut nat_conn = NatConn::default(); + let name = fdecl.name.clone(); + let merge = fdecl + .merge + .as_ref() + .expect("custom FD view requires a :merge"); + + let mut body_code = vec![]; + let mut idx = 0usize; + let merged = self.instrument_merge_body( + &merge.result, + &mut body_code, + &name, + &mut idx, + &mut nat_conn, + ); + let row_proof = if self.egraph.proof_state.proofs_enabled { + let fresh = self.term_proof_for_justification( + &mut body_code, + "", + "", + &Justification::MergeRow(name.clone(), "old1".to_string(), "new1".to_string()), + ); + // Keep the proof column stable: when the merged output equals a + // colliding premise's output (as with idempotent `min`/`max`/... merges + // that keep one input), reuse that premise's existing proof so the row + // stays value-identical and the merge saturates. Otherwise the fresh + // `MergeRow` justifies the newly-computed output. (`old0`/`new0` are the + // premise outputs, `old1`/`new1` their carried view proofs.) + format!("(select-eq {merged} old0 old1 (select-eq {merged} new0 new1 {fresh}))") } else { "()".to_string() }; - let mut merge_fn_code = vec![]; - // Proof instrumentation tracks the merged *value*; a `:merge` action block's effects are - // not proof-tracked (action-block merges under proofs are unsupported). - let merge_fn_var = self.instrument_action_expr( - &merge_fn.result, - &mut merge_fn_code, - &Justification::Merge(name.clone(), p1_fresh.clone(), p2_fresh.clone()), - ); - let merge_fn_code_str = merge_fn_code.join("\n"); - let mut updated = child_names.to_vec(); - updated.push(merge_fn_var.clone()); - let term = format!("({name} {child_names_str} {merge_fn_var})"); - - let rule_proof = if self.egraph.proof_state.proofs_enabled { - let to_ast = self.fname_to_ast_name(name); - let merge_fn_constructor = self.proof_names().merge_fn_constructor.clone(); - format!( - "(let {proof_var} - ({merge_fn_constructor} \"{name}\" - {p1_fresh} - {p2_fresh} - ({to_ast} {term})))" - ) + let value = format!("(values {merged} {row_proof})"); + if body_code.is_empty() { + value } else { - "".to_string() - }; - let term_and_proof = self.update_view(name, &updated, &proof_var); - let cleanup_constructor = self.egraph.parser.symbol_gen.fresh("mergecleanup"); - let fresh_sort = self.egraph.parser.symbol_gen.fresh("mergecleanupsort"); - let output_sort = fdecl.schema.output().clone(); - - // The first runs the merge function adding a new row. - // The second deletes rows with old values for the old variable, while the third deletes rows with new values for the new variable. - format!( - "(function {current_name} ({input_sorts}) {output_sort} - :merge {merge_fn} - :unextractable - :internal-hidden) - (sort {fresh_sort}) - (constructor {cleanup_constructor} ({output_sort} {output_sort}) {fresh_sort} :internal-hidden) - (rule (({view_name} {child_names_str} old) - ({view_name} {child_names_str} new) - (!= old new) - (= (ordering-max old new) new) - {proof_query}) - ( - {merge_fn_code_str} - {rule_proof} - {term_and_proof} - ({cleanup_constructor} {merge_fn_var} old) - ({cleanup_constructor} {merge_fn_var} new) - ) - :ruleset {rebuilding_ruleset} - :name \"{fresh_name}\") - (rule (({cleanup_constructor} merged old) - ({view_name} {child_names_str} merged) - ({view_name} {child_names_str} old) - (!= merged old)) - ((delete ({view_name} {child_names_str} old))) - :ruleset {rebuilding_cleanup_ruleset} - :name \"{cleanup_name}\") - (rule ((= selected ({current_name} {child_names_str})) - ({view_name} {child_names_str} selected) - ({view_name} {child_names_str} old) - (!= selected old)) - ((delete ({view_name} {child_names_str} old))) - :ruleset {rebuilding_cleanup_ruleset} - :name \"{current_cleanup_name}\") - ", - ) - } - - /// Use native `:no-merge` for primitive outputs and compare UF leaders for eq-sort outputs. - fn handle_no_merge_fn( - &mut self, - fdecl: &ResolvedFunctionDecl, - child_names: &[String], - child_names_str: &str, - rebuilding_ruleset: &str, - ) -> String { - let name = &fdecl.name; - let output_is_eq_sort = fdecl.resolved_schema.output().is_eq_sort(); - - if !output_is_eq_sort { - let input_sorts = ListDisplay(&fdecl.schema.input, " "); - let output_sort = fdecl.schema.output(); - let current_name = self - .egraph - .parser - .symbol_gen - .fresh(&format!("{name}Current")); - self.egraph - .proof_state - .merge_current - .insert(name.clone(), (current_name.clone(), child_names.len())); - return format!( - "(function {current_name} ({input_sorts}) {output_sort} - :no-merge - :unextractable - :internal-hidden)" - ); - } - - // Distinct encoded values can already belong to the same e-class. Wait - // for their encoded UF leaders before deciding whether the conflict is real. - let view_name = self.view_name(name); - let fresh_name = self.egraph.parser.symbol_gen.fresh("no_merge_rule"); - let uf_name = self.uf_name(fdecl.resolved_schema.output().name()); - - format!( - "(rule (({view_name} {child_names_str} old) - ({view_name} {child_names_str} new) - (= (values old_leader_ old_proof_) ({uf_name} old)) - (= (values new_leader_ new_proof_) ({uf_name} new)) - (!= old_leader_ new_leader_) - (= (ordering-max old new) new)) - ((panic \"Illegal merge attempted for function {name}\")) - :ruleset {rebuilding_ruleset} - :name \"{fresh_name}\")" - ) - } - - /// Generate rules that handle merge functions. - /// For custom functions, we generate rules that run the merge function. - /// Constructors need no rule: congruence is resolved by their view's `:merge`. - fn handle_merge_or_congruence(&mut self, fdecl: &ResolvedFunctionDecl) -> String { - let child_names = fdecl - .schema - .input - .iter() - .enumerate() - .map(|(i, _)| format!("c{i}_")) - .collect::>(); - let child_names_str = child_names.join(" "); - let rebuilding_ruleset = self.proof_names().rebuilding_ruleset_name.clone(); - let view_name = self.view_name(&fdecl.name); - if fdecl.subtype == FunctionSubtype::Custom { - if fdecl.merge.is_some() { - self.handle_merge_fn( - fdecl, - &child_names, - &child_names_str, - &view_name, - &rebuilding_ruleset, - ) - } else { - self.handle_no_merge_fn(fdecl, &child_names, &child_names_str, &rebuilding_ruleset) - } - } else { - // Congruence is resolved by the constructor view's `:merge`; no rule needed. - String::new() + format!("({}\n{value})", body_code.join("\n")) } } @@ -498,39 +512,39 @@ impl<'a> ProofInstrumentor<'a> { let delete_rule = self.delete_and_subsume(fdecl); let to_delete_name = self.delete_name(&fdecl.name); let subsumed_name = self.subsumed_name(&fdecl.name); + // True when the function's output value *is* its eclass, so the term + // relation needs no separate output column and the view is the + // congruence FD `(children) -> (eclass, proof)`. Holds for constructors + // (output is the built term) and for encoded globals (a nullary Custom + // `:internal-let` function whose output is the term it aliases): both + // give term row `(children eclass)`. A Custom function returning a + // distinct value (e.g. `-> i64`) is false — it keeps an output column + // plus a fresh eclass column. + let output_is_eclass = self.output_is_eclass(fdecl); let term_sorts = format!( "{in_sorts} {}", - if fdecl.subtype == FunctionSubtype::Constructor { + if output_is_eclass { "".to_string() } else { schema.output().to_string() } ); - let view_sorts = format!("{in_sorts} {out_type}"); - let proof_constructors = self.proof_functions(fdecl, &view_sorts); - let view_sort = if fdecl.subtype == FunctionSubtype::Constructor { + let view_sort = if output_is_eclass { schema.output().clone() } else { fresh_sort.clone() }; let to_ast_view_sort = self.add_to_ast(&view_sort); - if self.egraph.proof_state.proofs_enabled { - self.egraph - .proof_state - .proof_names - .fn_to_term_sort - .insert(name.clone(), view_sort.clone()); - } - let merge_rule = self.handle_merge_or_congruence(fdecl); - // the term table has child_sorts as inputs - // the view table has child_sorts + the leader term for the eclass - // Propagate cost, unextractable, hidden, and internal_let flags from the original function - let mut term_flags = String::new(); - if let Some(cost) = fdecl.cost { - term_flags.push_str(&format!(" :cost {cost}")); - } + // Record the term's eclass sort (its `view_sort`) so the creation site + // in `add_term_and_view` knows which `get-fresh!` to mint from. Needed in + // both term and proof mode now that terms are minted, not constructed. + self.egraph + .proof_state + .proof_names + .fn_to_term_sort + .insert(name.clone(), view_sort.clone()); // View is always a function (returning Proof or Unit), with :merge old let proof_type = self.proof_type_str().to_string(); let mut view_flags = String::new(); @@ -543,13 +557,19 @@ impl<'a> ProofInstrumentor<'a> { if fdecl.internal_let { view_flags.push_str(" :internal-let"); } - // A constructor's view is a functional-dependency tuple - // `(children) -> (eclass, {Unit|Proof})` whose `:merge` resolves congruence: - // it keeps the smaller eclass and unions the two eclasses in the sort's - // `@UF`. Custom functions keep the `(children eclass) -> {Unit|Proof}` form - // with a merge rule. - let fd_view = fdecl.subtype == FunctionSubtype::Constructor; - let view_decl = if fd_view { + // The view carries the user operation's extraction cost (the term table + // is a relation and can't carry `:cost`); the extractor reads it here. + if let Some(cost) = fdecl.cost { + view_flags.push_str(&format!(" :internal-cost {cost}")); + } + // Every encoded function uses the FD pair-valued view `(children) -> + // (output, {Unit|Proof})` keyed on children only. Constructors and globals + // resolve conflicts by congruence (`:merge`); custom `:merge` functions run + // the user merge; primitive/`Unit`-output `:no-merge` customs use a native + // `:no-merge` view guarded by `:internal-identity-vals 1` (a children + // collision keeps the old row iff the output value column is unchanged, and + // panics otherwise). Eq-sort-output `:no-merge` is rejected before encoding. + let view_decl = if output_is_eclass { // Two rows conflicting on the same children are congruent: keep the // smaller eclass and union the two eclasses in the sort's `@UF`. In // proof mode the view proofs (`eclass = f(children)`) compose into the @@ -558,11 +578,18 @@ impl<'a> ProofInstrumentor<'a> { let uf_name = self.uf_name(schema.output()); let trans = self.proof_names().eq_trans_constructor.clone(); let sym = self.proof_names().eq_sym_constructor.clone(); + let proof_sort = self.proof_sort(); + let mut mints = vec![]; + let sym_pf = self.mint(&mut mints, &sym, "lo_pf_", &proof_sort); + let union_pf = + self.mint(&mut mints, &trans, &format!("hi_pf_ {sym_pf}"), &proof_sort); + let mints_str = mints.join("\n "); format!( "((let hi_pf_ (proof-of-max old0 old1 new0 new1)) (let lo_pf_ (proof-of-min old0 old1 new0 new1)) + {mints_str} (set ({uf_name} (ordering-max old0 new0)) - (values (ordering-min old0 new0) ({trans} hi_pf_ ({sym} lo_pf_)))) + (values (ordering-min old0 new0) {union_pf})) (values (ordering-min old0 new0) lo_pf_))" ) } else { @@ -575,46 +602,71 @@ impl<'a> ProofInstrumentor<'a> { format!( "(function {view_name} ({in_sorts}) ({out_type} {proof_type}) :merge {congruence_merge} :internal-term-constructor {name}{view_flags} :internal-identity-vals 1)" ) + } else if fdecl.merge.is_some() { + // Custom function with a `:merge`: FD pair-valued view keyed on children; + // the value is `(output {Unit|Proof})` and the `:merge` runs the user + // merge once (see `custom_view_merge`). No `@UF` union. + let custom_merge = self.custom_view_merge(fdecl); + format!( + "(function {view_name} ({in_sorts}) ({out_type} {proof_type}) :merge {custom_merge} :internal-term-constructor {name}{view_flags} :internal-identity-vals 1)" + ) } else { + // Primitive/`Unit`-output `:no-merge` custom (eq-sort `:no-merge` is + // rejected before encoding). The FD view is declared native `:no-merge` + // with `:internal-identity-vals 1`: a children collision keeps the old + // row when value column 0 (the output) is unchanged — raw equality is + // equality for a primitive output — and panics when it differs. The + // proof column (value column 1) is a payload the identity guard ignores. + debug_assert!( + !fdecl.resolved_schema.output().is_eq_sort(), + "eq-sort `:no-merge` must be rejected by command_supports_proof_encoding" + ); format!( - "(function {view_name} ({view_sorts}) {proof_type} :merge old :internal-term-constructor {name}{view_flags})" + "(function {view_name} ({in_sorts}) ({out_type} {proof_type}) :no-merge :internal-term-constructor {name}{view_flags} :internal-identity-vals 1)" ) }; self.parse_program(&format!( " (sort {fresh_sort}) {to_ast_view_sort} - (constructor {name} ({term_sorts}) {view_sort}{term_flags} :internal-hidden :unextractable) + (function {name} ({term_sorts} {view_sort}) Unit :no-merge :internal-hidden) {view_decl} (constructor {to_delete_name} ({in_sorts}) {fresh_sort} :internal-hidden) (constructor {subsumed_name} ({in_sorts}) {fresh_sort} :internal-hidden) - {proof_constructors} - {merge_rule} {delete_rule}", )) } - fn proof_functions(&mut self, _fdecl: &ResolvedFunctionDecl, _view_sorts: &str) -> String { - // ViewProof is now merged into the view table as its output column - "".to_string() + /// Wrap one maintenance-rebuild rule (`facts` -> `actions`) with the rebuilding + /// ruleset, a fresh name, and `:internal-include-subsumed` (so stale rows are + /// rebuilt too). `naive` marks rules whose primitives read `@UF` tables the rule + /// body doesn't join on. + fn rebuild_rule(&mut self, facts: &str, actions: &str, naive: bool) -> String { + let ruleset = self.proof_names().rebuilding_ruleset_name.clone(); + let fresh_name = self.egraph.parser.symbol_gen.fresh("rebuild_rule"); + let naive = if naive { ":naive " } else { "" }; + format!( + "(rule ({facts})\n ({actions})\n :ruleset {ruleset} {naive}:name \"{fresh_name}\" :internal-include-subsumed)\n" + ) } - /// Rebuild rules that keep a table's view canonical, fanned out one rule per - /// rebuildable column (a canonical column has no `@UF` row, so it simply - /// doesn't match). A stale column is replaced by its `@UF` leader (eq-sorts) - /// or its rebuilt container. + /// Rebuild rules that keep a view canonical: one rule per rebuildable child + /// column (a canonical column has no `@UF` row, so the rule simply doesn't + /// match), plus a rule for the FD view's value column. A stale eq-sort column is + /// replaced by its `@UF` leader, a stale container by its rebuilt value. /// - /// A constructor's functional-dependency view `(children) -> (eclass, {Unit|Proof})` - /// re-keys the row for a child update — `set` at the canonicalized children - /// (congruence resolves collisions), then `delete` — and re-`set`s the same key - /// for an eclass update (the view `:merge` keeps the min). A custom function's - /// all-key view `(children eclass) -> {Unit|Proof}` re-keys every column. - /// In proof mode each rule composes the updated view proof, and a container - /// update records the rebuilt container's `Proof`. + /// A child update re-keys the row (`set` at the canonicalized children, then + /// `delete`); a collision on the new key runs the view's `:merge`. The value + /// column is canonicalized by [`Self::fd_value_rebuild_rule`]. In proof mode + /// each rule composes the updated view proof, and a container update records the + /// rebuilt container's `Proof`. fn rebuilding_rules(&mut self, fdecl: &ResolvedFunctionDecl) -> Vec { - // Constructors use the FD view keyed by children; custom functions the all-key view. - let fd = fdecl.subtype == FunctionSubtype::Constructor; + let fd = self.is_fd_view(fdecl); let proofs = self.proofs_enabled(); + // A global's output *is* its e-class (like a constructor's), so it takes the + // e-class rebuild below (union-tracking) — not the custom-output rebuild + // (congruence), which would emit a nonsensical `Congr` on its nullary term. + let output_is_eclass = self.output_is_eclass(fdecl); let types = fdecl.resolved_schema.view_types(); let n = types.len(); let child = |i: usize| format!("c{i}_"); @@ -623,7 +675,6 @@ impl<'a> ProofInstrumentor<'a> { let key_vars: Vec = (0..n_keys).map(child).collect(); let view_name = self.view_name(&fdecl.name); let keys_str = format!("{}", ListDisplay(&key_vars, " ")); - let rebuilding_ruleset = self.proof_names().rebuilding_ruleset_name.clone(); let mut rules = String::new(); // One rule per rebuildable key column (re-keys the row via set + delete). @@ -644,37 +695,43 @@ impl<'a> ProofInstrumentor<'a> { // Canonicalize the column with the container rebuild primitive or a `@UF` // lookup, and build the proof pieces. Container-reading rules are `:naive` // (the primitive reads `@UF` tables the rule doesn't join on). - let (canon_fact, naive, proof_lets, pf_arg, cproof_set) = if is_container { + let (canon_fact, proof_lets, pf_arg, cproof_set) = if is_container { let value_prim = self.ensure_container_rebuild(ty); let canon_fact = format!("(= {canon} ({value_prim} {ci}))"); if proofs { let congr = self.proof_names().congr_constructor.clone(); let trans = self.proof_names().eq_trans_constructor.clone(); let sym = self.proof_names().eq_sym_constructor.clone(); + let proof_sort = self.proof_sort(); let proof_prim = self.ensure_container_rebuild_proof(ty); let rebuild_pf = self.fresh_var(); - let new_pf = self.fresh_var(); let cproof = self.term_proof_name(ty.name()); + // proof_lets: bind the container rebuild proof, then mint the congr proof. + let mut lets = vec![format!("(let {rebuild_pf} ({proof_prim} {ci}))")]; + let new_pf = self.mint( + &mut lets, + &congr, + &format!("{view_prf} {i} {rebuild_pf}"), + &proof_sort, + ); + // cproof_set: mint (Sym rebuild_pf), (Trans .. rebuild_pf), then record it. + let mut cproof_stmts = vec![]; + let sym_pf = self.mint(&mut cproof_stmts, &sym, &rebuild_pf, &proof_sort); + let trans_pf = self.mint( + &mut cproof_stmts, + &trans, + &format!("{sym_pf} {rebuild_pf}"), + &proof_sort, + ); + cproof_stmts.push(format!("(set ({cproof} {canon}) {trans_pf})")); ( canon_fact, - ":naive ", - format!( - "(let {rebuild_pf} ({proof_prim} {ci})) - (let {new_pf} ({congr} {view_prf} {i} {rebuild_pf}))" - ), + lets.join("\n "), new_pf, - format!( - "(set ({cproof} {canon}) ({trans} ({sym} {rebuild_pf}) {rebuild_pf}))" - ), + cproof_stmts.join("\n "), ) } else { - ( - canon_fact, - ":naive ", - String::new(), - "()".to_string(), - String::new(), - ) + (canon_fact, String::new(), "()".to_string(), String::new()) } } else { let uf_name = self.uf_name(ty.name()); @@ -682,22 +739,22 @@ impl<'a> ProofInstrumentor<'a> { let canon_fact = format!("(= (values {canon} {uf_prf}) ({uf_name} {ci}))"); if proofs { let congr = self.proof_names().congr_constructor.clone(); - let new_pf = self.fresh_var(); + let proof_sort = self.proof_sort(); + let mut lets = vec![]; + let new_pf = self.mint( + &mut lets, + &congr, + &format!("{view_prf} {i} {uf_prf}"), + &proof_sort, + ); ( canon_fact, - "", - format!("(let {new_pf} ({congr} {view_prf} {i} {uf_prf}))"), + lets.join("\n "), new_pf, String::new(), ) } else { - ( - canon_fact, - "", - String::new(), - "()".to_string(), - String::new(), - ) + (canon_fact, String::new(), "()".to_string(), String::new()) } }; let mut updated = key_vars.clone(); @@ -706,54 +763,99 @@ impl<'a> ProofInstrumentor<'a> { Some(eclass) => self.update_fd_view(&fdecl.name, &updated, eclass, &pf_arg), None => self.update_view(&fdecl.name, &updated, &pf_arg), }; - let fresh_name = self.egraph.parser.symbol_gen.fresh("rebuild_rule"); - rules.push_str(&format!( - "(rule ({query_view} - {canon_fact} - (!= {ci} {canon})) - ( - {proof_lets} - {updated_view} - {cproof_set} - (delete ({view_name} {keys_str})) - ) - :ruleset {rebuilding_ruleset} {naive}:name \"{fresh_name}\" :internal-include-subsumed)\n" - )); + let facts = format!("{query_view}\n{canon_fact}\n(!= {ci} {canon})"); + let actions = format!( + "{proof_lets}\n{updated_view}\n{cproof_set}\n(delete ({view_name} {keys_str}))" + ); + rules.push_str(&self.rebuild_rule(&facts, &actions, is_container)); } - // FD views: one rule for the eclass value (same key, `set` only; the view - // merge keeps the min). - if fd { - let eclass_uf_name = self.uf_name(types[n - 1].name()); - let (query_view, eclass_var, view_prf) = self.query_fd_view(&fdecl.name, &key_vars); - let eclass_canon = self.fresh_var(); - let uf_prf = self.fresh_var(); - let (proof_lets, pf_arg) = if proofs { - let trans = self.proof_names().eq_trans_constructor.clone(); - let sym = self.proof_names().eq_sym_constructor.clone(); - let new_pf = self.fresh_var(); - ( - format!("(let {new_pf} ({trans} ({sym} {uf_prf}) {view_prf}))"), - new_pf, - ) - } else { - (String::new(), "()".to_string()) - }; - let updated_view = self.update_fd_view(&fdecl.name, &key_vars, &eclass_canon, &pf_arg); - let fresh_name = self.egraph.parser.symbol_gen.fresh("rebuild_rule"); - rules.push_str(&format!( - "(rule ({query_view} - (= (values {eclass_canon} {uf_prf}) ({eclass_uf_name} {eclass_var})) - (!= {eclass_var} {eclass_canon})) - ( - {proof_lets} - {updated_view} - ) - :ruleset {rebuilding_ruleset} :name \"{fresh_name}\" :internal-include-subsumed)\n" + // FD view value column: when it is unioned to a smaller `@UF` leader, re-key + // the row to the leader (see [`Self::fd_value_rebuild_rule`]). A + // constructor/global's value *is* its e-class; a custom `:merge` function's + // eq-sort output takes the delete-then-reinsert path. A non-eq-sort custom + // output has no leader to chase, so nothing is emitted. + if fd && output_is_eclass { + rules.push_str(&self.fd_value_rebuild_rule(fdecl, &key_vars, ValueRebuild::Eclass)); + } else if fd + && fdecl.subtype == FunctionSubtype::Custom + && !self.is_encoded_global(fdecl) + && types[n - 1].is_eq_sort() + && n_keys == n - 1 + { + rules.push_str(&self.fd_value_rebuild_rule( + fdecl, + &key_vars, + ValueRebuild::CustomOutput { out_idx: n - 1 }, )); } self.parse_program(&rules) } + /// One rule that canonicalizes an FD view's value column when it is unioned to a + /// smaller `@UF` leader, re-keying the row to the leader. + /// + /// * [`ValueRebuild::Eclass`] (constructors/globals): the value *is* the + /// e-class, so re-`set` the same key and let the congruence `:merge` keep the + /// min. The row proof `canon = f(children)` is `Trans(Sym(key = leader), key = + /// f(children))`. + /// * [`ValueRebuild::CustomOutput`] (a custom `:merge` function's eq-sort + /// output): `delete` the stale row first, so the re-`set` inserts without + /// re-running the user merge. The row proof rewrites the output child by + /// `Congr` at its position. + fn fd_value_rebuild_rule( + &mut self, + fdecl: &ResolvedFunctionDecl, + key_vars: &[String], + kind: ValueRebuild, + ) -> String { + let value_uf_name = self.uf_name(fdecl.resolved_schema.output().name()); + let (query_view, value_var, view_prf) = self.query_fd_view(&fdecl.name, key_vars); + let canon = self.fresh_var(); + let uf_prf = self.fresh_var(); + let (proof_lets, pf_arg) = if self.proofs_enabled() { + let proof_sort = self.proof_sort(); + let mut lets = vec![]; + let pf = match kind { + ValueRebuild::Eclass => { + let sym = self.proof_names().eq_sym_constructor.clone(); + let trans = self.proof_names().eq_trans_constructor.clone(); + let sym_pf = self.mint(&mut lets, &sym, &uf_prf, &proof_sort); + self.mint( + &mut lets, + &trans, + &format!("{sym_pf} {view_prf}"), + &proof_sort, + ) + } + ValueRebuild::CustomOutput { out_idx } => { + let congr = self.proof_names().congr_constructor.clone(); + self.mint( + &mut lets, + &congr, + &format!("{view_prf} {out_idx} {uf_prf}"), + &proof_sort, + ) + } + }; + (lets.join("\n "), pf) + } else { + (String::new(), "()".to_string()) + }; + let set_canon = self.update_fd_view(&fdecl.name, key_vars, &canon, &pf_arg); + let actions = match kind { + ValueRebuild::Eclass => format!("{proof_lets}\n{set_canon}"), + ValueRebuild::CustomOutput { .. } => { + let view_name = self.view_name(&fdecl.name); + let keys_str = ListDisplay(key_vars, " ").to_string(); + format!("{proof_lets}\n(delete ({view_name} {keys_str}))\n{set_canon}") + } + }; + let facts = format!( + "{query_view}\n(= (values {canon} {uf_prf}) ({value_uf_name} {value_var}))\n(!= {value_var} {canon})" + ); + self.rebuild_rule(&facts, &actions, false) + } + /// Rules that update the to_subsume tables when children change. One rule per /// eq-sort child (no proof needed for subsumed rows). fn rebuilding_subsumed_rules(&mut self, fdecl: &ResolvedFunctionDecl) -> Vec { @@ -836,7 +938,12 @@ impl<'a> ProofInstrumentor<'a> { // by re-evaluation (see `check_side_condition`, which shares this gate). if is_container_side_condition(fact) { res.push(fact.to_string()); - return format!("({})", self.proof_names().eval_constructor); + if self.egraph.proof_state.proofs_enabled { + let eval_constructor = self.proof_names().eval_constructor.clone(); + let proof_sort = self.proof_sort(); + return self.mint(action_lookups, &eval_constructor, "", &proof_sort); + } + return "()".to_string(); } match fact { // In proof normal form, this is the only way that function calls appear. @@ -860,23 +967,28 @@ impl<'a> ProofInstrumentor<'a> { new_args.push(var); arg_proofs.push(proof); } - new_args.push(v.to_string()); let view_name = self.view_name(head.name()); - let args_str = ListDisplay(new_args, " "); - // View is always a function; query it and bind the output + // Every custom function has the FD pair-valued view keyed by children + // with value `(output {Unit|Proof})`: bind the output `v` (pair-first) + // and the row's existence proof `proof_var` (pair-second). let proof_var = self.fresh_var(); - res.push(format!("(= {proof_var} ({view_name} {args_str}))")); + let children_str = ListDisplay(&new_args, " "); + res.push(format!( + "(= (values {v} {proof_var}) ({view_name} {children_str}))" + )); if self.egraph.proof_state.proofs_enabled { + let congr = self.proof_names().congr_constructor.clone(); + let proof_sort = self.proof_sort(); let mut proof = proof_var; for (i, arg_proof) in arg_proofs.into_iter().enumerate() { - let congr = &self.proof_names().congr_constructor; - proof = format!( - " - ({congr} {proof} {i} {arg_proof}) - " + proof = self.mint( + action_lookups, + &congr, + &format!("{proof} {i} {arg_proof}"), + &proof_sort, ); } proof @@ -888,10 +1000,20 @@ impl<'a> ProofInstrumentor<'a> { let (v1, p1) = self.instrument_fact_expr(left_expr, res, action_lookups); let (v2, p2) = self.instrument_fact_expr(right_expr, res, action_lookups); res.push(format!("(= {v1} {v2})")); - let sym = &self.proof_names().eq_sym_constructor; - let trans = &self.proof_names().eq_trans_constructor; - - format!("({trans} ({sym} {p1}) {p2})",) + if self.egraph.proof_state.proofs_enabled { + let sym = self.proof_names().eq_sym_constructor.clone(); + let trans = self.proof_names().eq_trans_constructor.clone(); + let proof_sort = self.proof_sort(); + let sym_pf = self.mint(action_lookups, &sym, &p1, &proof_sort); + self.mint( + action_lookups, + &trans, + &format!("{sym_pf} {p2}"), + &proof_sort, + ) + } else { + "()".to_string() + } } ResolvedFact::Fact(generic_expr) => { let (_, proof) = self.instrument_fact_expr(generic_expr, res, action_lookups); @@ -902,7 +1024,9 @@ impl<'a> ProofInstrumentor<'a> { if p.output().is_eq_container_sort() ) { - format!("({})", self.proof_names().eval_constructor) + let eval_constructor = self.proof_names().eval_constructor.clone(); + let proof_sort = self.proof_sort(); + self.mint(action_lookups, &eval_constructor, "", &proof_sort) } else { proof } @@ -923,14 +1047,25 @@ impl<'a> ProofInstrumentor<'a> { match expr { ResolvedExpr::Lit(_, lit) => { let proof_code = if self.egraph.proof_state.proofs_enabled { - let fiat_constructor = &self.proof_names().fiat_constructor; let lit_sort = literal_sort(lit); let to_ast = self .proof_names() .sort_to_ast_constructor .get(lit_sort.name()) - .unwrap(); - format!("({fiat_constructor} ({to_ast} {lit}) ({to_ast} {lit}))") + .unwrap() + .clone(); + let fiat_constructor = self.proof_names().fiat_constructor.clone(); + let proof_sort = self.proof_sort(); + let ast_sort = self.proof_names().ast_sort.clone(); + let lit_str = format!("{lit}"); + let a1 = self.mint(action_lookups, &to_ast, &lit_str, &ast_sort); + let a2 = self.mint(action_lookups, &to_ast, &lit_str, &ast_sort); + self.mint( + action_lookups, + &fiat_constructor, + &format!("{a1} {a2}"), + &proof_sort, + ) } else { "()".to_string() }; @@ -959,14 +1094,24 @@ impl<'a> ProofInstrumentor<'a> { .push(format!("(let {fresh_proof} ({term_proof_name} {var}))")); fresh_proof } else { - let fiat_constructor = &self.proof_names().fiat_constructor; let lit_sort = resolved_var.sort.name(); let to_ast = self .proof_names() .sort_to_ast_constructor .get(lit_sort) - .unwrap(); - format!("({fiat_constructor} ({to_ast} {var}) ({to_ast} {var}))") + .unwrap() + .clone(); + let fiat_constructor = self.proof_names().fiat_constructor.clone(); + let proof_sort = self.proof_sort(); + let ast_sort = self.proof_names().ast_sort.clone(); + let a1 = self.mint(action_lookups, &to_ast, var, &ast_sort); + let a2 = self.mint(action_lookups, &to_ast, var, &ast_sort); + self.mint( + action_lookups, + &fiat_constructor, + &format!("{a1} {a2}"), + &proof_sort, + ) }, ) } @@ -986,9 +1131,14 @@ impl<'a> ProofInstrumentor<'a> { } match resolved_call { ResolvedCall::Func(func_type) => { + // Constructors and encoded globals both have the FD view + // `(children) -> (eclass, proof)`, so the same view read binds + // the e-class + proof. Other custom lookups are banned here by + // proof normal form. assert!( - func_type.subtype == FunctionSubtype::Constructor, - "Only constructor function calls are allowed in fact expressions due to proof normal form. Got {func_type:?}", + func_type.subtype == FunctionSubtype::Constructor + || self.egraph.type_info.is_global(&func_type.name), + "Only constructor (or global) function calls are allowed in fact expressions due to proof normal form. Got {func_type:?}", ); let fv = self.fresh_var(); @@ -1004,14 +1154,16 @@ impl<'a> ProofInstrumentor<'a> { "(= (values {fv} {view_proof_var}) ({view_name} {args_str}))" )); if self.proofs_enabled() { + let congr = self.proof_names().congr_constructor.clone(); + let proof_sort = self.proof_sort(); let mut proof = view_proof_var; for (i, arg_proof) in arg_proofs.into_iter().enumerate() { if let Some(arg_proof) = arg_proof { - let congr = &self.proof_names().congr_constructor; - proof = format!( - " - ({congr} {proof} {i} {arg_proof}) - " + proof = self.mint( + action_lookups, + &congr, + &format!("{proof} {i} {arg_proof}"), + &proof_sort, ); } } @@ -1052,13 +1204,23 @@ impl<'a> ProofInstrumentor<'a> { } else { // Base primitives produce a literal result; a // reflexive `Fiat` over a literal is checker-valid. - let fiat_constructor = &self.proof_names().fiat_constructor; let to_ast = self .proof_names() .sort_to_ast_constructor .get(specialized_primitive.output().name()) - .unwrap(); - format!("({fiat_constructor} ({to_ast} {fv}) ({to_ast} {fv}))") + .unwrap() + .clone(); + let fiat_constructor = self.proof_names().fiat_constructor.clone(); + let proof_sort = self.proof_sort(); + let ast_sort = self.proof_names().ast_sort.clone(); + let a1 = self.mint(action_lookups, &to_ast, &fv, &ast_sort); + let a2 = self.mint(action_lookups, &to_ast, &fv, &ast_sort); + self.mint( + action_lookups, + &fiat_constructor, + &format!("{a1} {a2}"), + &proof_sort, + ) }; (fv.clone(), proof) @@ -1088,7 +1250,15 @@ impl<'a> ProofInstrumentor<'a> { proof.push(f_proof); } - (res, action_lookups, self.format_prooflist(&proof)) + // The prooflist mints are actions (emitted into `action_lookups` before + // the proof binding). Only proof mode consumes the prooflist; in term + // mode it is discarded, so skip the mints to keep `action_lookups` empty. + let proof_list = if self.proofs_enabled() { + self.format_prooflist(&mut action_lookups, &proof) + } else { + String::new() + }; + (res, action_lookups, proof_list) } // Actions need to be instrumented to add to the view @@ -1097,27 +1267,69 @@ impl<'a> ProofInstrumentor<'a> { &mut self, action: &ResolvedAction, justification: &Justification, + nat_conn: &mut NatConn, ) -> Vec { let mut res = vec![]; match action { ResolvedAction::Let(_span, v, generic_expr) => { - let v2 = self.instrument_action_expr(generic_expr, &mut res, justification); + let v2 = + self.instrument_action_expr(generic_expr, &mut res, justification, nat_conn); + // Carry the canonicalization info onto the let-bound name. `v2` is + // the built term's deduped e-class var, keyed in `nat_conn` by that + // fresh var; without this, a later reference to `v.name` (e.g. the + // `new-e` in `(let new-e (Bop …)) (union e new-e)`) misses in + // `nat_conn` and `union` falls into the no-connector branch, whose + // bare `@Rule` endpoint extracts the deduped (canonicalized) shape + // instead of the natural one the rule head produced. + if let Some(info) = nat_conn.get(&v2).cloned() { + nat_conn.insert(v.name.clone(), info); + } res.push(format!("(let {} {})", v.name, v2)); } ResolvedAction::Set(_span, h, generic_exprs, generic_expr) => { - let mut exprs = vec![]; - for e in generic_exprs.iter().chain(std::iter::once(generic_expr)) { - exprs.push(self.instrument_action_expr(e, &mut res, justification)); - } - let ResolvedCall::Func(func_type) = h else { panic!( "Set action on non-function, should have been prevented by typechecking" ); }; - let (add_code, _fv) = self.add_term_and_view(func_type, &exprs, justification); + // Global definition `(set (x) e)`: x is a nullary `:internal-let` + // function aliasing e. Store e's value+proof directly in x's FD view + // (x's e-class *is* e's) — no term mint, which would use the wrong + // arity for x's term relation (its output is the eclass, so it has + // no separate output column). + if generic_exprs.is_empty() && self.egraph.type_info.is_global(&func_type.name) { + let e_value = self.instrument_action_expr( + generic_expr, + &mut res, + justification, + nat_conn, + ); + let proof = if self.proofs_enabled() { + self.global_value_proof( + &mut res, + func_type, + &e_value, + justification, + nat_conn, + ) + } else { + "()".to_string() + }; + // Term row (`x`'s e-class is e's) + the FD view `() -> (val, proof)`. + res.push(format!("(set ({} {e_value}) ())", func_type.name)); + res.push(self.update_fd_view(&func_type.name, &[], &e_value, &proof)); + return res; + } + + let mut exprs = vec![]; + for e in generic_exprs.iter().chain(std::iter::once(generic_expr)) { + exprs.push(self.instrument_action_expr(e, &mut res, justification, nat_conn)); + } + + let (add_code, _fv) = + self.add_term_and_view(func_type, &exprs, justification, nat_conn); res.extend(add_code); } ResolvedAction::Change(_span, change, h, generic_exprs) => { @@ -1128,7 +1340,7 @@ impl<'a> ProofInstrumentor<'a> { }; let children = generic_exprs .iter() - .map(|e| self.instrument_action_expr(e, &mut res, justification)) + .map(|e| self.instrument_action_expr(e, &mut res, justification, nat_conn)) .collect::>(); res.push(format!("({symbol} {})", ListDisplay(children, " "))); @@ -1139,18 +1351,20 @@ impl<'a> ProofInstrumentor<'a> { } } ResolvedAction::Union(_span, generic_expr, generic_expr1) => { - let v1 = self.instrument_action_expr(generic_expr, &mut res, justification); - let v2 = self.instrument_action_expr(generic_expr1, &mut res, justification); + let v1 = + self.instrument_action_expr(generic_expr, &mut res, justification, nat_conn); + let v2 = + self.instrument_action_expr(generic_expr1, &mut res, justification, nat_conn); let ot = generic_expr.output_type(); let type_name = ot.name(); - let unioned = self.union(type_name, &v1, &v2, justification); + let unioned = self.union(&mut res, type_name, &v1, &v2, justification, nat_conn); res.push(unioned); } ResolvedAction::Panic(..) => { res.push(format!("{action}")); } ResolvedAction::Expr(_span, generic_expr) => { - self.instrument_action_expr(generic_expr, &mut res, justification); + self.instrument_action_expr(generic_expr, &mut res, justification, nat_conn); } } @@ -1161,43 +1375,95 @@ impl<'a> ProofInstrumentor<'a> { /// (wrapped by the AST constructor `to_ast`) proving `fv = fv`, from the /// surrounding [`Justification`]. Shared by constructor creation /// (`add_term_and_view`) and container creation. + /// Build the `t = t` (or merge/fiat) proof for a freshly created term `fv`, + /// emitting the proof-relation mints onto `stmts` and returning the proof var. fn term_proof_for_justification( - &self, + &mut self, + stmts: &mut Vec, fv: &str, to_ast: &str, justification: &Justification, ) -> String { - let rule_constructor = &self.proof_names().rule_constructor; - let fiat_constructor = &self.proof_names().fiat_constructor; + let ast_sort = self.proof_names().ast_sort.clone(); + let proof_sort = self.proof_sort(); match justification { - Justification::Rule(rule_name, rule_proof) => format!( - "({rule_constructor} {rule_name} {rule_proof} ({to_ast} {fv}) ({to_ast} {fv}))" - ), + Justification::Rule(rule_name, rule_proof) => { + let a1 = self.mint(stmts, to_ast, fv, &ast_sort); + let a2 = self.mint(stmts, to_ast, fv, &ast_sort); + let rule = self.proof_names().rule_constructor.clone(); + self.mint( + stmts, + &rule, + &format!("{rule_name} {rule_proof} {a1} {a2}"), + &proof_sort, + ) + } Justification::Fiat => { - format!("({fiat_constructor} ({to_ast} {fv}) ({to_ast} {fv}))") + let a1 = self.mint(stmts, to_ast, fv, &ast_sort); + let a2 = self.mint(stmts, to_ast, fv, &ast_sort); + let fiat = self.proof_names().fiat_constructor.clone(); + self.mint(stmts, &fiat, &format!("{a1} {a2}"), &proof_sort) } - Justification::Merge(fn_name, p1, p2) => { - let merge_constructor = &self.proof_names().merge_fn_constructor; - format!("({merge_constructor} \"{fn_name}\" {p1} {p2} ({to_ast} {fv}))") + // Term-free: no AST minted (`fv`/`to_ast` unused). The checker + // reconstructs the conclusion from the merge body + premise outputs. + Justification::MergeIdx(fn_name, p1, p2, idx) => { + let merge_idx = self.proof_names().merge_fn_idx_constructor.clone(); + self.mint( + stmts, + &merge_idx, + &format!("\"{fn_name}\" {p1} {p2} {idx}"), + &proof_sort, + ) + } + Justification::MergeRow(fn_name, p1, p2) => { + let merge_row = self.proof_names().merge_fn_row_constructor.clone(); + self.mint( + stmts, + &merge_row, + &format!("\"{fn_name}\" {p1} {p2}"), + &proof_sort, + ) } } } - /// Update the view with the given arguments. - /// The arguments include the eclass for constructors. + /// Proof stored in a global's FD view for the value `e` it aliases. + /// + /// When `e` is a built term (e.g. `(Plus …)`), `add_term_and_view` has already + /// proved its *natural* form — the literal term the checker reconstructs from + /// the global's `(let x e)` — and recorded a `connector : natural = e_value` in + /// `nat_conn`. Anchor the global's proof on that natural form (a reflexive + /// `e_value = e_value` routed through it) instead of fiat-ing the canonical + /// `e_value` directly: `e_value`'s shape may be a rewritten (canonicalized) + /// child the checker cannot establish, whereas the natural form is exactly the + /// global definition it can. An atomic value (a literal, or a bare reference to + /// another global) has no connector and is fiat-ed directly — a literal is + /// self-justifying and a global alias is already established. + fn global_value_proof( + &mut self, + res: &mut Vec, + func_type: &FuncType, + e_value: &str, + justification: &Justification, + nat_conn: &NatConn, + ) -> String { + if let Some((_nat, Some(connector))) = nat_conn.get(e_value).cloned() { + let proof_sort = self.proof_sort(); + let sym = self.proof_names().eq_sym_constructor.clone(); + let trans = self.proof_names().eq_trans_constructor.clone(); + let sym_conn = self.mint(res, &sym, &connector, &proof_sort); + self.mint(res, &trans, &format!("{sym_conn} {connector}"), &proof_sort) + } else { + let to_ast = self.fname_to_ast_name(&func_type.name).to_string(); + self.term_proof_for_justification(res, e_value, &to_ast, justification) + } + } + + /// Update a non-FD (all-key) view with the given arguments (children + output). /// View is always a function (returning Proof or Unit). fn update_view(&mut self, fname: &str, args: &[String], proof: &str) -> String { let view_name = self.view_name(fname); - let view_update = format!("(set ({view_name} {}) {proof})", ListDisplay(args, " ")); - if let Some((current_name, input_arity)) = - self.egraph.proof_state.merge_current.get(fname).cloned() - && args.len() == input_arity + 1 - { - let inputs = ListDisplay(&args[..input_arity], " "); - let output = &args[input_arity]; - return format!("{view_update}\n(set ({current_name} {inputs}) {output})"); - } - view_update + format!("(set ({view_name} {}) {proof})", ListDisplay(args, " ")) } /// Write a row into a constructor's functional-dependency view @@ -1223,59 +1489,223 @@ impl<'a> ProofInstrumentor<'a> { /// /// Returns a vector of strings representing code to add and a variable for the created term. /// We could return the term itself, but this might make the encoding blow up the code. + /// Mint a fresh id of `out_sort` and assert the relation row + /// `({name} {args_joined} )`, appending the `let`/`set` onto `stmts` + /// and returning the fresh variable. Terms and proofs are relations rather + /// than constructors, so an id is minted explicitly here rather than by a + /// constructor call; every minted id keeps its row (nothing is merged away). + pub(crate) fn mint( + &mut self, + stmts: &mut Vec, + name: &str, + args_joined: &str, + out_sort: &str, + ) -> String { + let v = self.fresh_var(); + // The generic `get-fresh!` takes the target sort as a string literal so it + // types its output without per-sort primitives (its runtime ignores the arg). + let get_fresh = crate::proofs::proof_fresh::GET_FRESH_PRIM_NAME; + stmts.push(format!("(let {v} ({get_fresh} \"{out_sort}\"))")); + stmts.push(format!("(set ({name} {args_joined} {v}) ())")); + v + } + + /// Read an encoded global's value from its FD view `() -> (val, proof)`, for a + /// global reference `(x)` appearing in an action. `set-if-empty` returns the + /// stored e-class (a global is `set` before it is used, so the fresh fallback is + /// dead code that only fires on a malformed program). The value read is already + /// the view's canonical e-class, so no natural/deduped connector is recorded. + fn lookup_global(&mut self, name: &str, res: &mut Vec) -> String { + let view = self.view_name(name); + let set_if_empty = crate::proofs::proof_fresh::set_if_empty_prim_name(&view); + let get_fresh = crate::proofs::proof_fresh::GET_FRESH_PRIM_NAME; + let view_sort = self + .proof_names() + .fn_to_term_sort + .get(name) + .expect("term sort recorded in term_and_view") + .clone(); + let fresh_e = self.fresh_var(); + res.push(format!("(let {fresh_e} ({get_fresh} \"{view_sort}\"))")); + let vx = self.fresh_var(); + let fallback_proof = if self.proofs_enabled() { + let to_ast = self.fname_to_ast_name(name).to_string(); + self.term_proof_for_justification(res, &fresh_e, &to_ast, &Justification::Fiat) + } else { + "()".to_string() + }; + res.push(format!( + "(let {vx} ({set_if_empty} {fresh_e} {fallback_proof}))" + )); + vx + } + + /// The `Proof` datatype's sort name (mint target for proof relations). + pub(crate) fn proof_sort(&self) -> String { + self.proof_names().proof_datatype.clone() + } + fn add_term_and_view( &mut self, func_type: &FuncType, args: &[String], justification: &Justification, + nat_conn: &mut NatConn, ) -> (Vec, String) { - // A fresh variable for the new term. - let fv = self.fresh_var(); let mut res = vec![]; - // TODO might be able to get rid of this intermediate variable in encoding - res.push(format!( - "(let {fv} ({} {}))", - func_type.name, - ListDisplay(args, " ") - )); - - let (proof_str, view_proof_var) = if self.egraph.proof_state.proofs_enabled { - let to_ast = self.fname_to_ast_name(&func_type.name); - let proof = self.term_proof_for_justification(&fv, to_ast, justification); - - let proof_var = self.fresh_var(); - // add a proof for the constructor if needed - let term_proof = if func_type.subtype == FunctionSubtype::Constructor { - let term_proof_constructor = self.term_proof_name(func_type.output().name()); - format!("(set ({term_proof_constructor} {fv}) {proof_var})") + let view_sort = self + .egraph + .proof_state + .proof_names + .fn_to_term_sort + .get(&func_type.name) + .expect("term sort recorded in term_and_view") + .clone(); + let proofs = self.egraph.proof_state.proofs_enabled; + + // Custom functions (and globals-as-constructors): mint the term-relation + // row and record its term proof. No canonicalization threading. + if func_type.subtype != FunctionSubtype::Constructor { + let fv = self.mint( + &mut res, + &func_type.name, + &ListDisplay(args, " ").to_string(), + &view_sort, + ); + let view_proof_var = if proofs { + let to_ast = self.fname_to_ast_name(&func_type.name).to_string(); + self.term_proof_for_justification(&mut res, &fv, &to_ast, justification) } else { - "".to_string() + "()".to_string() }; + // Every custom function has the FD pair-valued view: key on the children, + // value `(output {Unit|Proof})`. `args` ends with the output value (from + // the `(set (f c..) v)` action). `view_proof_var` proves the row's + // f-application term `f(children, output)` (what `fv` extracts to) — the + // premise `MergeRow`/`MergeIdx` reconstruct their conclusion from. A + // children-key collision runs the view's `:merge` (the user merge) or, for + // a `:no-merge` custom, the native `:no-merge` identity-vals guard. + let (output, children) = args.split_last().expect("custom set needs an output"); + res.push(self.update_fd_view(&func_type.name, children, output, &view_proof_var)); + return (res, fv); + } - ( - format!( - "(let {proof_var} {proof}) - {term_proof}" - ), - proof_var, - ) - } else { - ("".to_string(), "()".to_string()) - }; + let view = self.view_name(&func_type.name); + let set_if_empty = crate::proofs::proof_fresh::set_if_empty_prim_name(&view); + + // Term-only: build the term with canonical children and canonicalize it to + // the view's e-class via `set-if-empty`; return that canonical id so + // parents build with canonical children (views stay canonical). + if !proofs { + let fv = self.mint( + &mut res, + &func_type.name, + &ListDisplay(args, " ").to_string(), + &view_sort, + ); + let canon = self.fresh_var(); + res.push(format!( + "(let {canon} ({set_if_empty} {} {fv} ()))", + ListDisplay(args, " ") + )); + return (res, canon); + } - res.push(proof_str); - if func_type.subtype == FunctionSubtype::Constructor { - // FD view: children are the key, the fresh term is the eclass value. - res.push(self.update_fd_view(&func_type.name, args, &fv, &view_proof_var)); - } else { - // Custom function: `args` already includes the output column. - res.push(self.update_view(&func_type.name, args, &view_proof_var)); + // Proof mode (the flatten-canonicalize-thread plan): build the *natural* + // term (children at their as-built ids) and the *canonical* term (children + // at their view-deduped ids), connect them with a `Congr` chain over the + // changed children, then `set-if-empty` to the view's deduped e-class and + // stitch on the view-dedup edge. Return the deduped e-class (so parents and + // views stay canonical) and record `(natural, connector : natural = + // deduped)` in `nat_conn` for the parent's `Congr` and the root `union`. + let to_ast = self.fname_to_ast_name(&func_type.name).to_string(); + let proof_sort = self.proof_sort(); + let congr = self.proof_names().congr_constructor.clone(); + let trans = self.proof_names().eq_trans_constructor.clone(); + let sym = self.proof_names().eq_sym_constructor.clone(); + let term_proof_constructor = self.term_proof_name(func_type.output().name()); + + // Each arg is a child's deduped id; look up its natural id + connector. + let children: Vec<(String, String, Option)> = args + .iter() + .map(|a| match nat_conn.get(a) { + Some((nat, conn)) => (a.clone(), nat.clone(), conn.clone()), + None => (a.clone(), a.clone(), None), + }) + .collect(); + let nat_args: Vec = children.iter().map(|(_, n, _)| n.clone()).collect(); + let dedup_args: Vec = children.iter().map(|(d, _, _)| d.clone()).collect(); + + // Mint the proof terms first, then assemble the statements. `fv_nat` stays + // *unseeded* (only `fv_can` is written to the view) so it is never pulled + // into the view's congruence `:merge`; its `@Rule` endpoint therefore keeps + // the as-built shape the rule head produced. `fv_can` is always a separate + // node (even when no child changed) with the reflexive proof `fv_can = + // fv_can`, exempt from the rule-head check. + let fv_nat = self.mint( + &mut res, + &func_type.name, + &ListDisplay(&nat_args, " ").to_string(), + &view_sort, + ); + let nat_prf = self.term_proof_for_justification(&mut res, &fv_nat, &to_ast, justification); + // `Congr` chain: `fv_nat = f(deduped children)`. + let mut nat_to_dedup_term = nat_prf.clone(); + for (i, (_, _, conn)) in children.iter().enumerate() { + if let Some(conn) = conn { + nat_to_dedup_term = self.mint( + &mut res, + &congr, + &format!("{nat_to_dedup_term} {i} {conn}"), + &proof_sort, + ); + } } + let fv_can = self.mint( + &mut res, + &func_type.name, + &ListDisplay(&dedup_args, " ").to_string(), + &view_sort, + ); + let sym_ntd = self.mint(&mut res, &sym, &nat_to_dedup_term, &proof_sort); + let can_prf = self.mint( + &mut res, + &trans, + &format!("{sym_ntd} {nat_to_dedup_term}"), + &proof_sort, + ); - // No self-loop seed: the single-key `@UF` needs none — a root term simply has - // no `@UF` row (identity-on-miss), in both term and proof mode. + // Anchor both term proofs, dedup `fv_can` to the view e-class, and read the + // view's stored proof (`dedup = f(children)`). + let dedup = self.fresh_var(); + let vprf = self.fresh_var(); + let view_proof = crate::proofs::proof_fresh::view_proof_prim_name(&view); + let dedup_args = ListDisplay(&dedup_args, " "); + res.push(format!( + "(set ({term_proof_constructor} {fv_nat}) {nat_prf})" + )); + res.push(format!( + "(set ({term_proof_constructor} {fv_can}) {can_prf})" + )); + res.push(format!( + "(let {dedup} ({set_if_empty} {dedup_args} {fv_can} {can_prf}))" + )); + res.push(format!( + "(let {vprf} ({view_proof} {dedup_args} {can_prf}))" + )); - (res, fv) + // connector `fv_nat = dedup` = Trans(nat_to_dedup, Sym(dedup = f(children))). + // `sym_vprf` reads the `vprf` let, so it must follow the statements above. + let sym_vprf = self.mint(&mut res, &sym, &vprf, &proof_sort); + let connector = self.mint( + &mut res, + &trans, + &format!("{nat_to_dedup_term} {sym_vprf}"), + &proof_sort, + ); + + nat_conn.insert(dedup.clone(), (fv_nat, Some(connector))); + (res, dedup) } /// Returns a query for (fname args) and a variable for the proof (or Unit) output. @@ -1302,11 +1732,94 @@ impl<'a> ProofInstrumentor<'a> { } // Add to view and term tables, returning a variable for the created term. + /// Rebuild a custom function's merge body inside the pair-valued `current` + /// helper's `:merge`, minting each constructor subterm via `add_term_and_view` + /// (so canonical ids are used, like every other term site) with a term-free + /// `MergeIdx` proof. `idx` is threaded pre-order (incremented once per node, + /// leaves included) to match the checker's `subexpr_at_index`, so subexpr `idx` + /// evaluated on the premise outputs reconstructs exactly this node's term. + /// `old`/`new` in the body map to the `:merge` output columns `old0`/`new0`; + /// the carried view proofs are `old1`/`new1`. + fn instrument_merge_body( + &mut self, + expr: &ResolvedExpr, + res: &mut Vec, + fname: &str, + idx: &mut usize, + nat_conn: &mut NatConn, + ) -> String { + let my_idx = *idx; + *idx += 1; + match expr { + ResolvedExpr::Lit(_, lit) => format!("{lit}"), + ResolvedExpr::Var(_, resolved_var) => match resolved_var.name.as_str() { + "old" => "old0".to_string(), + "new" => "new0".to_string(), + other => other.to_string(), + }, + ResolvedExpr::Call(_, ResolvedCall::Func(func_type), args) => { + let arg_vars = args + .iter() + .map(|a| self.instrument_merge_body(a, res, fname, idx, nat_conn)) + .collect::>(); + let just = Justification::MergeIdx( + fname.to_string(), + "old1".to_string(), + "new1".to_string(), + my_idx, + ); + let (code, fv) = self.add_term_and_view(func_type, &arg_vars, &just, nat_conn); + res.extend(code); + fv + } + // A container-producing primitive (e.g. `set-intersect`): build the + // container over the recursively-built args and anchor a term-free + // `MergeIdx` container proof in `Proof` (the container rebuild's + // anchor). No AST/children needed. + ResolvedExpr::Call(_, ResolvedCall::Primitive(sp), args) => { + let arg_vars = args + .iter() + .map(|a| self.instrument_merge_body(a, res, fname, idx, nat_conn)) + .collect::>(); + let prim_name = sp.name().to_string(); + let out = sp.output(); + let fv = self.fresh_var(); + res.push(format!( + "(let {fv} ({prim_name} {}))", + ListDisplay(&arg_vars, " ") + )); + if self.egraph.proof_state.proofs_enabled && out.is_eq_container_sort() { + let csort = out.name().to_string(); + let to_ast = self + .proof_names() + .sort_to_ast_constructor + .get(&csort) + .unwrap() + .clone(); + let just = Justification::MergeIdx( + fname.to_string(), + "old1".to_string(), + "new1".to_string(), + my_idx, + ); + let proof_var = self.term_proof_for_justification(res, &fv, &to_ast, &just); + let cproof = self.term_proof_name(&csort); + res.push(format!("(set ({cproof} {fv}) {proof_var})")); + } + fv + } + ResolvedExpr::Call(_, _, _) => { + panic!("proof-mode merge body for `{fname}` contains an unsupported call form") + } + } + } + fn instrument_action_expr( &mut self, expr: &ResolvedExpr, res: &mut Vec, proof: &Justification, + nat_conn: &mut NatConn, ) -> String { match expr { ResolvedExpr::Lit(_, lit) => format!("{lit}"), @@ -1314,52 +1827,87 @@ impl<'a> ProofInstrumentor<'a> { ResolvedExpr::Call(_, resolved_call, args) => { let args = args .iter() - .map(|arg| self.instrument_action_expr(arg, res, proof)) + .map(|arg| self.instrument_action_expr(arg, res, proof, nat_conn)) .collect::>(); match resolved_call { ResolvedCall::Func(func_type) => { if func_type.subtype == FunctionSubtype::Custom { - // Globals are desugared to no-arg functions (in non-proof mode) - // They're allowed, in proof mode they are constructors. + // Proof normal form bans looking up custom functions in + // actions — EXCEPT encoded globals. A global is a nullary + // `:internal-let` function with the FD view + // `() -> (val, proof)`; read its value+proof from the view + // (`set-if-empty` returns the stored e-class, `view-proof` + // its proof). This is the only custom lookup allowed here. if self.egraph.type_info.is_global(&func_type.name) { - return format!("({} {})", func_type.name, ListDisplay(&args, " ")); + return self.lookup_global(&func_type.name, res); } panic!( "Found a function lookup in actions, should have been prevented by typechecking" ); } - let (add_code, fv) = self.add_term_and_view(func_type, &args, proof); + let (add_code, fv) = + self.add_term_and_view(func_type, &args, proof, nat_conn); res.extend(add_code); fv } ResolvedCall::Primitive(specialized_primitive) => { + let prim_name = specialized_primitive.name().to_string(); + let out = specialized_primitive.output(); + let container_proof = + self.egraph.proof_state.proofs_enabled && out.is_eq_container_sort(); + let csort = out.name().to_string(); + // The Rust container rebuild canonicalizes elements of any + // container uniformly, but its *proof* is a positional + // per-element `@Congr` fold — sound only when element order is + // stable. So only an ordered container (`vec-of`) over a single + // eq-sort threads element canonicalization here; unordered + // containers (sets/maps) reorder and keep the deduped path + // (making them generic needs an order-independent rebuild + // proof — a follow-up). + let elem_sort: Option = if container_proof && prim_name == "vec-of" + { + match out.inner_sorts().as_slice() { + [s] if s.is_eq_sort() => Some(s.name().to_string()), + _ => None, + } + } else { + None + }; + // Build over *natural* element ids where we have them and + // record each `natural -> (canonical, connector)` edge in + // `UF-Aux`, so the rebuild canonicalizes the element and threads + // its connector (the container-general analog of the + // constructor connector). + let mut build_args = Vec::with_capacity(args.len()); + for a in &args { + match (elem_sort.clone(), nat_conn.get(a).cloned()) { + (Some(es), Some((nat, Some(conn)))) => { + let aux = self.uf_aux_name(&es); + res.push(format!("(set ({aux} {nat}) (values {a} {conn}))")); + build_args.push(nat); + } + _ => build_args.push(a.clone()), + } + } let fv = self.fresh_var(); res.push(format!( - "(let {} ({} {}))", - fv, - specialized_primitive.name(), - ListDisplay(&args, " ") + "(let {fv} ({prim_name} {}))", + ListDisplay(&build_args, " ") )); - // In proof mode, a primitive that builds a container - // value records a reflexive term-proof in `Proof`. - // This is the anchor for the container's rebuild - // congruence proofs (see `rebuilding_rules`). - if self.egraph.proof_state.proofs_enabled { - let out = specialized_primitive.output(); - if out.is_eq_container_sort() { - let csort = out.name().to_string(); - let to_ast = self - .proof_names() - .sort_to_ast_constructor - .get(&csort) - .unwrap() - .clone(); - let proof_str = - self.term_proof_for_justification(&fv, &to_ast, proof); - let cproof = self.term_proof_name(&csort); - res.push(format!("(set ({cproof} {fv}) {proof_str})")); - } + // A container-producing primitive records a term-proof in + // `Proof`, the anchor for the container rebuild. + if container_proof { + let to_ast = self + .proof_names() + .sort_to_ast_constructor + .get(&csort) + .unwrap() + .clone(); + let proof_var = + self.term_proof_for_justification(res, &fv, &to_ast, proof); + let cproof = self.term_proof_name(&csort); + res.push(format!("(set ({cproof} {fv}) {proof_var})")); } fv } @@ -1376,10 +1924,11 @@ impl<'a> ProofInstrumentor<'a> { &mut self, actions: &[ResolvedAction], justification: &Justification, + nat_conn: &mut NatConn, ) -> Vec { let mut res = vec![]; for action in actions { - res.extend(self.instrument_action(action, justification)); + res.extend(self.instrument_action(action, justification, nat_conn)); } res } @@ -1389,6 +1938,12 @@ impl<'a> ProofInstrumentor<'a> { /// When proofs are enabled we query proof tables, then build a proof for the rule in the actions. /// Finally, each view update also updates the proof tables. fn instrument_rule(&mut self, rule: &ResolvedRule) -> Vec { + // `nat_conn` maps this rule's freshly-minted vars (and let-bound names) to + // their natural/connector; it is scoped to this generated program, so it is + // a fresh local map threaded through the action builders below. (A shared + // field would leak stale entries keyed by repeated user let names — e.g. + // `new-e` — from earlier rules/merges, referencing out-of-scope vars.) + let mut nat_conn = NatConn::default(); // term_proofs are fetched as action-side lookups (see instrument_facts), // so a rule with any needs a Read/Full action context (`eval_opt` below). let (facts, action_lookups, proof_str) = self.instrument_facts(&rule.body); @@ -1414,7 +1969,7 @@ impl<'a> ProofInstrumentor<'a> { "".to_string() }; - let actions = self.instrument_actions(&rule.head.0, &proof); + let actions = self.instrument_actions(&rule.head.0, &proof, &mut nat_conn); let name = &rule.name; let ruleset_opt = if rule.ruleset.is_empty() { "".to_string() @@ -1579,7 +2134,14 @@ impl<'a> ProofInstrumentor<'a> { let uf_name = if is_container { None } else { - Some((self.uf_name(name), None)) + // Proof mode also records the aux union-find (`:internal-uf-aux`) + // so container rebuild recovers it on re-parse. + let aux = if self.egraph.proof_state.proofs_enabled { + Some(self.uf_aux_name(name)) + } else { + None + }; + Some((self.uf_name(name), None, aux)) }; // Every sort (containers included) records its `Proof` // table via `:internal-proof-func` so container rebuild can @@ -1632,8 +2194,11 @@ impl<'a> ProofInstrumentor<'a> { res.extend(self.instrument_rule(rule)); } ResolvedNCommand::CoreAction(action) => { + // Each top-level action is its own generated program, so naturals do + // not carry across commands; scope `nat_conn` to a fresh local map. + let mut nat_conn = NatConn::default(); let instrumented = self - .instrument_action(action, &Justification::Fiat) + .instrument_action(action, &Justification::Fiat, &mut nat_conn) .join("\n"); res.extend(self.parse_program(&instrumented)); } @@ -1647,30 +2212,37 @@ impl<'a> ProofInstrumentor<'a> { ResolvedNCommand::RunSchedule(schedule) => { res.push(Command::RunSchedule(self.instrument_schedule(schedule))); } - ResolvedNCommand::Fail(span, cmd) => { + ResolvedNCommand::Fail(span, cmds) => { + // Encode every wrapped command and keep the whole flattened result + // inside one `fail` (a single command can encode to several). let mut encoded = vec![]; - self.term_encode_command(cmd, &mut encoded)?; - if encoded.len() != 1 { - return Err(Error::UnsupportedProofCommand { - command: cmd.to_command().to_string(), - reason: ProofEncodingUnsupportedReason::FailNonAtomicCommand, - }); + for cmd in cmds { + self.term_encode_command(cmd, &mut encoded)?; } - res.push(Command::Fail( - span.clone(), - Box::new(encoded.pop().unwrap()), - )); + res.push(Command::Fail(span.clone(), encoded)); } ResolvedNCommand::Input { .. } => { - unreachable!("inputs should be lowered before term/proof instrumentation") + // Loaded natively at run time (see `EGraph::native_input`), inserting + // straight into the encoded tables. Pass the command through so + // `run_command` dispatches it. + res.push(command.to_command().make_unresolved()); } ResolvedNCommand::Extract(span, expr, variants) => { // Instrument the expressions to use view tables (like actions, not facts) let mut action_stmts = vec![]; - let instrumented_expr = - self.instrument_action_expr(expr, &mut action_stmts, &Justification::Fiat); - let instrumented_variants = - self.instrument_action_expr(variants, &mut action_stmts, &Justification::Fiat); + let mut nat_conn = NatConn::default(); + let instrumented_expr = self.instrument_action_expr( + expr, + &mut action_stmts, + &Justification::Fiat, + &mut nat_conn, + ); + let instrumented_variants = self.instrument_action_expr( + variants, + &mut action_stmts, + &Justification::Fiat, + &mut nat_conn, + ); // Add any action statements needed to set up the expressions for stmt in action_stmts { @@ -1735,12 +2307,34 @@ impl<'a> ProofInstrumentor<'a> { for command in program { self.term_encode_command(&command, &mut res)?; - // run rebuilding after every command except a few - if let ResolvedNCommand::Function(..) - | ResolvedNCommand::NormRule { .. } - | ResolvedNCommand::Sort { .. } = &command + // A `set` (including a global-let's `(set (g) e)`) or a top-level + // expression over non-container sorts builds and dedups terms via + // `set-if-empty` without merging e-classes or deferring work, so no + // maintenance rebuild is needed after it — this is what stops N + // global-let `set`s from each triggering a rebuild (quadratic). We still + // rebuild after everything else: `union` merges e-classes, `delete`/ + // `subsume` defer work to the maintenance ruleset, and a container-valued + // action needs the (`:naive`) container rebuild to recanonicalize it — + // all need the following rebuild to run. + fn touches_container(e: &ResolvedExpr) -> bool { + e.output_type().is_eq_container_sort() + || matches!(e, ResolvedExpr::Call(_, _, args) if args.iter().any(touches_container)) + } + let is_set_or_expr = match &command { + ResolvedNCommand::CoreAction(ResolvedAction::Expr(_, e)) => !touches_container(e), + ResolvedNCommand::CoreAction(ResolvedAction::Set(_, _, args, rhs)) => !args + .iter() + .chain(std::iter::once(rhs)) + .any(touches_container), + _ => false, + }; + if !matches!( + &command, + ResolvedNCommand::Function(..) + | ResolvedNCommand::NormRule { .. } + | ResolvedNCommand::Sort { .. } + ) && !is_set_or_expr { - } else { res.push(Command::RunSchedule(self.rebuild())); } } diff --git a/egglog/src/proofs/proof_encoding_helpers.rs b/egglog/src/proofs/proof_encoding_helpers.rs index e604dce3..8e7ddb31 100644 --- a/egglog/src/proofs/proof_encoding_helpers.rs +++ b/egglog/src/proofs/proof_encoding_helpers.rs @@ -24,7 +24,8 @@ pub(crate) struct EncodingNames { pub(crate) proof_datatype: String, pub(crate) fiat_constructor: String, pub(crate) rule_constructor: String, - pub(crate) merge_fn_constructor: String, + pub(crate) merge_fn_idx_constructor: String, + pub(crate) merge_fn_row_constructor: String, pub(crate) eq_trans_constructor: String, pub(crate) eq_sym_constructor: String, pub(crate) congr_constructor: String, @@ -54,7 +55,17 @@ pub(crate) struct EncodingNames { pub(crate) enum Justification { Rule(String, String), // rule-name expression and proof-list expression Fiat, - Merge(String, String, String), // function name, proof1, proof2 + /// Term-free merge justification for a merge-body subexpression: function + /// name, the two premise (view) proof expressions, and the pre-order index of + /// this subexpression in the merge body (matches the checker's + /// `subexpr_at_index`). It embeds no AST, so it needs neither the merged term + /// nor the function key/children — usable in a `:merge` action. + MergeIdx(String, String, String, usize), + /// Term-free merge justification for the whole view row (function name + two + /// premise proof expressions). The conclusion `f(children, merged)` is + /// reconstructed by the checker by running the whole merge body on the premise + /// outputs; no AST/children needed. + MergeRow(String, String, String), } impl EncodingNames { @@ -65,7 +76,8 @@ impl EncodingNames { proof_datatype: symbol_gen.fresh("Proof"), fiat_constructor: symbol_gen.fresh("Fiat"), rule_constructor: symbol_gen.fresh("Rule"), - merge_fn_constructor: symbol_gen.fresh("Merge"), + merge_fn_idx_constructor: symbol_gen.fresh("MergeIdx"), + merge_fn_row_constructor: symbol_gen.fresh("MergeRow"), eq_trans_constructor: symbol_gen.fresh("Trans"), eq_sym_constructor: symbol_gen.fresh("Sym"), congr_constructor: symbol_gen.fresh("Congr"), @@ -101,6 +113,29 @@ impl ProofInstrumentor<'_> { } } + /// Fresh name of a sort's auxiliary union-find `UF_Aux_` (natural + /// e-class id -> canonical dedup id + connector proof `natural = canonical`), + /// cached in `uf_aux_parent`. Written only for container elements; the + /// container rebuild reads it (alongside the main `UF`) to canonicalize an + /// element built over its natural id and thread its connector proof. The name + /// round-trips via `:internal-uf-aux` so re-parse recovers it, not recomputes. + pub(crate) fn uf_aux_name(&mut self, sort: &str) -> String { + if let Some(name) = self.egraph.proof_state.uf_aux_parent.get(sort) { + name.clone() + } else { + let fresh_name = self + .egraph + .parser + .symbol_gen + .fresh(&format!("UF_Aux_{sort}")); + self.egraph + .proof_state + .uf_aux_parent + .insert(sort.to_string(), fresh_name.clone()); + fresh_name + } + } + pub(crate) fn parse_program(&mut self, input: &str) -> Vec { self.egraph.parser.ensure_no_reserved_symbols = false; let res = self.egraph.parser.get_program_from_string(None, input); @@ -109,13 +144,26 @@ impl ProofInstrumentor<'_> { res.unwrap() } - pub(crate) fn format_prooflist(&self, proofs: &[String]) -> String { - let pcons = &self.proof_names().pcons; - let pnil = &self.proof_names().pnil; + /// Build a ProofList relation (`pnil`, then `pcons` folds) by minting a fresh + /// id per node and asserting the row, emitting the mints onto `stmts` and + /// returning the final list's var. + pub(crate) fn format_prooflist( + &mut self, + stmts: &mut Vec, + proofs: &[String], + ) -> String { + let pcons = self.proof_names().pcons.clone(); + let pnil = self.proof_names().pnil.clone(); + let proof_list_sort = self.proof_names().proof_list_sort.clone(); - let mut prooflist = format!("({pnil})"); + let mut prooflist = self.mint(stmts, &pnil, "", &proof_list_sort); for proof in proofs.iter().rev() { - prooflist = format!("({pcons} {proof} {prooflist})"); + prooflist = self.mint( + stmts, + &pcons, + &format!("{proof} {prooflist}"), + &proof_list_sort, + ); } prooflist } @@ -253,7 +301,9 @@ impl ProofInstrumentor<'_> { .sort_to_ast_constructor .insert(sort.to_string(), to_ast_constructor.clone()); let ast_sort = &self.proof_names().ast_sort; - format!("(constructor {to_ast_constructor} ({sort}) {ast_sort} :internal-hidden)") + format!( + "(function {to_ast_constructor} ({sort} {ast_sort}) Unit :no-merge :internal-hidden)" + ) } else { "".to_string() } @@ -321,7 +371,7 @@ impl ProofInstrumentor<'_> { .sort_to_ast_constructor .insert(sort_name.clone(), ast_constructor.clone()); to_ast_constructors.push(format!( - "(constructor {ast_constructor} ({sort_name} ) {} :internal-hidden)", + "(function {ast_constructor} ({sort_name} {}) Unit :no-merge :internal-hidden)", self.proof_names().ast_sort )); } @@ -334,7 +384,8 @@ impl ProofInstrumentor<'_> { ref proof_datatype, ref fiat_constructor, ref rule_constructor, - ref merge_fn_constructor, + ref merge_fn_idx_constructor, + ref merge_fn_row_constructor, ref eq_trans_constructor, ref eq_sym_constructor, ref congr_constructor, @@ -351,41 +402,50 @@ impl ProofInstrumentor<'_> { (sort {ast_sort}) ;; wrap sorts in this for proofs ;; The proof datatype records the global proof constructor names so container ;; rebuild can recover them on re-parse (see ContainerRebuildSpec). -(sort {proof_datatype} :internal-proof-names {congr_constructor} {eq_trans_constructor} {eq_sym_constructor} {container_normalize_constructor}) +(sort {proof_datatype} :internal-proof-names {congr_constructor} {eq_trans_constructor} {eq_sym_constructor} {container_normalize_constructor} {fiat_constructor}) -(constructor {pcons} ({proof_datatype} {proof_list_sort}) {proof_list_sort} :internal-hidden) -(constructor {pnil} () {proof_list_sort} :internal-hidden) +;; Proof/AST/ProofList terms are relations, not constructors: the encoding mints +;; a fresh id (`get-fresh!`) and asserts the row, so congruent duplicates are +;; kept (never merged away) rather than relying on native congruence. The final +;; column of each relation is the minted output id. +(function {pcons} ({proof_datatype} {proof_list_sort} {proof_list_sort}) Unit :no-merge :internal-hidden) +(function {pnil} ({proof_list_sort}) Unit :no-merge :internal-hidden) {to_ast_str} ;; Fiat justification for globals and primitives, gives two terms t1 = t2 for the proposition being justified -(constructor {fiat_constructor} ({ast_sort} {ast_sort}) {proof_datatype} :internal-hidden) +(function {fiat_constructor} ({ast_sort} {ast_sort} {proof_datatype}) Unit :no-merge :internal-hidden) ;; name of rule, one proof per fact in the query, proposition being proven t1 = t2 -(constructor {rule_constructor} (String {proof_list_sort} {ast_sort} {ast_sort}) {proof_datatype} :internal-hidden) +(function {rule_constructor} (String {proof_list_sort} {ast_sort} {ast_sort} {proof_datatype}) Unit :no-merge :internal-hidden) -;; merge function justification- name of function and two proofs for the two terms being merged, -;; and the proposition being justified t = t -(constructor {merge_fn_constructor} (String {proof_datatype} {proof_datatype} {ast_sort}) {proof_datatype} :internal-hidden) +;; term-free merge justification for an FD custom-function view subexpression: +;; name of function, two premise proofs, and the pre-order index of the merge-body +;; subexpression whose conclusion is reconstructed during proof conversion +(function {merge_fn_idx_constructor} (String {proof_datatype} {proof_datatype} i64 {proof_datatype}) Unit :no-merge :internal-hidden) +;; term-free merge justification for an FD custom-function view row: +;; name of function and two premise proofs; the whole-row conclusion is +;; reconstructed during proof conversion by running the whole merge body +(function {merge_fn_row_constructor} (String {proof_datatype} {proof_datatype} {proof_datatype}) Unit :no-merge :internal-hidden) ;; transitivity of equality proofs -(constructor {eq_trans_constructor} ({proof_datatype} {proof_datatype}) {proof_datatype} :internal-hidden) +(function {eq_trans_constructor} ({proof_datatype} {proof_datatype} {proof_datatype}) Unit :no-merge :internal-hidden) ;; symmetry of equality proofs -(constructor {eq_sym_constructor} ({proof_datatype}) {proof_datatype} :internal-hidden) +(function {eq_sym_constructor} ({proof_datatype} {proof_datatype}) Unit :no-merge :internal-hidden) ;; given a proof that t1 = f(..., ci, ...) ;; and the child index i of ci in the term f(..., ci, ...) ;; and a proof that ci = c2, ;; produces a justification that t1 = f(..., c2, ...) -(constructor {congr_constructor} ({proof_datatype} i64 {proof_datatype}) {proof_datatype} :internal-hidden) +(function {congr_constructor} ({proof_datatype} i64 {proof_datatype} {proof_datatype}) Unit :no-merge :internal-hidden) ;; given a proof that t1 = c, where c is a container term, produces a proof that ;; t1 = normalize(c) (the container's canonicalization: sort/dedup for sets, ;; last-write-wins for maps, sort for multisets) -(constructor {container_normalize_constructor} ({proof_datatype}) {proof_datatype} :internal-hidden) +(function {container_normalize_constructor} ({proof_datatype} {proof_datatype}) Unit :no-merge :internal-hidden) ;; marks the proof of a container side condition. Carries nothing: the side ;; condition is re-evaluated against the rule body when checked. -(constructor {eval_constructor} () {proof_datatype} :internal-hidden) +(function {eval_constructor} ({proof_datatype}) Unit :no-merge :internal-hidden) " ) } @@ -447,10 +507,6 @@ pub enum ProofEncodingUnsupportedReason { UserDefinedCommand, #[error("`fail` wrapping an `input` command is not supported by proof encoding.")] FailInputCommand, - #[error( - "`fail` requires exactly one atomic encoded command; wrapped `set` and multi-operation commands are not supported." - )] - FailNonAtomicCommand, #[error( "let binding with a primitive in the body. For silly internal reasons, we don't support primitive bindings for proofs at the moment, sorry." )] @@ -469,6 +525,10 @@ pub enum ProofEncodingUnsupportedReason { "a `:merge` action block (actions before the result value) is not supported by the term/proof encoding." )] MergeActionBlock, + #[error( + "eq-sort-output `:no-merge` functions are not supported by the term/proof encoding (their conflict check needs union-find leaders); run them on the native backend, or give the function a `:merge` (e.g. `:merge old`). Primitive/`Unit`-output `:no-merge` functions are supported." + )] + NoMergeEqSortFunction, } /// Checks whether a desugared program supports proof encoding. @@ -553,6 +613,28 @@ pub(crate) fn command_supports_proof_encoding( return Err(ProofEncodingUnsupportedReason::TupleOutputFunction); } + // An eq-sort-output `:no-merge` function is not modeled by the encoding: its + // conflict check needs union-find leaders (raw id equality is not e-class + // equality), which the encoding has no eager hook for. A file using one is + // proof-unsupported and runs plain only. Primitive/`Unit`-output `:no-merge` IS + // supported (raw equality is equality there — encoded as an FD view declared + // native `:no-merge` + `:internal-identity-vals 1`). Constructors/relations are + // `Constructor` commands (not `Function`), and encoded globals (`:internal-let`, + // produced by `remove_globals` before this check in the plain-resolve path + // `file_supports_proofs` uses) have their own FD-view encoding — both excluded. + if let crate::ast::GenericCommand::Function { + merge: None, + let_binding: false, + schema, + .. + } = command + && type_info + .get_sort_by_name(schema.output()) + .is_some_and(|sort| sort.is_eq_sort()) + { + return Err(ProofEncodingUnsupportedReason::NoMergeEqSortFunction); + } + // A `:merge` action block runs actions before its result; the proof encoding only instruments // the merged value, so mark it unsupported rather than emit silently-incomplete proofs. if let crate::ast::GenericCommand::Function { @@ -590,9 +672,7 @@ pub(crate) fn command_supports_proof_encoding( if let GenericCommand::Function { merge: Some(merge), .. } = command - && type_info - .expr_has_function_lookup(&merge.result) - .is_some() + && type_info.expr_has_function_lookup(&merge.result).is_some() { return Err(ProofEncodingUnsupportedReason::FunctionLookupInAction); } @@ -668,13 +748,15 @@ pub(crate) fn command_supports_proof_encoding( Ok(()) } } - GenericCommand::Fail(_, command) => match command.as_ref() { - GenericCommand::Input { .. } => Err(ProofEncodingUnsupportedReason::FailInputCommand), - GenericCommand::Action(ResolvedAction::Set(..)) => { - Err(ProofEncodingUnsupportedReason::FailNonAtomicCommand) + GenericCommand::Fail(_, commands) => { + for command in commands { + if let GenericCommand::Input { .. } = command { + return Err(ProofEncodingUnsupportedReason::FailInputCommand); + } + command_supports_proof_encoding(command, type_info)?; } - command => command_supports_proof_encoding(command, type_info), - }, + Ok(()) + } // let binding with non-eq sort not supported by proof_global_desugar ResolvedCommand::Action(ResolvedAction::Let(_, _, expr)) => { // let binding with non-eq sort not supported by proof_global_desugar @@ -779,3 +861,65 @@ impl crate::constraint::TypeConstraint for OrientProofTypeConstraint { ] } } + +/// The `select-eq` primitive: `(select-eq test cand if-eq else) -> if-eq` when +/// `test == cand`, else `else`. Typed `(T T P P) -> P` for any sorts `T` and `P`. +/// +/// Used by a custom function's FD-view `:merge` to keep its proof column stable: +/// when the merged output equals a colliding premise's output, reuse that +/// premise's existing proof rather than mint a fresh one. Without this the proof +/// column would change on every idempotent merge (`min`/`max`/...), bumping the +/// row's timestamp and preventing saturation. +#[derive(Clone)] +pub(crate) struct SelectEqProof; + +impl crate::Primitive for SelectEqProof { + fn name(&self) -> &str { + "select-eq" + } + + fn get_type_constraints(&self, span: &Span) -> Box { + Box::new(SelectEqProofTypeConstraint { span: span.clone() }) + } +} + +impl crate::PurePrim for SelectEqProof { + fn apply<'a, 'db>(&self, _state: crate::PureState<'a, 'db>, args: &[Value]) -> Option { + let [test, cand, if_eq, els] = args else { + return None; + }; + Some(if test == cand { *if_eq } else { *els }) + } +} + +struct SelectEqProofTypeConstraint { + span: Span, +} + +impl crate::constraint::TypeConstraint for SelectEqProofTypeConstraint { + fn get( + &self, + arguments: &[crate::core::AtomTerm], + _typeinfo: &TypeInfo, + ) -> Vec>> { + // `(test cand if-eq else) -> out`: `test`/`cand` share one sort; + // `if-eq`/`else`/`out` another. + if arguments.len() != 5 { + return vec![crate::constraint::impossible( + crate::constraint::ImpossibleConstraint::ArityMismatch { + atom: crate::core::Atom { + span: self.span.clone(), + head: "select-eq".to_string(), + args: arguments.to_vec(), + }, + expected: 5, + }, + )]; + } + vec![ + crate::constraint::eq(arguments[1].clone(), arguments[0].clone()), + crate::constraint::eq(arguments[3].clone(), arguments[2].clone()), + crate::constraint::eq(arguments[4].clone(), arguments[2].clone()), + ] + } +} diff --git a/egglog/src/proofs/proof_extraction.rs b/egglog/src/proofs/proof_extraction.rs index 3b5b05a9..157e98c3 100644 --- a/egglog/src/proofs/proof_extraction.rs +++ b/egglog/src/proofs/proof_extraction.rs @@ -1,8 +1,7 @@ -use crate::ast::FunctionSubtype; use crate::proofs::proof_encoding::ProofInstrumentor; use crate::proofs::proof_extractor::extract_root; use crate::proofs::proof_format::{Justification, ProofId, ProofStore, proof_store_from_term}; -use crate::{ResolvedCall, TermDag}; +use crate::{ResolvedCall, TermDag, Value}; use egglog_backend_trait::BackendExt; use thiserror::Error; @@ -24,10 +23,7 @@ impl ProofInstrumentor<'_> { call: &ResolvedCall, ) -> Result<(ProofStore, ProofId), ProveExistsError> { let func = match call { - ResolvedCall::Func(func) if func.subtype == FunctionSubtype::Constructor => func, - ResolvedCall::Func(_) => { - return Err(ProveExistsError::RequiresConstructor); - } + ResolvedCall::Func(func) => func, ResolvedCall::Primitive(_) => { return Err(ProveExistsError::PrimitivesUnsupported); } @@ -43,37 +39,42 @@ impl ProofInstrumentor<'_> { .unwrap_or_else(|| panic!("constructor {} is not declared", func.name)); let backend_id = function.backend_id; - let output_sort = function.schema.output().clone(); + // The eclass sort and its column (last input for a relation, output for a + // plain constructor). + let output_sort = function.extraction_output_sort().clone(); + let output_index = function.extraction_output_index(); let mut termdag = TermDag::default(); - let mut witness_value = None; - self.egraph.backend.for_each_while(backend_id, |row| { - let value = *row - .vals - .last() - .expect("constructor rows include their output value"); - witness_value = Some(value); - false + // Pick the lexicographically-smallest row as the witness rather than + // whichever row the backend happens to yield first. A backend whose row + // order is not deterministic (e.g. the differential-dataflow backend's + // hash-set mirror) would otherwise make the extracted existence proof — + // and thus proof snapshots — vary run to run. + let mut best_row: Option> = None; + self.egraph.backend.for_each(backend_id, |row| { + if best_row.as_deref().is_none_or(|best| row.vals < best) { + best_row = Some(row.vals.to_vec()); + } }); - - let witness_value = witness_value.ok_or_else(|| ProveExistsError::QueryDidNotMatch { - constructor: func.name.clone(), + let witness_value = best_row.map(|row| row[output_index]).ok_or_else(|| { + ProveExistsError::QueryDidNotMatch { + constructor: func.name.clone(), + } })?; - let proof_function_name = self + // `prove-exists` targets a constructor, whose eq-sort output has a proof + // table. A function with a base-sort output (e.g. `(function f (i64) i64)`) + // has none, so there is nothing to prove — reject it rather than panic. + let Some(proof_function_name) = self .egraph .proof_state .proof_func_parent .get(output_sort.name()) - .unwrap_or_else(|| { - panic!( - "no :internal-proof-func annotation recorded for sort {} (constructor {})", - output_sort.name(), - func.name - ) - }) - .clone(); + .cloned() + else { + return Err(ProveExistsError::RequiresConstructor); + }; let proof_function = self .egraph .functions @@ -116,14 +117,18 @@ impl ProofInstrumentor<'_> { panic!("Failed to remove globals from proof: {e}"); } - // if the existence proof has a single premise, extract that premise proof + // If the existence proof is a single-premise rule, strip that wrapping rule + // and use its premise; otherwise use the proof as-is (an existence proof need + // not be rule-justified — `check_proof` below validates it either way). Which + // shape arises depends on the witness row, chosen deterministically above, so + // this is stable across runs and backends. let proof = proof_store.get(proof_id); let extra_rule_removed = match proof.justification() { Justification::Rule { premise_proofs, .. } => match premise_proofs.as_slice() { [premise_proof_id] => *premise_proof_id, _ => proof_id, }, - _ => panic!("expected rule justification for existence proof"), + _ => proof_id, }; // Check the proof before simplification diff --git a/egglog/src/proofs/proof_extractor.rs b/egglog/src/proofs/proof_extractor.rs index 534ce18d..463eac3c 100644 --- a/egglog/src/proofs/proof_extractor.rs +++ b/egglog/src/proofs/proof_extractor.rs @@ -96,7 +96,9 @@ impl RootExtractor { sort: &ArcSort, ) -> Option { for func in egraph.functions.values() { - if func.decl.subtype != FunctionSubtype::Constructor + // Term/proof relations (function-to-Unit, id in the last input) and + // ordinary constructors both reconstruct here; views are skipped. + if (func.decl.subtype != FunctionSubtype::Constructor && !func.is_relation_term()) || func.extraction_output_sort().name() != sort.name() || func.decl.term_constructor.is_some() { @@ -112,6 +114,10 @@ impl RootExtractor { matching_rows.push(row.vals.to_vec()); } }); + // Reconstruct from the lexicographically-smallest matching row so the + // chosen term does not depend on the backend's (possibly nondeterministic) + // row iteration order — see `prove_exists`. + matching_rows.sort(); for row in matching_rows { let num_children = func.extraction_num_children(); diff --git a/egglog/src/proofs/proof_format.rs b/egglog/src/proofs/proof_format.rs index 9861de45..4153f930 100644 --- a/egglog/src/proofs/proof_format.rs +++ b/egglog/src/proofs/proof_format.rs @@ -1,9 +1,14 @@ use crate::{ ResolvedCall, Term, TermDag, TermId, - ast::{FunctionSubtype, ResolvedExpr, ResolvedFact, ResolvedNCommand}, - proofs::{proof_checker::gather_globals, proof_encoding_helpers::EncodingNames}, + ast::{FunctionSubtype, GenericNCommand, ResolvedExpr, ResolvedFact, ResolvedNCommand}, + proofs::{ + proof_checker::{ + ProofCheckError, ProofCheckErrorKind, eval_expr_with_subst, gather_globals, run_merge, + }, + proof_encoding_helpers::EncodingNames, + }, typechecking::{FuncType, PrimitiveValidator}, - util::{HEntry, HashMap, IndexSet, SymbolGen}, + util::{HEntry, HashMap, HashSet, IndexSet, SymbolGen}, }; use egglog_ast::generic_ast::Literal; use egglog_numeric_id::{DenseIdMap, NumericId, define_id}; @@ -22,6 +27,73 @@ impl fmt::Display for ProofId { } } +/// Find the subexpression at pre-order position `idx` in `expr`'s tree (index 0 +/// is `expr` itself). Must mirror the indexing the proof encoder uses to tag +/// `MergeFnIdx` proofs. +fn subexpr_at_index(expr: &ResolvedExpr, idx: usize) -> Option<&ResolvedExpr> { + let mut counter = 0; + fn walk<'a>( + expr: &'a ResolvedExpr, + target: usize, + counter: &mut usize, + ) -> Option<&'a ResolvedExpr> { + if *counter == target { + return Some(expr); + } + *counter += 1; + if let ResolvedExpr::Call(_, _, args) = expr { + for arg in args { + if let Some(found) = walk(arg, target, counter) { + return Some(found); + } + } + } + None + } + walk(expr, idx, &mut counter) +} + +/// Run subexpression `idx` of a function's merge body with `old`/`new` bound to +/// `old_term`/`new_term`, returning the resulting term. `idx` is a pre-order +/// index over the merge body tree (see [`subexpr_at_index`]); `idx == 0` is the +/// whole body. Evaluating the subexpression reconstructs the term the FD +/// custom-function view merge minted at that position, so each nested +/// merge-body subexpression yields its own conclusion. Used when converting a +/// `MergeFnIdx`/`MergeFnRow` raw proof into its `MergeFn` conclusion. +fn run_merge_subexpr( + term_dag: &mut TermDag, + func_name: &str, + prog: &[ResolvedNCommand], + old_term: TermId, + new_term: TermId, + idx: usize, +) -> Result<(TermId, HashSet), ProofCheckError> { + let mut subst = HashMap::default(); + subst.insert("old".to_string(), old_term); + subst.insert("new".to_string(), new_term); + for cmd in prog { + if let GenericNCommand::Function(func_decl) = cmd + && func_decl.name == func_name + { + let merge = func_decl.merge.as_ref().ok_or_else(|| { + ProofCheckError::from(ProofCheckErrorKind::FunctionNotFound { + function_name: func_name.to_string(), + }) + })?; + let subexpr = subexpr_at_index(&merge.result, idx).ok_or_else(|| { + ProofCheckError::from(ProofCheckErrorKind::FunctionNotFound { + function_name: format!("{func_name} (merge subexpr index {idx} out of range)"), + }) + })?; + return eval_expr_with_subst("merge_function", subexpr, term_dag, &subst); + } + } + Err(ProofCheckErrorKind::FunctionNotFound { + function_name: func_name.to_string(), + } + .into()) +} + /// A proof straight from the e-graph, not exposed to users. struct RawProofStore { term_dag: TermDag, @@ -58,10 +130,19 @@ enum RawProof { /// Given a rule name and proofs for each premise, produces a proof of a grounded equality t1 = t2 from the body of the rule. /// The subsitution is implicit- in [`ProofTerm`] they are explicit. Rule(String, Vec, TermId, TermId), - /// Given two proofs f(c1, c2, ..., old) = f(c1, c2, ..., old) and f(c1, c2, ..., new) = f(c1, c2, ..., new) and a term t produces a proof - /// of t = t. - /// The term t is either f(c1, c2, ..., merge_fn) or some subexpression of the merge function. Here the merge function is evaluted on the terms old and new. - MergeFn(String, RawProofId, RawProofId, TermId), + /// A term-free merge proof: given proofs `f(…, old) = f(…, old)` and + /// `f(…, new) = f(…, new)`, the index `idx` identifies which subexpression of the + /// merge body this justifies (a pre-order index over the body tree). The + /// conclusion is reconstructed during conversion by evaluating subexpression + /// `idx` on the premise outputs; the index distinguishes nested subexpressions + /// that share the same premises. Used by the FD custom-function view merge, which + /// runs without access to children. + MergeFnIdx(String, RawProofId, RawProofId, usize), + /// Like [`RawProof::MergeFnIdx`] but for the FD view row (no index). The conclusion + /// `f(children) = eval(whole merge body)` is reconstructed during conversion by + /// running the whole body on the two premise outputs. Used as the proof column of + /// every FD pair-valued view's `:merge`. + MergeFnRow(String, RawProofId, RawProofId), Trans(RawProofId, RawProofId), Sym(RawProofId), /// given a proof that t1 = f(..., ci, ...) @@ -257,13 +338,19 @@ impl RawProofStore { let name = self.parse_string(args[0]); let premises = self.parse_proof_list(args[1]); RawProof::Rule(name, premises, args[2], args[3]) - } else if head == self.names.merge_fn_constructor { - assert!(args.len() == 4, "merge constructor should have 4 args"); + } else if head == self.names.merge_fn_idx_constructor { + assert!(args.len() == 4, "merge-idx constructor should have 4 args"); + let function = self.parse_string(args[0]); + let old_proof = self.parse_proof(args[1]); + let new_proof = self.parse_proof(args[2]); + let idx = self.parse_index(args[3]); + RawProof::MergeFnIdx(function, old_proof, new_proof, idx) + } else if head == self.names.merge_fn_row_constructor { + assert!(args.len() == 3, "merge-row constructor should have 3 args"); let function = self.parse_string(args[0]); let old_proof = self.parse_proof(args[1]); let new_proof = self.parse_proof(args[2]); - let term = args[3]; - RawProof::MergeFn(function, old_proof, new_proof, term) + RawProof::MergeFnRow(function, old_proof, new_proof) } else if head == self.names.eq_trans_constructor { assert!(args.len() == 2, "trans constructor should have 2 args"); let left = self.parse_proof(args[0]); @@ -363,6 +450,18 @@ impl RawProofStore { } } +/// True iff `fact` is a custom-function application fact `(= (f args) v)` (either +/// argument order), for which the checker's proof normal form expects a *reflexive* +/// premise proof. Constructor and plain equality facts are excluded. +fn is_custom_func_fact(fact: &ResolvedFact) -> bool { + let call = match fact { + ResolvedFact::Eq(_, ResolvedExpr::Call(_, c, _), ResolvedExpr::Var(..)) + | ResolvedFact::Eq(_, ResolvedExpr::Var(..), ResolvedExpr::Call(_, c, _)) => c, + _ => return false, + }; + matches!(call, ResolvedCall::Func(ft) if ft.subtype == FunctionSubtype::Custom) +} + impl ProofStore { /// Get the term DAG used by this proof store. pub fn term_dag(&self) -> &TermDag { @@ -419,6 +518,80 @@ impl ProofStore { (store, proof_id) } + /// Reflexivize a (possibly non-reflexive) proof so it can serve as a `MergeFn` + /// premise (the checker requires premises to be reflexive, `lhs == rhs`). For + /// `p : A = B` returns a proof of `B = B` as `Trans(Sym(p), p)`; an already- + /// reflexive `p` is returned unchanged. + /// + /// This handles eq-sort inputs to FD custom functions: rebuild rewrites the + /// view row's proof into a congruence proof `f(orig) = f(canon)`, and + /// reflexivizing to its RHS lands both premises on the same canonical view row + /// so the checker's input-match succeeds. + fn reflexivize_premise(&mut self, premise_id: ProofId) -> ProofId { + let prop = self.id_to_proof[premise_id].proposition.clone(); + if prop.lhs == prop.rhs { + return premise_id; + } + // Sym(p) : rhs = lhs + let sym_id = self.id_to_proof.push(Proof { + proposition: Proposition::new(prop.rhs, prop.lhs), + justification: Justification::Sym(premise_id), + }); + // Trans(Sym(p), p) : rhs = rhs + self.id_to_proof.push(Proof { + proposition: Proposition::new(prop.rhs, prop.rhs), + justification: Justification::Trans(sym_id, premise_id), + }) + } + + /// The two `MergeFn*` premise proofs are reflexive proofs of colliding view + /// terms `f(inputs.., output)`. Extract the view head, the shared input args, + /// and the two output values from the premises' rhs (read before reflexivizing). + fn merge_premise_view( + &self, + old_proof_id: ProofId, + new_proof_id: ProofId, + ) -> (String, Vec, TermId, TermId) { + let old_view = self.id_to_proof[old_proof_id].rhs(); + let new_view = self.id_to_proof[new_proof_id].rhs(); + match (self.term_dag.get(old_view), self.term_dag.get(new_view)) { + (Term::App(old_head, old_args), Term::App(_new_head, new_args)) => { + let head = old_head.clone(); + let old_output = *old_args.last().expect("merge view term has no args"); + let new_output = *new_args.last().expect("merge view term has no args"); + let inputs = old_args[..old_args.len() - 1].to_vec(); + (head, inputs, old_output, new_output) + } + _ => panic!( + "MergeFn premise proofs should prove function application terms, got {:?} and {:?}", + self.term_dag.get(old_view), + self.term_dag.get(new_view) + ), + } + } + + /// Build a `MergeFn` proof of `to_prove = to_prove` from the two premises, + /// reflexivizing each (rebuild can rewrite an eq-sort-input premise into a + /// non-reflexive congruence proof; reflexive premises pass through unchanged). + fn merge_fn_proof( + &mut self, + function: &str, + old_proof_id: ProofId, + new_proof_id: ProofId, + to_prove: TermId, + ) -> Proof { + let old_proof = self.reflexivize_premise(old_proof_id); + let new_proof = self.reflexivize_premise(new_proof_id); + Proof { + proposition: Proposition::new(to_prove, to_prove), + justification: Justification::MergeFn { + function: function.to_string(), + old_proof, + new_proof, + }, + } + } + /// Converts a raw proof into a user-facing proof, recursively converting sub-proofs as needed. /// This adds new metadata to the proof, such as the substitution for rules. /// @@ -449,6 +622,40 @@ impl ProofStore { .map(|pid| self.convert_raw_proof(prog, globals, raw_store, *pid)) .collect(); + // Rebuild/canonicalization can rewrite a matched custom-function-fact + // premise `(= (f args) v)` into a non-reflexive natural->canonical + // `Congr` proof `(f nat) = (f canon)` (e.g. when an argument's e-class + // has several equivalent shapes from commutativity/associativity + // rewrites). The checker's function-fact normal form expects a + // reflexive premise at the matched (canonical) shape, so reflexivize + // those. Equality-fact premises `(= a b)` must stay non-reflexive. + let reflex_mask: Vec = { + let rule = prog + .iter() + .find_map(|cmd| match cmd { + ResolvedNCommand::NormRule { rule } if rule.name == *name => Some(rule), + _ => None, + }) + .unwrap_or_else(|| panic!("could not find rule with name {name}")); + rule.body.iter().map(is_custom_func_fact).collect() + }; + // `zip` intentionally aligns premises with the body facts and stops + // at the shorter: a body fact can contribute more than one premise + // proof (e.g. a container side condition), so there may be more + // premises than facts, and only the leading per-fact ones carry a + // reflexivization decision. + let converted_premises: Vec = converted_premises + .into_iter() + .zip(reflex_mask) + .map(|(pid, reflex)| { + if reflex { + self.reflexivize_premise(pid) + } else { + pid + } + }) + .collect(); + let mut substitution = self.compute_rule_substitution(prog, name, &converted_premises); // remove globals from the substitution, since they are not necessary @@ -466,18 +673,49 @@ impl ProofStore { }, } } - RawProof::MergeFn(function, old_raw, new_raw, to_prove) => { + RawProof::MergeFnIdx(function, old_raw, new_raw, idx) => { let old_proof_id = self.convert_raw_proof(prog, globals, raw_store, *old_raw); let new_proof_id = self.convert_raw_proof(prog, globals, raw_store, *new_raw); - let to_prove = raw_store.unwrap_ast(*to_prove); - Proof { - proposition: Proposition::new(to_prove, to_prove), - justification: Justification::MergeFn { - function: function.clone(), - old_proof: old_proof_id, - new_proof: new_proof_id, - }, - } + // The two premise proofs are reflexive proofs of the colliding view terms + // `f(c..., old_output)` and `f(c..., new_output)`. We extract the two outputs + // and reconstruct the conclusion term by evaluating subexpression `idx` of + // `function`'s merge body on those outputs (`old`/`new` bound accordingly). + // + // `idx` indexes all body nodes (pre-order, top node included). The + // conclusion is that node's own minted term, i.e. its existence proof in + // its FD view. The whole-view-row conclusion comes from `MergeFnRow`. + // The conclusion is subexpression `idx`'s own minted term (its + // existence proof in its FD view); the whole-view-row conclusion comes + // from `MergeFnRow`. + let (_head, _inputs, old_output, new_output) = + self.merge_premise_view(old_proof_id, new_proof_id); + let (to_prove, _props) = run_merge_subexpr( + &mut self.term_dag, + function, + prog, + old_output, + new_output, + *idx, + ) + .unwrap_or_else(|e| { + panic!("failed to run merge subexpr {idx} for {function}: {e}") + }); + self.merge_fn_proof(function, old_proof_id, new_proof_id, to_prove) + } + RawProof::MergeFnRow(function, old_raw, new_raw) => { + let old_proof_id = self.convert_raw_proof(prog, globals, raw_store, *old_raw); + let new_proof_id = self.convert_raw_proof(prog, globals, raw_store, *new_raw); + // The conclusion is the whole view row `f(inputs..., merged)`, where + // `merged` is the whole merge body evaluated on the two premise outputs. + let (view_head, input_args, old_output, new_output) = + self.merge_premise_view(old_proof_id, new_proof_id); + let (merged_child, _props) = + run_merge(&mut self.term_dag, function, prog, old_output, new_output) + .unwrap_or_else(|e| panic!("failed to run merge for {function}: {e}")); + let mut merged_args = input_args; + merged_args.push(merged_child); + let to_prove = self.term_dag.app(view_head, merged_args); + self.merge_fn_proof(function, old_proof_id, new_proof_id, to_prove) } RawProof::Trans(left_raw, right_raw) => { let left_id = self.convert_raw_proof(prog, globals, raw_store, *left_raw); diff --git a/egglog/src/proofs/proof_fresh.rs b/egglog/src/proofs/proof_fresh.rs new file mode 100644 index 00000000..071282f3 --- /dev/null +++ b/egglog/src/proofs/proof_fresh.rs @@ -0,0 +1,218 @@ +//! The term/proof encoding's mint + canonicalize primitives. +//! +//! With terms and proofs encoded as relations (rather than constructors), an +//! e-node / proof-node id is no longer minted by a constructor call. Instead the +//! encoding mints a fresh id explicitly and asserts the relation row: +//! +//! ```text +//! (let fresh (@get-fresh-Math!)) +//! (Add a b fresh) +//! ``` +//! +//! These primitives (`get-fresh!`, `set-if-empty`, and its proof-column reader) +//! carry only type constraints here; their runtime behavior is minted by the +//! backend SPI ([`Backend::register_get_fresh`] / [`Backend::register_set_if_empty`] +//! / [`Backend::register_view_proof`]) so each backend services the mint / +//! canonicalize against its own storage — db tables for the reference bridge, a +//! host-side mirror for the Differential Dataflow backend. + +use crate::*; + +/// Deterministic name of an FD view's `set-if-empty` primitive. The stable +/// `set-if-empty-` prefix carries no internal-symbol marker, so a name-sanitizer +/// leaves it alone; only the embedded `view_name` (a fresh internal symbol) is +/// rewritten, and it is rewritten identically here and at the view's declaration, +/// so the primitive stays resolvable when the desugared program is re-parsed. +pub(crate) fn set_if_empty_prim_name(view_name: &str) -> String { + format!("set-if-empty-{view_name}!") +} + +/// Deterministic name of an FD view's proof-column read primitive. See +/// [`set_if_empty_prim_name`] for why the prefix carries no internal marker. +pub(crate) fn view_proof_prim_name(view_name: &str) -> String { + format!("view-proof-{view_name}") +} + +/// Register an FD view's `set-if-empty` primitive and (in proof mode) its +/// proof-column reader, so the encoding can canonicalize a freshly-built term +/// to the view's canonical e-class at insertion time. `out_sorts` is the view's +/// output tuple `(eclass, proof)` (proof is `Unit` when proofs are off). The +/// runtime entrypoint is minted by the backend so the op reads/writes the +/// backend's own view storage. +pub(crate) fn register_set_if_empty( + eg: &mut EGraph, + view_name: &str, + key_sorts: Vec, + out_sorts: Vec, +) { + let n_keys = key_sorts.len(); + let out_arity = out_sorts.len(); + let set_if_empty = SetIfEmpty { + name: set_if_empty_prim_name(view_name), + key_sorts: key_sorts.clone(), + out_sorts: out_sorts.clone(), + eclass_sort: out_sorts[0].clone(), + }; + let name = view_name.to_string(); + eg.add_backend_op_primitive( + set_if_empty, + WriteState::valid_contexts(), + move |backend, _| backend.register_set_if_empty(name.clone(), n_keys, out_arity), + ); + + // The proof column reader is only meaningful in proof mode (2-output view). + if out_sorts.len() >= 2 { + let view_proof = ViewProof { + name: view_proof_prim_name(view_name), + key_sorts, + proof_sort: out_sorts[1].clone(), + }; + let name = view_name.to_string(); + eg.add_backend_op_primitive( + view_proof, + WriteState::valid_contexts(), + move |backend, _| backend.register_view_proof(name.clone(), n_keys), + ); + } +} + +/// `set-if-empty`: get-or-insert-with-default on an FD view. Looks up +/// `(view keys)`; if present returns its e-class (column 0), else inserts +/// `(keys default_eclass default_proof)` and returns `default_eclass`. This lets +/// the encoding thread canonical e-classes through term construction so the view +/// tables stay canonical (nothing to re-key at rebuild). The lookup/insert is +/// serviced by the backend against its own view storage. +#[derive(Clone)] +struct SetIfEmpty { + name: String, + key_sorts: Vec, + out_sorts: Vec, + eclass_sort: ArcSort, +} + +impl Primitive for SetIfEmpty { + fn name(&self) -> &str { + &self.name + } + fn get_type_constraints(&self, span: &Span) -> Box { + // (keys… default_eclass default_proof) -> eclass + let mut sig = self.key_sorts.clone(); + sig.extend(self.out_sorts.iter().cloned()); + sig.push(self.eclass_sort.clone()); + SimpleTypeConstraint::new(&self.name, sig, span.clone()).into_box() + } +} + +/// Reads an FD view's proof column (column 1) by its key, for building the +/// `fresh = canonical` connector proof after `set-if-empty`. +/// +/// Signature `(keys… fallback) -> proof`: returns the committed view proof for +/// the key, or `fallback` when the key is absent. The fallback lets the caller +/// build `Trans(term_proof, Sym(view_proof))` uniformly — when the view was just +/// seeded (empty at read time) the caller passes the term proof itself, so the +/// connector collapses to a reflexive `fresh = fresh`. Serviced by the backend +/// against its own view storage. +#[derive(Clone)] +struct ViewProof { + name: String, + key_sorts: Vec, + proof_sort: ArcSort, +} + +impl Primitive for ViewProof { + fn name(&self) -> &str { + &self.name + } + fn get_type_constraints(&self, span: &Span) -> Box { + // (keys… fallback_proof) -> proof + let mut sig = self.key_sorts.clone(); + sig.push(self.proof_sort.clone()); + sig.push(self.proof_sort.clone()); + SimpleTypeConstraint::new(&self.name, sig, span.clone()).into_box() + } +} + +/// Name of the single generic mint primitive. It takes the target sort as a +/// string literal — `(get-fresh! "Math")` — so one primitive serves every +/// eq-sort and the desugared program references a stable, always-registered name +/// (rather than a per-sort `@`-name a name-sanitizer would mangle on re-parse). +pub(crate) const GET_FRESH_PRIM_NAME: &str = "get-fresh!"; + +/// Register the generic `get-fresh!` primitive, minting from the backend's +/// eq-class id counter. Called once when the term/proof encoding is enabled (see +/// [`EGraph::enable_term_encoding`]); the registration walks the typechecker +/// chain, so the primitive is available both during encoding and when the +/// desugared program is re-parsed. A no-op on backends without an id counter +/// (those assign ids deterministically and need no mint primitive). +pub(crate) fn register_get_fresh(eg: &mut EGraph) { + // No counter → the backend assigns ids deterministically; nothing to mint. + if eg.backend.eclass_id_counter().is_none() { + return; + } + eg.add_backend_op_primitive(GetFresh, WriteState::valid_contexts(), |backend, _| { + backend.register_get_fresh() + }); +} + +/// `get-fresh! "Sort" -> Sort`: mint a fresh id of the named eq-sort from the +/// shared eq-class id counter. Impure — every call returns a new id. The leading +/// string names the output sort (its runtime ignores the arg and just mints); the +/// mint itself is serviced by the backend. +#[derive(Clone)] +struct GetFresh; + +impl Primitive for GetFresh { + fn name(&self) -> &str { + GET_FRESH_PRIM_NAME + } + fn get_type_constraints(&self, span: &Span) -> Box { + Box::new(GetFreshTypeConstraint { span: span.clone() }) + } +} + +/// `(get-fresh! "Sort") -> Sort`: the leading string literal names the output +/// eq-sort; the output is constrained to that sort. +struct GetFreshTypeConstraint { + span: Span, +} + +impl TypeConstraint for GetFreshTypeConstraint { + fn get( + &self, + arguments: &[crate::core::AtomTerm], + typeinfo: &TypeInfo, + ) -> Vec>> { + // `("Sort") -> out`: two signature entries (the string arg and the output). + let [arg, out] = arguments else { + return vec![crate::constraint::impossible( + crate::constraint::ImpossibleConstraint::ArityMismatch { + atom: crate::core::Atom { + span: self.span.clone(), + head: GET_FRESH_PRIM_NAME.to_string(), + args: arguments.to_vec(), + }, + expected: 2, + }, + )]; + }; + let string_sort = typeinfo.get_sort_by_name("String"); + // At real type-checking time the first arg is the sort-name string literal; + // resolve the output eq-sort from it. At `accept`/resolution time the + // constraint is run over placeholder literals (no real string), so fall + // back to only requiring the first arg to be a `String` — the output sort + // then comes from the already-resolved types. + if let crate::core::AtomTerm::Literal(_, crate::ast::Literal::String(sort_name)) = arg + && let Some(out_sort) = typeinfo.get_sort_by_name(sort_name) + { + let mut cs = vec![crate::constraint::assign(out.clone(), out_sort.clone())]; + if let Some(ss) = string_sort { + cs.push(crate::constraint::assign(arg.clone(), ss.clone())); + } + return cs; + } + match string_sort { + Some(ss) => vec![crate::constraint::assign(arg.clone(), ss.clone())], + None => vec![], + } + } +} diff --git a/egglog/src/proofs/proof_tests.rs b/egglog/src/proofs/proof_tests.rs index 50cda2e1..f3c1369a 100644 --- a/egglog/src/proofs/proof_tests.rs +++ b/egglog/src/proofs/proof_tests.rs @@ -179,22 +179,30 @@ mod tests { ); let rule_name_var = rule_name_vars[0]; - let mut rule_uses = 0; - rule.head.clone().visit_exprs(&mut |expr| { - if let ResolvedExpr::Call(_, ResolvedCall::Func(func), args) = &expr - && func.name == rule_constructor - { - rule_uses += 1; - assert!( - matches!( - args.first(), - Some(ResolvedExpr::Var(_, var)) if var.name == rule_name_var - ), - "generated Rule constructor did not reuse the rule-name variable" - ); - } - expr - }); + // Proof constructors are relations, so each `Rule` proof is emitted as a + // `(set (@Rule ) ())` action, not a + // call expression. Count those set actions and check they reuse the hoisted + // rule-name variable as their first argument. + let rule_uses = rule + .head + .0 + .iter() + .filter(|action| match action { + ResolvedAction::Set(_, ResolvedCall::Func(func), args, _) + if func.name == rule_constructor => + { + assert!( + matches!( + args.first(), + Some(ResolvedExpr::Var(_, var)) if var.name == rule_name_var + ), + "generated Rule constructor did not reuse the rule-name variable" + ); + true + } + _ => false, + }) + .count(); assert!( rule_uses > 1, "expected the multi-action rule to emit multiple Rule constructors" diff --git a/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap b/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap index b9faf24b..ee2902ed 100644 --- a/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap +++ b/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap @@ -14,7 +14,7 @@ expression: snapshot ((set (__UF_Math __uf_a) (values __uf_c ()))) :ruleset __parent :name "__uf_path_compress") (sort __view) -(constructor Add (i64 i64) Math :unextractable :internal-hidden) +(function Add (i64 i64 Math) Unit :no-merge :unextractable :internal-hidden) (function __AddView (i64 i64) (Math Unit) :merge ((set (__UF_Math (ordering-max old0 new0)) (values (ordering-min old0 new0) ())) (values (ordering-min old0 new0) ())) :internal-term-constructor Add :internal-identity-vals 1) (constructor __to_delete_Add (i64 i64) __view :internal-hidden) (constructor __to_subsume_Add (i64 i64) __view :internal-hidden) @@ -33,17 +33,20 @@ expression: snapshot ((set (__AddView c0_ c1_) (values __v6 ()))) :ruleset __rebuilding :name "__rebuild_rule" :internal-include-subsumed) (function __v8 () Math :no-merge :unextractable :internal-let) -(set (__v8) (Add 1 2)) -(set (__AddView 1 2) (values (__v8) ())) -(run-schedule (seq (saturate (seq (run __rebuilding_cleanup) (saturate (run __parent)) (run __rebuilding))) (run __delete_subsume_ruleset))) -(rule ((= (values __v9 __v10) (__AddView a b))) - ((let __v12 (Add a b)) - (set (__AddView a b) (values __v12 ())) - (let __v13 (Add b a)) - (set (__AddView b a) (values __v13 ())) - (set (__UF_Math (ordering-max __v12 __v13)) (values (ordering-min __v12 __v13) ()))) +(set (__v8) (get-fresh! "Math")) +(set (Add 1 2 (__v8)) ()) +(function __v9 () Math :no-merge :unextractable :internal-let) +(set (__v9) (set-if-empty-__AddView! 1 2 (__v8) ())) +(rule ((= (values __v10 __v11) (__AddView a b))) + ((let __v13 (get-fresh! "Math")) + (set (Add a b __v13) ()) + (let __v14 (set-if-empty-__AddView! a b __v13 ())) + (let __v15 (get-fresh! "Math")) + (set (Add b a __v15) ()) + (let __v16 (set-if-empty-__AddView! b a __v15 ())) + (set (__UF_Math (ordering-max __v14 __v16)) (values (ordering-min __v14 __v16) ()))) :name "commutativity") -(check (= (values __v14 __v15) (__AddView 1 2)) -(= (values __v16 __v17) (__AddView 2 1)) -(= __v14 __v16)) +(check (= (values __v17 __v18) (__AddView 1 2)) +(= (values __v19 __v20) (__AddView 2 1)) +(= __v17 __v19)) (run-schedule (seq (saturate (seq (run __rebuilding_cleanup) (saturate (run __parent)) (run __rebuilding))) (run __delete_subsume_ruleset))) diff --git a/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap b/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap index 4591a0e4..4a1bccb1 100644 --- a/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap +++ b/egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap @@ -1,5 +1,5 @@ --- -source: src/proofs/proof_tests.rs +source: egglog/src/proofs/proof_tests.rs expression: snapshot --- (ruleset __parent) @@ -7,43 +7,19 @@ expression: snapshot (ruleset __rebuilding_cleanup) (ruleset __delete_subsume_ruleset) (sort __view) -(constructor add (i64 i64 i64) __view :unextractable :internal-hidden) -(function __addView (i64 i64 i64) Unit :merge old :unextractable :internal-term-constructor add) +(function add (i64 i64 i64 __view) Unit :no-merge :unextractable :internal-hidden) +(function __addView (i64 i64) (i64 Unit) :merge (values old0 ()) :unextractable :internal-term-constructor add :internal-identity-vals 1) (constructor __to_delete_add (i64 i64) __view :internal-hidden) (constructor __to_subsume_add (i64 i64) __view :internal-hidden) -(function __addCurrent (i64 i64) i64 :merge old :unextractable :internal-hidden) -(sort __mergecleanupsort) -(constructor __mergecleanup (i64 i64) __mergecleanupsort :internal-hidden) -(rule ((__addView c0_ c1_ old) - (__addView c0_ c1_ new) - (!= old new) - (= (ordering-max old new) new)) - ((set (__addView c0_ c1_ old) ()) - (set (__addCurrent c0_ c1_) old) - (__mergecleanup old old) - (__mergecleanup old new)) - :ruleset __rebuilding :name "__merge_rule") -(rule ((__mergecleanup merged old) - (__addView c0_ c1_ merged) - (__addView c0_ c1_ old) - (!= merged old)) - ((delete (__addView c0_ c1_ old))) - :ruleset __rebuilding_cleanup :name "__merge_cleanup") -(rule ((= selected (__addCurrent c0_ c1_)) - (__addView c0_ c1_ selected) - (__addView c0_ c1_ old) - (!= selected old)) - ((delete (__addView c0_ c1_ old))) - :ruleset __rebuilding_cleanup :name "__merge_current_cleanup") (rule ((__to_delete_add c0_ c1_) - (__addView c0_ c1_ out)) - ((delete (__addView c0_ c1_ out)) + (= (values __v __v1) (__addView c0_ c1_))) + ((delete (__addView c0_ c1_)) (delete (__to_delete_add c0_ c1_))) :ruleset __delete_subsume_ruleset :name "__delete_rule") (rule ((__to_subsume_add c0_ c1_) - (__addView c0_ c1_ out)) - ((subsume (__addView c0_ c1_ out))) + (= (values __v2 __v3) (__addView c0_ c1_))) + ((subsume (__addView c0_ c1_))) :ruleset __delete_subsume_ruleset :name "__delete_rule_subsume") -(check (= __v (__addView 0 0 __n)) +(check (= (values __n __v4) (__addView 0 0)) (= __n 0)) (run-schedule (seq (saturate (seq (run __rebuilding_cleanup) (saturate (run __parent)) (run __rebuilding))) (run __delete_subsume_ruleset))) diff --git a/egglog/src/typechecking.rs b/egglog/src/typechecking.rs index 5d83eb04..5b073340 100644 --- a/egglog/src/typechecking.rs +++ b/egglog/src/typechecking.rs @@ -383,6 +383,29 @@ impl EGraph { }); } + /// Register a term-encoding op primitive whose runtime entrypoint is minted + /// by the backend SPI (`register_get_fresh` / `register_set_if_empty` / + /// `register_view_proof`) rather than by wrapping the primitive in a registry + /// action wrapper. `prim` supplies only the type constraints (its body is + /// never invoked); `make_id` asks each backend on the typechecker chain for + /// the [`ExternalFunctionId`] that services this op against that backend's + /// own storage. This bypasses the `action_registry()`-is-`Some` gate in + /// [`Self::register_registry_primitive`], so a backend without a registry + /// (e.g. Differential Dataflow) can still service these ops. + pub(crate) fn add_backend_op_primitive( + &mut self, + prim: T, + valid_ctxs: &[Context], + mut make_id: F, + ) where + T: Primitive + Clone, + F: FnMut(&mut dyn Backend, Context) -> ExternalFunctionId, + { + self.register_per_context(prim, None, valid_ctxs, move |backend, _x, ctx| { + make_id(backend, ctx) + }); + } + /// Shared registration engine. Stores one primitive definition, plus /// one runtime id per valid [`Context`]. Each wrapper carries its /// specific context stamped onto the state wrapper at invoke time. @@ -454,6 +477,19 @@ impl EGraph { let command: ResolvedNCommand = match command { NCommand::Function(fdecl) => { let resolved = self.type_info.typecheck_function(symbol_gen, fdecl)?; + // An FD view (function carrying `term_constructor` with a tuple + // `(eclass, proof)` output) gets a `set-if-empty` primitive (+ a + // proof-column reader) so the encoding can canonicalize a term to + // the view's e-class at insertion time. Registered here so it + // survives re-parse of the desugared program. + if resolved.term_constructor.is_some() + && let ResolvedCall::Func(ft) = &resolved.resolved_schema + && ft.outputs.len() >= 2 + { + let (name, input, outputs) = + (resolved.name.clone(), ft.input.clone(), ft.outputs.clone()); + crate::proofs::proof_fresh::register_set_if_empty(self, &name, input, outputs); + } // If this is a let binding, add it to global_sorts // This preserves bahavior for lets after desugaring if resolved.internal_let { @@ -490,10 +526,15 @@ impl EGraph { // run_command also does) so the container rebuild registration // below can recover them — including this container's own proof // table, which has not run yet. - if let Some((uf_ctor, _uf_index)) = uf { + if let Some((uf_ctor, _uf_index, uf_aux)) = uf { self.proof_state .uf_parent .insert(name.clone(), uf_ctor.clone()); + if let Some(uf_aux) = uf_aux { + self.proof_state + .uf_aux_parent + .insert(name.clone(), uf_aux.clone()); + } } if let Some(pf) = proof_func { self.proof_state @@ -594,9 +635,12 @@ impl EGraph { span.clone(), self.type_info.typecheck_facts(symbol_gen, facts)?, ), - NCommand::Fail(span, cmd) => { - ResolvedNCommand::Fail(span.clone(), Box::new(self.typecheck_command(cmd)?)) - } + NCommand::Fail(span, cmds) => ResolvedNCommand::Fail( + span.clone(), + cmds.iter() + .map(|cmd| self.typecheck_command(cmd)) + .collect::>()?, + ), NCommand::RunSchedule(schedule) => ResolvedNCommand::RunSchedule( self.type_info.typecheck_schedule(symbol_gen, schedule)?, ), @@ -629,16 +673,13 @@ impl EGraph { ResolvedNCommand::PrintSize(span.clone(), n.clone()) } NCommand::ProveExists(span, constructor) => { + // prove-exists targets a table: a constructor, or its lowering to + // a term relation (a function) under the term/proof encoding. + // `get_func_type` already rejects primitives/unbound names. let func_type = self .type_info .get_func_type(constructor) .ok_or_else(|| TypeError::UnboundFunction(constructor.clone(), span.clone()))?; - if func_type.subtype != FunctionSubtype::Constructor { - return Err(TypeError::ProveExistsRequiresConstructor( - constructor.clone(), - span.clone(), - )); - } ResolvedNCommand::ProveExists(span.clone(), ResolvedCall::Func(func_type.clone())) } NCommand::Output { span, file, exprs } => { @@ -1388,8 +1429,6 @@ pub enum TypeError { UndefinedSort(String, Span), #[error("{1}\nUnbound function {0}")] UnboundFunction(String, Span), - #[error("{1}\nprove-exists requires constructor function, but {0} is not a constructor")] - ProveExistsRequiresConstructor(String, Span), #[error("{1}\nFunction already bound {0}")] FunctionAlreadyBound(String, Span), #[error("{1}\nSort {0} already declared.")] diff --git a/egglog/tests/eggcc-2mm.egg b/egglog/tests/eggcc-2mm.egg index 5ba6aa21..ca1cd9c5 100644 --- a/egglog/tests/eggcc-2mm.egg +++ b/egglog/tests/eggcc-2mm.egg @@ -132,7 +132,7 @@ (constructor TermConcat (Term Term) Term) (function LoopNumItersGuess (Expr Expr) i64 :merge (max 1 (min old new))) (relation RELIESONCONTEXT ()) -(function DUMMYCTX () Assumption :no-merge) +(function DUMMYCTX () Assumption :merge old) (set (DUMMYCTX) (InFunc "DUMMY")) (ruleset never) (ruleset type-analysis) @@ -144,7 +144,7 @@ (rule ((= _rewrite_var__86 (TLConcat (TCons hd tl) r))) ((union _rewrite_var__86 (TCons hd (TLConcat tl r)))) :ruleset type-helpers :name "(rewrite (TLConcat (TCons hd tl) r) (TCons hd (TLConcat tl r)) :ruleset type-helpers)") -(function TypeList-length (TypeList) i64 :no-merge) +(function TypeList-length (TypeList) i64 :merge old) (constructor TypeList-ith (TypeList i64) BaseType :unextractable) (rule () ((set (TypeList-length (TNil )) 0)) @@ -852,7 +852,7 @@ (PureTypeList tl)) ((PureTypeList (TCons hd tl))) :ruleset type-analysis )") -(function ListExpr-length (ListExpr) i64 :no-merge) +(function ListExpr-length (ListExpr) i64 :merge old) (constructor ListExpr-ith (ListExpr i64) Expr :unextractable) (constructor ListExpr-suffix (ListExpr i64) ListExpr :unextractable) (constructor Append (ListExpr Expr) ListExpr :unextractable) @@ -879,7 +879,7 @@ (rule ((= _rewrite_var__633 (Append (Nil ) e))) ((union _rewrite_var__633 (Cons e (Nil )))) :ruleset always-run :name "(rewrite (Append (Nil ) e) (Cons e (Nil )) :ruleset always-run)") -(function tuple-length (Expr) i64 :no-merge) +(function tuple-length (Expr) i64 :merge old) (rule ((HasType expr (TupleT tl)) (= len (TypeList-length tl))) ((set (tuple-length expr) len)) @@ -3322,7 +3322,7 @@ (sort List) (constructor Nil-List () List) (constructor Cons-List (i64 IntInterval List) List) -(function Length-List (List) i64 :no-merge) +(function Length-List (List) i64 :merge old) (rule ((= x (Nil-List ))) ((set (Length-List x) 0)) :ruleset always-run :name "(rule ((= x (Nil-List ))) @@ -3562,7 +3562,7 @@ (sort List) (constructor Nil-List () List) (constructor Cons-List (PtrPointees List) List) -(function Length-List (List) i64 :no-merge) +(function Length-List (List) i64 :merge old) (rule ((= x (Nil-List ))) ((set (Length-List x) 0)) :ruleset always-run :name "(rule ((= x (Nil-List ))) @@ -3674,7 +3674,7 @@ (rule ((= _rewrite_var__2874 (ExprSet-insert (ES set1) x))) ((union _rewrite_var__2874 (ES (set-insert set1 x)))) :ruleset memory-helpers :name "(rewrite (ExprSet-insert (ES set1) x) (ES (set-insert set1 x)) :ruleset memory-helpers)") -(function ExprSet-length (ExprSet) i64 :no-merge) +(function ExprSet-length (ExprSet) i64 :merge old) (rule ((ES set1)) ((set (ExprSet-length (ES set1)) (set-length set1))) :ruleset memory-helpers :name "(rule ((ES set1)) @@ -3987,7 +3987,7 @@ ((union (PointsToCellsAtIter aps inputs pred-body 0) (PointsToCells inputs aps)) (union (PointsToCellsAtIter aps inputs pred-body 1) (UnionPointees (PointsToCellsAtIter aps inputs pred-body 0) (PointeesDropFirst (PointsToCells pred-body (PointsToCellsAtIter aps inputs pred-body 0)))))) :ruleset memory-helpers )") -(function succ (i64) i64 :no-merge) +(function succ (i64) i64 :merge old) (rule ((PointsToCellsAtIter aps inputs pred-body i)) ((set (succ i) (+ i 1))) :ruleset memory-helpers :name "(rule ((PointsToCellsAtIter aps inputs pred-body i)) diff --git a/egglog/tests/proof_mode_regression.rs b/egglog/tests/proof_mode_regression.rs index d11db152..5a37a4c0 100644 --- a/egglog/tests/proof_mode_regression.rs +++ b/egglog/tests/proof_mode_regression.rs @@ -83,45 +83,19 @@ fn term_and_proof_modes_lower_input_rows_as_fiat_actions() { } #[test] -fn term_and_proof_modes_allow_no_merge_outputs_in_the_same_eclass() { - for mut egraph in [ - EGraph::new_with_term_encoding(), - EGraph::new_with_proofs().with_proof_testing(), - ] { - egraph - .parse_and_run_program( - None, - r#" - (sort Foo) - (function bar () Foo :no-merge) - (constructor baz () Foo) - (constructor qux () Foo) - (set (bar) (baz)) - (union (baz) (qux)) - (set (bar) (qux)) - "#, - ) - .unwrap(); - } -} - -#[test] -fn term_and_proof_modes_reject_distinct_no_merge_primitive_outputs() { +fn term_and_proof_modes_reject_eq_sort_no_merge_functions() { + // Eq-sort-output `:no-merge` is not modeled by the encoding (its conflict check + // needs union-find leaders); such a program is unsupported and runs plain only. + // Primitive/Unit-output `:no-merge` is supported (see the input test above). for mut egraph in [ EGraph::new_with_term_encoding(), EGraph::new_with_proofs().with_proof_testing(), ] { let error = egraph - .parse_and_run_program( - None, - r#" - (function score () i64 :no-merge) - (set (score) 1) - (set (score) 2) - "#, - ) + .parse_and_run_program(None, "(sort Foo) (function bar () Foo :no-merge)") .unwrap_err(); - assert!(error.to_string().contains("Illegal merge attempted")); + assert!(matches!(error, Error::UnsupportedProofCommand { .. })); + assert!(error.to_string().contains("`:no-merge`")); } } @@ -138,41 +112,55 @@ fn proof_mode_rejects_fail_wrapped_input() { .unwrap_err(); assert!(matches!(error, Error::UnsupportedProofCommand { .. })); - assert!(error.to_string().contains("`fail` wrapping an `input` command")); + assert!( + error + .to_string() + .contains("`fail` wrapping an `input` command") + ); } #[test] -fn proof_mode_rejects_fail_wrapped_set() { +fn proof_mode_allows_fail_wrapping_set() { + // A `(fail (set …))` is accepted by proof encoding (it used to be rejected as a + // non-atomic wrapped command). The set succeeds, so `fail` reports that its + // wrapped command did not fail. let error = EGraph::new_with_proofs() .parse_and_run_program( None, r#" - (function score () i64 :no-merge) + (function score () i64 :merge old) (fail (set (score) 1)) "#, ) .unwrap_err(); - assert!(matches!(error, Error::UnsupportedProofCommand { .. })); - assert!( - error - .to_string() - .contains("exactly one atomic encoded command") - ); + assert!(matches!(error, Error::ExpectFail(..))); } #[test] -fn proof_mode_rejects_fail_wrapped_multi_operation_encoding() { +fn proof_mode_allows_fail_wrapping_multi_operation_encoding() { + // A wrapped command that encodes to several commands is now accepted; + // declaring the function succeeds, so `fail` reports it did not fail. let error = EGraph::new_with_proofs() - .parse_and_run_program(None, "(fail (function score () i64 :no-merge))") + .parse_and_run_program(None, "(fail (function score () i64 :merge old))") .unwrap_err(); - assert!(matches!(error, Error::UnsupportedProofCommand { .. })); - assert!( - error - .to_string() - .contains("exactly one atomic encoded command") - ); + assert!(matches!(error, Error::ExpectFail(..))); +} + +#[test] +fn proof_mode_fail_catches_failure_among_wrapped_commands() { + // `fail` runs every wrapped command in order and succeeds when any one fails: + // the set succeeds and the mismatched check fails, so the `fail` passes. + EGraph::new_with_proofs() + .parse_and_run_program( + None, + r#" + (function score () i64 :merge old) + (fail (set (score) 1) (check (= (score) 2))) + "#, + ) + .unwrap(); } #[test] diff --git a/egglog/tests/snapshots/files__proof_unsupported_files.snap b/egglog/tests/snapshots/files__proof_unsupported_files.snap index 6aaea1cd..05984294 100644 --- a/egglog/tests/snapshots/files__proof_unsupported_files.snap +++ b/egglog/tests/snapshots/files__proof_unsupported_files.snap @@ -19,6 +19,7 @@ hardboiled_conv1d_128.egg herbie-tutorial.egg herbie.egg interval.egg +lambda.egg levenshtein-distance.egg looking_up_global.egg luminal-llama.egg diff --git a/egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap b/egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap index 91fdf996..d66c335c 100644 --- a/egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap @@ -8,50 +8,45 @@ expression: proof_snapshot (let t3 (AU t1 t2)) (let t4 (AU (Var "x") (Var "y"))) (let t5 (AU t0 t0)) -(let t6 (Add t5 t4)) -(let t7 (Add t0 (Var "x"))) -(let t8 (Add t0 (Var "y"))) +(let t6 (Add t0 (Var "x"))) +(let t7 (Add t0 (Var "y"))) (let prf0 (Rule - (= (Num 3) t0) + (= t0 (Num 3)) (name "(rewrite (Add (Num x) (Num y)) (Num (+ x y)))") (premises (Fiat (= t0 t0))) (substitution (@rewrite_var__1 t0) (x 1) (y 2)))) -(let t9 +(let t8 (premises + (Congr + (= t3 (AU t6 t7)) (Congr - (= t3 (AU t7 t8)) - (Congr - (= t3 (AU t7 t2)) - (Fiat (= t3 t3)) - (Sym - (= t1 t7) - (Rule - (= t7 t1) - (name "(rewrite (Add x y) (Add y x))") - (premises (Fiat (= t1 t1))) - (substitution (y t0) (x (Var "x")) (@rewrite_var__ t1)))) - 0) - (Congr (= t2 t8) (Fiat (= t2 t2)) prf0 0) - 1))) -(let t10 + (= t3 (AU t6 t2)) + (Fiat (= t3 t3)) + (Rule + (= t1 t6) + (name "(rewrite (Add x y) (Add y x))") + (premises (Fiat (= t1 t1))) + (substitution (y t0) (x (Var "x")) (@rewrite_var__ t1))) + 0) + (Congr (= t2 t7) (Fiat (= t2 t2)) (Sym (= (Num 3) t0) prf0) 0) + 1))) +(let t9 (substitution - (@rewrite_var__3 t3) - (a t0) - (d (Var "y")) - (b (Var "x")) - (c t0))) + (@rewrite_var__3 t3) + (a t0) + (d (Var "y")) + (b (Var "x")) + (c t0))) (Congr (= t3 (Add (Num 3) t4)) (Congr (= t3 (Add t0 t4)) - (Sym - (= t3 t6) - (Rule - (= t6 t3) - (name "(rewrite (AU (Add a b) (Add c d)) (Add (AU a c) (AU b d)))") - t9 - t10)) + (Rule + (= t3 (Add t5 t4)) + (name "(rewrite (AU (Add a b) (Add c d)) (Add (AU a c) (AU b d)))") + t8 + t9) (Rule (= t5 t0) (name "(rewrite (AU x x) x)") @@ -59,9 +54,9 @@ expression: proof_snapshot (Rule (= t5 t5) (name "(rewrite (AU (Add a b) (Add c d)) (Add (AU a c) (AU b d)))") - t9 - t10)) + t8 + t9)) (substitution (@rewrite_var__2 t5) (x t0))) 0) - (Sym (= t0 (Num 3)) prf0) + prf0 0) diff --git a/egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap b/egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap index 7024fcad..d720ff71 100644 --- a/egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap @@ -3,20 +3,14 @@ source: egglog/tests/files.rs expression: proof_snapshot --- (let t0 (Add (Add (Lit 1) (Lit 2)) (Lit 3))) -(let t1 (Add (Lit 1) (Add (Lit 2) (Lit 3)))) -(Sym - (= t0 t1) - (Rule - (= t1 t0) - (name "(birewrite (Add (Add x y) z) (Add x (Add y z)))=>") - (premises (Fiat (= t0 t0))) - (substitution (z (Lit 3)) (y (Lit 2)) (x (Lit 1)) (@rewrite_var__ t0)))) +(Rule + (= t0 (Add (Lit 1) (Add (Lit 2) (Lit 3)))) + (name "(birewrite (Add (Add x y) z) (Add x (Add y z)))=>") + (premises (Fiat (= t0 t0))) + (substitution (z (Lit 3)) (y (Lit 2)) (x (Lit 1)) (@rewrite_var__ t0))) (let t0 (Add (Lit 4) (Add (Lit 5) (Lit 6)))) -(let t1 (Add (Add (Lit 4) (Lit 5)) (Lit 6))) -(Sym - (= t0 t1) - (Rule - (= t1 t0) - (name "(birewrite (Add (Add x y) z) (Add x (Add y z)))<=") - (premises (Fiat (= t0 t0))) - (substitution (z (Lit 6)) (y (Lit 5)) (@rewrite_var__1 t0) (x (Lit 4))))) +(Rule + (= t0 (Add (Add (Lit 4) (Lit 5)) (Lit 6))) + (name "(birewrite (Add (Add x y) z) (Add x (Add y z)))<=") + (premises (Fiat (= t0 t0))) + (substitution (z (Lit 6)) (y (Lit 5)) (@rewrite_var__1 t0) (x (Lit 4)))) diff --git a/egglog/tests/snapshots/files__proofs__calc_proof_testing.snap b/egglog/tests/snapshots/files__proofs__calc_proof_testing.snap index 046485cf..3fd74f44 100644 --- a/egglog/tests/snapshots/files__proofs__calc_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__calc_proof_testing.snap @@ -7,18 +7,15 @@ expression: proof_snapshot (Fiat (= t1 t1)) (let t0 (g* (g* (AConst) (AConst)) (g* (AConst) (AConst)))) (let t1 (g* t0 t0)) -(let t2 (g* (g* (AConst) (AConst)) (g* (g* (AConst) (AConst)) t0))) -(Sym - (= t1 t2) - (Rule - (= t2 t1) - (name "(birewrite (g* (g* a b) c) (g* a (g* b c)))=>") - (premises (Fiat (= t1 t1))) - (substitution - (c t0) - (a (g* (AConst) (AConst))) - (b (g* (AConst) (AConst))) - (@rewrite_var__ t1)))) +(Rule + (= t1 (g* (g* (AConst) (AConst)) (g* (g* (AConst) (AConst)) t0))) + (name "(birewrite (g* (g* a b) c) (g* a (g* b c)))=>") + (premises (Fiat (= t1 t1))) + (substitution + (c t0) + (a (g* (AConst) (AConst))) + (b (g* (AConst) (AConst))) + (@rewrite_var__ t1))) (let t0 (g* (inv (aConst)) (aConst))) (let t1 (g* (bConst) t0)) (let t2 (g* t1 (inv (bConst)))) diff --git a/egglog/tests/snapshots/files__proofs__combinators_proof_testing.snap b/egglog/tests/snapshots/files__proofs__combinators_proof_testing.snap index 3f6dd6f1..f730374e 100644 --- a/egglog/tests/snapshots/files__proofs__combinators_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__combinators_proof_testing.snap @@ -19,765 +19,724 @@ expression: proof_snapshot (let t14 (CApp t13 (CFConst))) (let t15 (CAbs "x" (Comb (N 2)))) (let t16 (CApp t15 (CFConst))) -(let t17 (CApp t14 t16)) -(let t18 (CApp (CApp (SConst) t13) t15)) -(let t19 (Comb t2)) -(let t20 (CApp t19 (Comb (FConst)))) -(let t21 (x t3)) -(let t22 (CApp t12 (Comb (N 2)))) -(let t23 (Comb t1)) -(let t24 (CAbs "x" t23)) -(let t25 +(let t17 (CApp (CApp (SConst) t13) t15)) +(let t18 (Comb t2)) +(let t19 (x t3)) +(let t20 (CApp t12 (Comb (N 2)))) +(let t21 (Comb t1)) +(let t22 (premises - (Congr - (= t19 (CAbs "x" t22)) - (Sym - (= t19 t24) - (Rule - (= t24 t19) - (name - "(rule ((= x (Abs v body))) + (Congr + (= t18 (CAbs "x" t20)) + (Rule + (= t18 (CAbs "x" t21)) + (name + "(rule ((= x (Abs v body))) ((union (Comb x) (CAbs v (Comb body)))) )") - (premises (Fiat (= t2 t2))) - (substitution - (v "x") - (body t1) - (x t2)))) - (Sym - (= t23 t22) - (Rule - (= t22 t23) - (name - "(rule ((= x (Add l r))) + (premises (Fiat (= t2 t2))) + (substitution (v "x") (body t1) (x t2))) + (Rule + (= t21 t20) + (name + "(rule ((= x (Add l r))) ((union (Comb x) (CApp (CApp $CAdd (Comb l)) (Comb r)))) )") - (premises (Fiat (= t1 t1))) - (substitution - (r (N 2)) - (x t1) - (l t0)))) - 1))) -(let t26 + (premises (Fiat (= t1 t1))) + (substitution (r (N 2)) (x t1) (l t0))) + 1))) +(let t23 (substitution - (v "x") - (y (Comb (N 2))) - (x t12) - (@rewrite_var__9 t19))) -(let t27 + (v "x") + (y (Comb (N 2))) + (x t12) + (@rewrite_var__9 t18))) +(let t24 (premises - (Congr - (= t9 (CApp t18 (CFConst))) - (Congr - (= t9 (CApp t19 (CFConst))) - (Sym - (= t9 t20) - (Rule - (= t20 t9) - (name - "(rule ((= x (App f a))) + (Congr + (= t9 (CApp t17 (CFConst))) + (Congr + (= t9 (CApp t18 (CFConst))) + (Rule + (= t9 (CApp t18 (Comb (FConst)))) + (name + "(rule ((= x (App f a))) ((union (Comb x) (CApp (Comb f) (Comb a)))) )") - (premises (Fiat (= t3 t3))) - (substitution (a (FConst)) (f t2) t21))) - (Rule - (= (Comb (FConst)) (CFConst)) - (name - "(rule ((= x $F)) + (premises (Fiat (= t3 t3))) + (substitution (a (FConst)) (f t2) t19)) + (Rule + (= (Comb (FConst)) (CFConst)) + (name + "(rule ((= x $F)) ((union (Comb x) $CF)) )") - (premises (Fiat (= (FConst) (FConst)))) - (substitution (x (FConst)))) - 1) - (Sym - (= t19 t18) - (Rule - (= t18 t19) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t25 - t26)) - 0))) -(let t28 + (premises (Fiat (= (FConst) (FConst)))) + (substitution (x (FConst)))) + 1) + (Rule + (= t18 t17) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t22 + t23) + 0))) +(let t25 (substitution - (cz (CFConst)) - (cx t13) - (@rewrite_var__16 t9) - (cy t15))) -(let t29 (CApp (KConst) (Comb (N 2)))) -(let t30 (CApp (KConst) (CN 2))) + (cz (CFConst)) + (cx t13) + (@rewrite_var__16 t9) + (cy t15))) +(let t26 (CApp (KConst) (Comb (N 2)))) +(let t27 (CApp (KConst) (CN 2))) (let prf0 (Rule - (= (CN 2) (Comb (N 2))) - (name - "(rule ((= x (N n))) + (= (Comb (N 2)) (CN 2)) + (name + "(rule ((= x (N n))) ((union (Comb x) (CN n))) )") - (premises (Fiat (= (N 2) (N 2)))) - (substitution (x (N 2)) (n 2)))) -(let prf1 (Sym (= (Comb (N 2)) (CN 2)) prf0)) -(let t31 (cx (Comb (N 2)))) -(let t32 (CApp (CAbs "x" (CAddConst)) (CFConst))) -(let t33 (CApp t32 t6)) -(let t34 - (CApp - (CApp (SConst) (CAbs "x" (CAddConst))) - t5)) -(let t35 + (premises (Fiat (= (N 2) (N 2)))) + (substitution (x (N 2)) (n 2)))) +(let t28 (premises - (Rule - (= t13 t13) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t25 - t26))) -(let t36 + (Congr + (= t15 (CAbs "x" (CN 2))) + (Rule + (= t15 t15) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t22 + t23) + prf0 + 1))) +(let t29 (substitution - (v "x") - (y t4) - (x (CAddConst)) - (@rewrite_var__9 t13))) -(let t37 + (@rewrite_var__4 t15) + (v "x") + (n 2))) +(let t30 (cx (Comb (N 2)))) +(let t31 (CApp (CAbs "x" (CAddConst)) (CFConst))) +(let t32 (CApp (CApp (SConst) (CAbs "x" (CAddConst))) t5)) +(let t33 (premises - (Congr - (= t14 (CApp t34 (CFConst))) (Rule - (= t14 t14) + (= t13 t13) (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t27 - t28) - (Sym - (= t13 t34) - (Rule - (= t34 t13) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t35 - t36)) - 0))) -(let t38 + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t22 + t23))) +(let t34 (substitution - (cz (CFConst)) - (cx (CAbs "x" (CAddConst))) - (@rewrite_var__16 t14) - (cy t5))) -(let t39 + (v "x") + (y t4) + (x (CAddConst)) + (@rewrite_var__9 t13))) +(let t35 (premises + (Congr + (= t14 (CApp t32 (CFConst))) + (Rule + (= t14 t14) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t24 + t25) + (Rule + (= t13 t32) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t33 + t34) + 0))) +(let t36 + (substitution + (cz (CFConst)) + (cx (CAbs "x" (CAddConst))) + (@rewrite_var__16 t14) + (cy t5))) +(let t37 + (premises + (Congr + (= t9 (CApp t11 (Comb (N 2)))) (Congr - (= t9 (CApp t11 (Comb (N 2)))) - (Congr - (= t9 (CApp t14 (Comb (N 2)))) - (Sym - (= t9 t17) - (Rule - (= t17 t9) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t27 - t28)) - (Rule - (= t16 (Comb (N 2))) - (name "(rewrite (CApp (CApp $K cx) cy) cx)") - (premises - (Congr - (= t16 (CApp t29 (CFConst))) + (= t9 (CApp t14 (Comb (N 2)))) + (Rule + (= t9 (CApp t14 t16)) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t24 + t25) + (Rule + (= t16 (Comb (N 2))) + (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (premises + (Congr + (= t16 (CApp t26 (CFConst))) + (Rule + (= t16 t16) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t24 + t25) + (Trans + (= t15 t26) (Rule - (= t16 t16) + (= t15 t27) (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t27 - t28) + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t28 + t29) (Congr - (= t15 t29) - (Sym - (= t15 t30) - (Rule - (= t30 t15) - (name - "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") - (premises - (Congr - (= t15 (CAbs "x" (CN 2))) - (Rule - (= t15 t15) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t25 - t26) - prf1 - 1)) - (substitution - (@rewrite_var__4 t15) - (v "x") - (n 2)))) - prf0 - 1) - 0)) - (substitution - (@rewrite_var__15 t16) - t31 - (cy (CFConst)))) - 1) - (Congr - (= t14 t11) - (Sym - (= t14 t33) - (Rule - (= t33 t14) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t37 - t38)) - (Rule - (= t32 (CAddConst)) - (name "(rewrite (CApp (CApp $K cx) cy) cx)") - (premises - (Congr + (= t27 t26) + (Rule + (= t27 t27) + (name + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t28 + t29) + (Sym (= (CN 2) (Comb (N 2))) prf0) + 1)) + 0)) + (substitution (@rewrite_var__15 t16) t30 (cy (CFConst)))) + 1) + (Congr + (= t14 t11) + (Rule + (= t14 (CApp t31 t6)) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t35 + t36) + (Rule + (= t31 (CAddConst)) + (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (premises + (Congr + (= t31 (CApp (CApp (KConst) (CAddConst)) (CFConst))) + (Rule + (= t31 t31) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t35 + t36) + (Rule (= - t32 - (CApp (CApp (KConst) (CAddConst)) (CFConst))) - (Rule - (= t32 t32) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t37 - t38) - (Sym - (= - (CAbs "x" (CAddConst)) - (CApp (KConst) (CAddConst))) + (CAbs "x" (CAddConst)) + (CApp (KConst) (CAddConst))) + (name "(rewrite (CAbs v $CAdd) (CApp $K $CAdd))") + (premises (Rule (= - (CApp (KConst) (CAddConst)) + (CAbs "x" (CAddConst)) (CAbs "x" (CAddConst))) (name - "(rewrite (CAbs v $CAdd) (CApp $K $CAdd))") - (premises - (Rule - (= - (CAbs "x" (CAddConst)) - (CAbs "x" (CAddConst))) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t35 - t36)) - (substitution - (v "x") - (@rewrite_var__8 (CAbs "x" (CAddConst)))))) - 0)) - (substitution - (@rewrite_var__15 t32) - (cx (CAddConst)) - (cy (CFConst)))) - 0) - 0))) -(let t40 (substitution (cl t6) (cr (Comb (N 2))) (cx t9))) -(let t41 (Uncomb (Comb (N 2)))) -(let t42 (Add t7 t41)) -(let t43 (Uncomb (Comb (N 0)))) -(let t44 (Uncomb (Comb (N 1)))) -(let t45 (If (Uncomb (CFConst)) t43 t44)) -(let t46 (CApp (CApp (CIfConst) (CFConst)) (Comb (N 0)))) -(let t47 (CApp (CIfConst) (Comb (Var "x")))) -(let t48 (CApp t47 (Comb (N 0)))) -(let t49 (CAbs "x" t48)) -(let t50 (CApp t49 (CFConst))) -(let t51 (CAbs "x" (Comb (N 1)))) -(let t52 (CApp t51 (CFConst))) -(let t53 (CApp t50 t52)) -(let t54 (CApp (CApp (SConst) t49) t51)) -(let t55 (CApp t48 (Comb (N 1)))) -(let t56 + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t33 + t34)) + (substitution + (v "x") + (@rewrite_var__8 (CAbs "x" (CAddConst))))) + 0)) + (substitution + (@rewrite_var__15 t31) + (cx (CAddConst)) + (cy (CFConst)))) + 0) + 0))) +(let t38 (substitution (cl t6) (cr (Comb (N 2))) (cx t9))) +(let t39 (Uncomb (Comb (N 2)))) +(let t40 (Add t7 t39)) +(let t41 (If (FConst) (N 0) (N 1))) +(let t42 (Uncomb (Comb (N 0)))) +(let t43 (Uncomb (Comb (N 1)))) +(let t44 (If (Uncomb (CFConst)) t42 t43)) +(let t45 (CApp (CApp (CIfConst) (CFConst)) (Comb (N 0)))) +(let t46 (CApp (CIfConst) (Comb (Var "x")))) +(let t47 (CApp t46 (Comb (N 0)))) +(let t48 (CAbs "x" t47)) +(let t49 (CApp t48 (CFConst))) +(let t50 (CAbs "x" (Comb (N 1)))) +(let t51 (CApp t50 (CFConst))) +(let t52 (CApp (CApp (SConst) t48) t50)) +(let t53 (CApp t47 (Comb (N 1)))) +(let t54 (premises - (Congr - (= t5 (CAbs "x" t55)) - (Rule - (= t5 t5) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t35 - t36) - (Sym - (= t4 t55) - (Rule - (= t55 t4) - (name - "(rule ((= x (If c t f))) + (Congr + (= t5 (CAbs "x" t53)) + (Rule + (= t5 t5) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t33 + t34) + (Rule + (= t4 t53) + (name + "(rule ((= x (If c t f))) ((union (Comb x) (CApp (CApp (CApp $CIf (Comb c)) (Comb t)) (Comb f)))) )") - (premises (Fiat (= t0 t0))) - (substitution - (t (N 0)) - (f (N 1)) - (x t0) - (c (Var "x"))))) - 1))) -(let t57 + (premises (Fiat (= t0 t0))) + (substitution + (t (N 0)) + (f (N 1)) + (x t0) + (c (Var "x")))) + 1))) +(let t55 (substitution - (v "x") - (y (Comb (N 1))) - (x t48) - (@rewrite_var__9 t5))) -(let t58 + (v "x") + (y (Comb (N 1))) + (x t47) + (@rewrite_var__9 t5))) +(let t56 (premises - (Congr - (= t6 (CApp t54 (CFConst))) - (Rule - (= t6 t6) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t37 - t38) - (Sym - (= t5 t54) - (Rule - (= t54 t5) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t56 - t57)) - 0))) -(let t59 + (Congr + (= t6 (CApp t52 (CFConst))) + (Rule + (= t6 t6) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t35 + t36) + (Rule + (= t5 t52) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t54 + t55) + 0))) +(let t57 (substitution - (cz (CFConst)) - (cx t49) - (@rewrite_var__16 t6) - (cy t51))) -(let t60 (CApp (KConst) (Comb (N 1)))) -(let t61 (CApp (KConst) (CN 1))) -(let prf2 + (cz (CFConst)) + (cx t48) + (@rewrite_var__16 t6) + (cy t50))) +(let t58 (CApp (KConst) (Comb (N 1)))) +(let t59 (CApp (KConst) (CN 1))) +(let prf1 (Rule - (= (CN 1) (Comb (N 1))) - (name - "(rule ((= x (N n))) + (= (Comb (N 1)) (CN 1)) + (name + "(rule ((= x (N n))) ((union (Comb x) (CN n))) )") - (premises - (Fiat (= (N 1) (N 1)))) - (substitution (x (N 1)) (n 1)))) -(let prf3 (Sym (= (Comb (N 1)) (CN 1)) prf2)) + (premises (Fiat (= (N 1) (N 1)))) + (substitution (x (N 1)) (n 1)))) +(let t60 + (premises + (Congr + (= t50 (CAbs "x" (CN 1))) + (Rule + (= t50 t50) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t54 + t55) + prf1 + 1))) +(let t61 (substitution (@rewrite_var__4 t50) (v "x") (n 1))) (let t62 (cx (Comb (N 1)))) -(let t63 (CAbs "x" t47)) +(let t63 (CAbs "x" t46)) (let t64 (CApp t63 (CFConst))) (let t65 (CAbs "x" (Comb (N 0)))) (let t66 (CApp t65 (CFConst))) -(let t67 (CApp t64 t66)) -(let t68 (CApp (CApp (SConst) t63) t65)) -(let t69 +(let t67 (CApp (CApp (SConst) t63) t65)) +(let t68 (premises - (Rule - (= t49 t49) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t56 - t57))) -(let t70 + (Rule + (= t48 t48) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t54 + t55))) +(let t69 (substitution - (v "x") - (y (Comb (N 0))) - (x t47) - (@rewrite_var__9 t49))) -(let t71 + (v "x") + (y (Comb (N 0))) + (x t46) + (@rewrite_var__9 t48))) +(let t70 (premises - (Congr - (= t50 (CApp t68 (CFConst))) - (Rule - (= t50 t50) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t58 - t59) - (Sym - (= t49 t68) - (Rule - (= t68 t49) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t69 - t70)) - 0))) -(let t72 + (Congr + (= t49 (CApp t67 (CFConst))) + (Rule + (= t49 t49) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t56 + t57) + (Rule + (= t48 t67) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t68 + t69) + 0))) +(let t71 (substitution - (cz (CFConst)) - (cx t63) - (@rewrite_var__16 t50) - (cy t65))) -(let t73 (CApp (KConst) (Comb (N 0)))) -(let t74 (CApp (KConst) (CN 0))) -(let prf4 + (cz (CFConst)) + (cx t63) + (@rewrite_var__16 t49) + (cy t65))) +(let t72 (CApp (KConst) (Comb (N 0)))) +(let t73 (CApp (KConst) (CN 0))) +(let prf2 (Rule - (= (CN 0) (Comb (N 0))) - (name - "(rule ((= x (N n))) + (= (Comb (N 0)) (CN 0)) + (name + "(rule ((= x (N n))) ((union (Comb x) (CN n))) )") - (premises - (Fiat (= (N 0) (N 0)))) - (substitution (x (N 0)) (n 0)))) -(let prf5 (Sym (= (Comb (N 0)) (CN 0)) prf4)) -(let t75 (cx (Comb (N 0)))) -(let t76 (CApp (CAbs "x" (CIfConst)) (CFConst))) -(let t77 (CAbs "x" (Comb (Var "x")))) -(let t78 (CApp t77 (CFConst))) -(let t79 (CApp t76 t78)) + (premises (Fiat (= (N 0) (N 0)))) + (substitution (x (N 0)) (n 0)))) +(let t74 + (premises + (Congr + (= t65 (CAbs "x" (CN 0))) + (Rule + (= t65 t65) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t68 + t69) + prf2 + 1))) +(let t75 + (substitution + (@rewrite_var__4 t65) + (v "x") + (n 0))) +(let t76 (cx (Comb (N 0)))) +(let t77 (CApp (CAbs "x" (CIfConst)) (CFConst))) +(let t78 (CAbs "x" (Comb (Var "x")))) +(let t79 (CApp t78 (CFConst))) (let t80 (CApp - (CApp - (SConst) - (CAbs "x" (CIfConst))) - t77)) + (CApp (SConst) (CAbs "x" (CIfConst))) + t78)) (let t81 (premises - (Rule - (= t63 t63) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t69 - t70))) + (Rule + (= t63 t63) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t68 + t69))) (let t82 (substitution - (v "x") - (y (Comb (Var "x"))) - (x (CIfConst)) - (@rewrite_var__9 t63))) + (v "x") + (y (Comb (Var "x"))) + (x (CIfConst)) + (@rewrite_var__9 t63))) (let t83 (premises - (Congr - (= t64 (CApp t80 (CFConst))) - (Rule - (= t64 t64) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t71 - t72) - (Sym - (= t63 t80) - (Rule - (= t80 t63) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t81 - t82)) - 0))) + (Congr + (= t64 (CApp t80 (CFConst))) + (Rule + (= t64 t64) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t70 + t71) + (Rule + (= t63 t80) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t81 + t82) + 0))) (let t84 (substitution - (cz (CFConst)) - (cx (CAbs "x" (CIfConst))) - (@rewrite_var__16 t64) - (cy t77))) -(Sym - (= t3 (N 3)) - (Rule - (= (N 3) t3) - (name "(rewrite (Add (N n) (N m)) (N (+ n m)))") - (premises - (Congr - (= t3 (Add (N 1) (N 2))) - (Trans - (= t3 t8) - (Sym - (= t3 t10) - (Rule - (= t10 t3) - (name "(rewrite (Uncomb (Comb x)) x)") - (premises - (Rule - (= t10 t10) - (name - "(rule ((= cx (CApp (CApp $CAdd cl) cr))) - ((union (Uncomb cx) (Add (Uncomb cl) (Uncomb cr)))) - )") - t39 - t40)) - (substitution (@rewrite_var__1 t10) t21))) - (Congr - (= t10 t8) - (Sym - (= t10 t42) - (Rule - (= t42 t10) - (name - "(rule ((= cx (CApp (CApp $CAdd cl) cr))) - ((union (Uncomb cx) (Add (Uncomb cl) (Uncomb cr)))) - )") - t39 - t40)) - (Rule - (= t41 (N 2)) - (name - "(rule ((= cx (CN n))) - ((union (Uncomb cx) (N n))) - )") - (premises prf1) - (substitution t31 (n 2))) - 1)) - (Rule - (= t7 (N 1)) - (name "(rewrite (If $F t f) f)") - (premises - (Congr - (= t7 (If (FConst) (N 0) (N 1))) - (Congr - (= t7 (If (Uncomb (CFConst)) (N 0) (N 1))) - (Congr - (= t7 (If (Uncomb (CFConst)) t43 (N 1))) - (Sym - (= t7 t45) - (Rule - (= t45 t7) - (name - "(rule ((= cx (CApp (CApp (CApp $CIf cc) ct) cf))) - ((union (Uncomb cx) (If (Uncomb cc) (Uncomb ct) (Uncomb cf)))) - )") - (premises - (Congr - (= t6 (CApp t46 (Comb (N 1)))) + (cz (CFConst)) + (cx (CAbs "x" (CIfConst))) + (@rewrite_var__16 t64) + (cy t78))) +(let t85 + (premises + (Congr + (= t6 (CApp t45 (Comb (N 1)))) + (Congr + (= t6 (CApp t49 (Comb (N 1)))) + (Rule + (= t6 (CApp t49 t51)) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t56 + t57) + (Rule + (= t51 (Comb (N 1))) + (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (premises (Congr - (= t6 (CApp t50 (Comb (N 1)))) - (Sym - (= t6 t53) + (= t51 (CApp t58 (CFConst))) + (Rule + (= t51 t51) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t56 + t57) + (Trans + (= t50 t58) (Rule - (= t53 t6) + (= t50 t59) + (name + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t60 + t61) + (Congr + (= t59 t58) + (Rule + (= t59 t59) + (name + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t60 + t61) + (Sym (= (CN 1) (Comb (N 1))) prf1) + 1)) + 0)) + (substitution (@rewrite_var__15 t51) t62 (cy (CFConst)))) + 1) + (Congr + (= t49 t45) + (Congr + (= t49 (CApp t64 (Comb (N 0)))) + (Rule + (= t49 (CApp t64 t66)) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t70 + t71) + (Rule + (= t66 (Comb (N 0))) + (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (premises + (Congr + (= t66 (CApp t72 (CFConst))) + (Rule + (= t66 t66) (name "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t58 - t59)) - (Rule - (= t52 (Comb (N 1))) - (name "(rewrite (CApp (CApp $K cx) cy) cx)") - (premises + t70 + t71) + (Trans + (= t65 t72) + (Rule + (= t65 t73) + (name + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t74 + t75) (Congr - (= t52 (CApp t60 (CFConst))) + (= t73 t72) (Rule - (= t52 t52) + (= t73 t73) (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t58 - t59) - (Congr - (= t51 t60) - (Sym - (= t51 t61) - (Rule - (= t61 t51) - (name - "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") - (premises - (Congr - (= t51 (CAbs "x" (CN 1))) - (Rule - (= t51 t51) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t56 - t57) - prf3 - 1)) - (substitution - (@rewrite_var__4 t51) - (v "x") - (n 1)))) - prf2 - 1) - 0)) - (substitution - (@rewrite_var__15 t52) - t62 - (cy (CFConst)))) - 1) - (Congr - (= t50 t46) - (Congr - (= t50 (CApp t64 (Comb (N 0)))) - (Sym - (= t50 t67) + "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") + t74 + t75) + (Sym (= (CN 0) (Comb (N 0))) prf2) + 1)) + 0)) + (substitution (@rewrite_var__15 t66) t76 (cy (CFConst)))) + 1) + (Congr + (= t64 (CApp (CIfConst) (CFConst))) + (Congr + (= t64 (CApp t77 (CFConst))) + (Rule + (= t64 (CApp t77 t79)) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t83 + t84) + (Rule + (= t79 (CFConst)) + (name "(rewrite (CApp $I cx) cx)") + (premises + (Congr + (= t79 (CApp (IConst) (CFConst))) (Rule - (= t67 t50) + (= t79 t79) (name "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t71 - t72)) - (Rule - (= t66 (Comb (N 0))) - (name "(rewrite (CApp (CApp $K cx) cy) cx)") - (premises - (Congr - (= t66 (CApp t73 (CFConst))) - (Rule - (= t66 t66) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t71 - t72) - (Congr - (= t65 t73) - (Sym - (= t65 t74) - (Rule - (= t74 t65) - (name - "(rewrite (CAbs v (CN n)) (CApp $K (CN n)))") - (premises - (Congr - (= t65 (CAbs "x" (CN 0))) - (Rule - (= t65 t65) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t69 - t70) - prf5 - 1)) - (substitution - (@rewrite_var__4 t65) - (v "x") - (n 0)))) - prf4 - 1) - 0)) - (substitution - (@rewrite_var__15 t66) - t75 - (cy (CFConst)))) - 1) - (Congr - (= t64 (CApp (CIfConst) (CFConst))) - (Congr - (= t64 (CApp t76 (CFConst))) - (Sym - (= t64 t79) - (Rule - (= t79 t64) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t83 - t84)) + t83 + t84) (Rule - (= t78 (CFConst)) - (name "(rewrite (CApp $I cx) cx)") + (= t78 (IConst)) + (name "(rewrite (CAbs v (CVar v)) $I)") (premises (Congr - (= t78 (CApp (IConst) (CFConst))) + (= t78 (CAbs "x" (CVar "x"))) (Rule (= t78 t78) (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t83 - t84) + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t81 + t82) (Rule - (= t77 (IConst)) - (name "(rewrite (CAbs v (CVar v)) $I)") - (premises - (Congr - (= t77 (CAbs "x" (CVar "x"))) - (Rule - (= t77 t77) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t81 - t82) - (Sym - (= (Comb (Var "x")) (CVar "x")) - (Rule - (= (CVar "x") (Comb (Var "x"))) - (name - "(rule ((= x (Var v))) - ((union (Comb x) (CVar v))) - )") - (premises - (Fiat (= (Var "x") (Var "x")))) - (substitution - (v "x") - (x (Var "x"))))) - 1)) - (substitution - (@rewrite_var__2 t77) - (v "x"))) - 0)) - (substitution - (@rewrite_var__14 t78) - (cx (CFConst)))) - 1) + (= (Comb (Var "x")) (CVar "x")) + (name + "(rule ((= x (Var v))) + ((union (Comb x) (CVar v))) + )") + (premises (Fiat (= (Var "x") (Var "x")))) + (substitution (v "x") (x (Var "x")))) + 1)) + (substitution (@rewrite_var__2 t78) (v "x"))) + 0)) + (substitution (@rewrite_var__14 t79) (cx (CFConst)))) + 1) + (Rule + (= t77 (CIfConst)) + (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (premises + (Congr + (= t77 (CApp (CApp (KConst) (CIfConst)) (CFConst))) + (Rule + (= t77 t77) + (name + "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") + t83 + t84) (Rule - (= t76 (CIfConst)) - (name "(rewrite (CApp (CApp $K cx) cy) cx)") + (= + (CAbs "x" (CIfConst)) + (CApp (KConst) (CIfConst))) + (name "(rewrite (CAbs v $CIf) (CApp $K $CIf))") (premises - (Congr + (Rule (= - t76 - (CApp - (CApp (KConst) (CIfConst)) - (CFConst))) - (Rule - (= t76 t76) - (name - "(rewrite (CApp (CApp (CApp $S cx) cy) cz) (CApp (CApp cx cz) (CApp cy cz)))") - t83 - t84) - (Sym - (= - (CAbs "x" (CIfConst)) - (CApp (KConst) (CIfConst))) - (Rule - (= - (CApp (KConst) (CIfConst)) - (CAbs "x" (CIfConst))) - (name - "(rewrite (CAbs v $CIf) (CApp $K $CIf))") - (premises - (Rule - (= - (CAbs "x" (CIfConst)) - (CAbs "x" (CIfConst))) - (name - "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") - t81 - t82)) - (substitution - (@rewrite_var__7 - (CAbs "x" (CIfConst))) - (v "x")))) - 0)) + (CAbs "x" (CIfConst)) + (CAbs "x" (CIfConst))) + (name + "(rewrite (CAbs v (CApp x y)) (CApp (CApp $S (CAbs v x)) (CAbs v y)))") + t81 + t82)) (substitution - (@rewrite_var__15 t76) - (cx (CIfConst)) - (cy (CFConst)))) - 0) - 0) - 0)) - (substitution - (cc (CFConst)) - (ct (Comb (N 0))) - (cx t6) - (cf (Comb (N 1)))))) + (@rewrite_var__7 (CAbs "x" (CIfConst))) + (v "x"))) + 0)) + (substitution + (@rewrite_var__15 t77) + (cx (CIfConst)) + (cy (CFConst)))) + 0) + 0) + 0))) +(let t86 + (substitution + (cc (CFConst)) + (ct (Comb (N 0))) + (cx t6) + (cf (Comb (N 1))))) +(Rule + (= t3 (N 3)) + (name "(rewrite (Add (N n) (N m)) (N (+ n m)))") + (premises + (Congr + (= t3 (Add (N 1) (N 2))) + (Trans + (= t3 t8) + (Sym + (= t3 t10) + (Rule + (= t10 t3) + (name "(rewrite (Uncomb (Comb x)) x)") + (premises + (Rule + (= t10 t10) + (name + "(rule ((= cx (CApp (CApp $CAdd cl) cr))) + ((union (Uncomb cx) (Add (Uncomb cl) (Uncomb cr)))) + )") + t37 + t38)) + (substitution (@rewrite_var__1 t10) t19))) + (Trans + (= t10 t8) + (Rule + (= t10 t40) + (name + "(rule ((= cx (CApp (CApp $CAdd cl) cr))) + ((union (Uncomb cx) (Add (Uncomb cl) (Uncomb cr)))) + )") + t37 + t38) + (Congr + (= t40 t8) + (Rule + (= t40 t40) + (name + "(rule ((= cx (CApp (CApp $CAdd cl) cr))) + ((union (Uncomb cx) (Add (Uncomb cl) (Uncomb cr)))) + )") + t37 + t38) + (Rule + (= t39 (N 2)) + (name + "(rule ((= cx (CN n))) + ((union (Uncomb cx) (N n))) + )") + (premises prf0) + (substitution t30 (n 2))) + 1))) + (Rule + (= t7 (N 1)) + (name "(rewrite (If $F t f) f)") + (premises + (Trans + (= t7 t41) + (Rule + (= t7 t44) + (name + "(rule ((= cx (CApp (CApp (CApp $CIf cc) ct) cf))) + ((union (Uncomb cx) (If (Uncomb cc) (Uncomb ct) (Uncomb cf)))) + )") + t85 + t86) + (Congr + (= t44 t41) + (Congr + (= t44 (If (FConst) (N 0) t43)) + (Congr + (= t44 (If (FConst) t42 t43)) (Rule - (= t44 (N 1)) + (= t44 t44) (name - "(rule ((= cx (CN n))) - ((union (Uncomb cx) (N n))) + "(rule ((= cx (CApp (CApp (CApp $CIf cc) ct) cf))) + ((union (Uncomb cx) (If (Uncomb cc) (Uncomb ct) (Uncomb cf)))) + )") + t85 + t86) + (Rule + (= (Uncomb (CFConst)) (FConst)) + (name + "(rule ((= cx $CF)) + ((union (Uncomb cx) $F)) )") - (premises prf3) - (substitution t62 (n 1))) - 2) + (premises (Fiat (= (CFConst) (CFConst)))) + (substitution (cx (CFConst)))) + 0) (Rule - (= t43 (N 0)) + (= t42 (N 0)) (name "(rule ((= cx (CN n))) ((union (Uncomb cx) (N n))) )") - (premises prf5) - (substitution t75 (n 0))) + (premises prf2) + (substitution t76 (n 0))) 1) (Rule - (= (Uncomb (CFConst)) (FConst)) + (= t43 (N 1)) (name - "(rule ((= cx $CF)) - ((union (Uncomb cx) $F)) + "(rule ((= cx (CN n))) + ((union (Uncomb cx) (N n))) )") - (premises (Fiat (= (CFConst) (CFConst)))) - (substitution (cx (CFConst)))) - 0)) - (substitution (@rewrite_var__12 t7) (t (N 0)) (f (N 1)))) - 0)) - (substitution (@rewrite_var__13 t3) (m 2) (n 1)))) + (premises prf1) + (substitution t62 (n 1))) + 2))) + (substitution (@rewrite_var__12 t7) (t (N 0)) (f (N 1)))) + 0)) + (substitution (@rewrite_var__13 t3) (m 2) (n 1))) diff --git a/egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap b/egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap index dd46ca7f..99474807 100644 --- a/egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap @@ -3,20 +3,16 @@ source: egglog/tests/files.rs expression: proof_snapshot --- (let prf0 - (Sym + (Rule (= (Add 2 3) (Add 3 2)) - (Rule - (= (Add 3 2) (Add 2 3)) - (name "rw1") - (premises (Fiat (= (Add 2 3) (Add 2 3)))) - (substitution (a 2) (b 3))))) + (name "rw1") + (premises (Fiat (= (Add 2 3) (Add 2 3)))) + (substitution (a 2) (b 3)))) (Trans (= (Add 2 3) (Num 5)) prf0 - (Sym + (Rule (= (Add 3 2) (Num 5)) - (Rule - (= (Num 5) (Add 3 2)) - (name "rw2") - (premises prf0 (Fiat (= () ()))) - (substitution (a 3) (b 2))))) + (name "rw2") + (premises prf0 (Fiat (= () ()))) + (substitution (a 3) (b 2)))) diff --git a/egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap b/egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap index 050ff508..00b20638 100644 --- a/egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap @@ -9,7 +9,7 @@ expression: proof_snapshot (= (@ExistsConstructor) (@ExistsConstructor)) (name "@prove_exists_rule") (premises prf0 (Fiat (= (Z) (Z))) prf0 (Eval) (Fiat (= t1 t1))) - (substitution (@v45 (S (Z))) (@v47 (S (Z))) (@v46 (Z)) (@v48 t0))) + (substitution (@v103 (Z)) (@v105 t0) (@v102 (S (Z))) (@v104 (S (Z))))) (let t0 (pair (Z) (S (Z)))) (let t1 (HasPair t0)) (Rule @@ -20,7 +20,7 @@ expression: proof_snapshot (Fiat (= (S (Z)) (S (Z)))) (Eval) (Fiat (= t1 t1))) - (substitution (@v98 (Z)) (@v99 (S (Z))) (@v100 t0))) + (substitution (@v235 t0) (@v233 (Z)) (@v234 (S (Z))))) (let t0 (vec-of (Z) (S (Z)))) (let t1 (HasVec t0)) (Rule @@ -31,7 +31,7 @@ expression: proof_snapshot (Fiat (= (S (Z)) (S (Z)))) (Eval) (Fiat (= t1 t1))) - (substitution (@v147 t0) (@v146 (S (Z))) (@v145 (Z)))) + (substitution (@v356 t0) (@v355 (S (Z))) (@v354 (Z)))) (let t0 (map-of (Z) (S (Z)))) (let t1 (HasMap t0)) (Rule @@ -42,7 +42,7 @@ expression: proof_snapshot (Fiat (= (S (Z)) (S (Z)))) (Eval) (Fiat (= t1 t1))) - (substitution (@v194 (S (Z))) (@v193 (Z)) (@v195 t0))) + (substitution (@v479 (Z)) (@v480 (S (Z))) (@v481 t0))) (let prf0 (Fiat (= (Z) (Z)))) (let t0 (multiset-of (S (Z)) (Z) (Z))) (let t1 (HasMS t0)) @@ -50,7 +50,7 @@ expression: proof_snapshot (= (@ExistsConstructor4) (@ExistsConstructor4)) (name "@prove_exists_rule4") (premises prf0 (Fiat (= (S (Z)) (S (Z)))) prf0 (Eval) (Fiat (= t1 t1))) - (substitution (@v242 (Z)) (@v244 (Z)) (@v243 (S (Z))) (@v245 t0))) + (substitution (@v612 (S (Z))) (@v614 t0) (@v613 (Z)) (@v611 (Z)))) (let t0 (vec-of (Z) (S (Z)))) (let t1 (HasVec t0)) (Rule @@ -94,7 +94,7 @@ expression: proof_snapshot ((MapGet w)) )") (premises (Fiat (= t1 t1)) (Fiat (= (Z) (Z))) (Fiat (= (S (Z)) (S (Z))))) - (substitution (m t0) (w (S (Z))) (@v369 (Z)))) + (substitution (m t0) (@v881 (Z)) (w (S (Z))))) (let t0 (multiset-of (S (Z)) (Z) (Z))) (let t1 (HasMS t0)) (Rule @@ -195,5 +195,5 @@ expression: proof_snapshot ((OutWrap w)) )") (premises (Fiat (= (HasElem (Z)) (HasElem (Z)))) (Eval) (Fiat (= t0 t0))) - (substitution (a (Z)) (@v711 (vec-of (Z) (Z))) (w t0)))) - (substitution (@v721 (Z)) (@v720 (Z)) (@v722 (vec-of (Z) (Z))))) + (substitution (a (Z)) (w t0) (@v1640 (vec-of (Z) (Z)))))) + (substitution (@v1667 (vec-of (Z) (Z))) (@v1666 (Z)) (@v1665 (Z)))) diff --git a/egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap b/egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap index 47eef67c..dae50326 100644 --- a/egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap @@ -2,7 +2,7 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(let prf0 (Fiat (= (B) (A)))) +(let prf0 (Sym (= (B) (A)) (Fiat (= (A) (B))))) (let t0 (Holds (set-of (A) (B)))) (let prf1 (Congr @@ -30,8 +30,8 @@ expression: proof_snapshot (Sym (= (Holds (set-of (A))) t0) prf1) prf1)) (substitution - (@v51 (set-of (A))) - (@v47 (A)) - (@v48 (A)) - (@v50 (A)) - (@v49 (set-of (A))))) + (@v142 (set-of (A))) + (@v144 (set-of (A))) + (@v140 (A)) + (@v143 (A)) + (@v141 (A)))) diff --git a/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap b/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap index d0d9e8f3..3fe9c1ee 100644 --- a/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap @@ -7,25 +7,17 @@ expression: proof_snapshot (let t2 (Add (Num 6) t1)) (let t3 (Add t1 (Num 6))) (let t4 (Mul (Num 2) (Num 3))) -(let t5 (Add t1 t4)) -(let t6 (premises (Fiat (= t0 t0)))) -(let t7 - (substitution - (@rewrite_var__1 t0) - (a (Num 2)) - (b (Var "x")) - (c (Num 3)))) +(let t5 (premises (Fiat (= t0 t0)))) +(let t6 (substitution (@rewrite_var__1 t0) (a (Num 2)) (b (Var "x")) (c (Num 3)))) (Trans (= t0 t2) (Congr (= t0 t3) - (Sym - (= t0 t5) - (Rule - (= t5 t0) - (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") - t6 - t7)) + (Rule + (= t0 (Add t1 t4)) + (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") + t5 + t6) (Rule (= t4 (Num 6)) (name "(rewrite (Mul (Num a) (Num b)) (Num (* a b)))") @@ -33,12 +25,14 @@ expression: proof_snapshot (Rule (= t4 t4) (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") - t6 - t7)) + t5 + t6)) (substitution (a 2) (b 3) (@rewrite_var__3 t4))) 1) - (Rule + (Sym (= t3 t2) - (name "(rewrite (Add a b) (Add b a))") - (premises (Fiat (= t2 t2))) - (substitution (a (Num 6)) (b t1) (@rewrite_var__ t2)))) + (Rule + (= t2 t3) + (name "(rewrite (Add a b) (Add b a))") + (premises (Fiat (= t2 t2))) + (substitution (a (Num 6)) (b t1) (@rewrite_var__ t2))))) diff --git a/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap b/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap index d0d9e8f3..3fe9c1ee 100644 --- a/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap @@ -7,25 +7,17 @@ expression: proof_snapshot (let t2 (Add (Num 6) t1)) (let t3 (Add t1 (Num 6))) (let t4 (Mul (Num 2) (Num 3))) -(let t5 (Add t1 t4)) -(let t6 (premises (Fiat (= t0 t0)))) -(let t7 - (substitution - (@rewrite_var__1 t0) - (a (Num 2)) - (b (Var "x")) - (c (Num 3)))) +(let t5 (premises (Fiat (= t0 t0)))) +(let t6 (substitution (@rewrite_var__1 t0) (a (Num 2)) (b (Var "x")) (c (Num 3)))) (Trans (= t0 t2) (Congr (= t0 t3) - (Sym - (= t0 t5) - (Rule - (= t5 t0) - (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") - t6 - t7)) + (Rule + (= t0 (Add t1 t4)) + (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") + t5 + t6) (Rule (= t4 (Num 6)) (name "(rewrite (Mul (Num a) (Num b)) (Num (* a b)))") @@ -33,12 +25,14 @@ expression: proof_snapshot (Rule (= t4 t4) (name "(rewrite (Mul a (Add b c)) (Add (Mul a b) (Mul a c)))") - t6 - t7)) + t5 + t6)) (substitution (a 2) (b 3) (@rewrite_var__3 t4))) 1) - (Rule + (Sym (= t3 t2) - (name "(rewrite (Add a b) (Add b a))") - (premises (Fiat (= t2 t2))) - (substitution (a (Num 6)) (b t1) (@rewrite_var__ t2)))) + (Rule + (= t2 t3) + (name "(rewrite (Add a b) (Add b a))") + (premises (Fiat (= t2 t2))) + (substitution (a (Num 6)) (b t1) (@rewrite_var__ t2))))) diff --git a/egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap b/egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap index 0a04948d..4e697c27 100644 --- a/egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap @@ -2,301 +2,235 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(let t0 (Add (Fib 6) (Fib 5))) (let prf0 (Fiat (= () ()))) -(let t1 (premises (Fiat (= (Fib 7) (Fib 7))) prf0)) -(let t2 (substitution (@rewrite_var__1 (Fib 7)) (x 7))) -(let t3 (Add (Fib 5) (Fib 4))) -(let t4 (Add (Fib 4) (Fib 3))) -(let t5 +(let t0 (premises (Fiat (= (Fib 7) (Fib 7))) prf0)) +(let t1 (substitution (@rewrite_var__1 (Fib 7)) (x 7))) +(let t2 (premises - (Rule - (= (Fib 5) (Fib 5)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t1 - t2) - prf0)) -(let t6 (substitution (@rewrite_var__1 (Fib 5)) (x 5))) -(let t7 (Add (Fib 3) (Fib 2))) -(let t8 (Add (Fib 2) (Fib 1))) -(let t9 + (Rule + (= (Fib 5) (Fib 5)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t0 + t1) + prf0)) +(let t3 (substitution (@rewrite_var__1 (Fib 5)) (x 5))) +(let t4 + (premises + (Rule + (= (Fib 3) (Fib 3)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t2 + t3) + prf0)) +(let t5 + (substitution + (@rewrite_var__1 (Fib 3)) + (x 3))) +(let prf1 + (Rule + (= (Fib 1) (Num 1)) + (name + "(rewrite (Fib x) (Num x) :when ((<= x 1)))") + (premises + (Rule + (= (Fib 1) (Fib 1)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t4 + t5) + prf0) + (substitution + (@rewrite_var__2 (Fib 1)) + (x 1)))) +(let prop0 (= (Fib 2) (Num 1))) +(let t6 (premises (Rule - (= (Fib 3) (Fib 3)) + (= (Fib 2) (Fib 2)) (name "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t5 - t6) + t4 + t5) prf0)) -(let t10 +(let t7 (substitution (@rewrite_var__1 - (Fib 3)) - (x 3))) -(let prf1 - (Rule - (= (Num 1) (Fib 1)) - (name - "(rewrite (Fib x) (Num x) :when ((<= x 1)))") - (premises - (Rule - (= - (Fib 1) - (Fib 1)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t9 - t10) - prf0) - (substitution - (@rewrite_var__2 - (Fib 1)) - (x 1)))) + (Fib 2)) + (x 2))) (let prf2 - (Sym - (= (Fib 1) (Num 1)) - prf1)) -(let t11 - (Add - (Fib 1) - (Fib 0))) -(let t12 - (premises - (Rule - (= - (Fib 2) - (Fib 2)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t9 - t10) - prf0)) -(let t13 - (substitution - (@rewrite_var__1 - (Fib 2)) - (x 2))) -(let prf3 (Rule - (= (Num 1) (Fib 2)) - (name - "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") - (premises - (Congr - (= - (Fib 2) - (Add - (Num 1) - (Num 0))) - (Congr - (= - (Fib 2) - (Add - (Num 1) - (Fib 0))) - (Sym - (= (Fib 2) t11) - (Rule - (= - t11 - (Fib 2)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t12 - t13)) - prf2 - 0) - (Sym - (= - (Fib 0) - (Num 0)) - (Rule - (= - (Num 0) - (Fib 0)) - (name - "(rewrite (Fib x) (Num x) :when ((<= x 1)))") - (premises - (Rule - (= - (Fib 0) - (Fib 0)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t12 - t13) - prf0) - (substitution - (@rewrite_var__2 - (Fib 0)) - (x 0)))) - 1)) - (substitution - (a 1) - (b 0) - (@rewrite_var__ - (Fib 2))))) -(let prop0 (= (Fib 2) (Num 1))) -(let prf4 - (Trans - prop0 - (Trans - (= (Fib 2) (Fib 1)) - (Sym prop0 prf3) - prf1) - prf2)) -(let prf5 - (Sym - (= (Fib 3) (Num 2)) - (Rule - (= (Num 2) (Fib 3)) - (name - "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") - (premises - (Congr - (= - (Fib 3) - (Add (Num 1) (Num 1))) - (Congr - (= - (Fib 3) - (Add (Num 1) (Fib 2))) + prop0 + (name + "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") + (premises (Congr (= - (Fib 3) - (Add (Fib 2) (Fib 2))) - (Sym - (= (Fib 3) t8) + (Fib 2) + (Add (Num 1) (Num 0))) + (Congr + (= + (Fib 2) + (Add (Num 1) (Fib 0))) (Rule - (= t8 (Fib 3)) + (= + (Fib 2) + (Add (Fib 1) (Fib 0))) (name "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t9 - t10)) - (Trans - (= (Fib 1) (Fib 2)) - prf2 - prf3) - 1) - prf4 - 0) - prf4 - 1)) - (substitution - (a 1) - (b 1) - (@rewrite_var__ (Fib 3)))))) -(let prf6 - (Sym - (= (Fib 4) (Num 3)) - (Rule - (= (Num 3) (Fib 4)) + t6 + t7) + prf1 + 0) + (Rule + (= (Fib 0) (Num 0)) + (name + "(rewrite (Fib x) (Num x) :when ((<= x 1)))") + (premises + (Rule + (= (Fib 0) (Fib 0)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t6 + t7) + prf0) + (substitution + (@rewrite_var__2 + (Fib 0)) + (x 0))) + 1)) + (substitution + (a 1) + (b 0) + (@rewrite_var__ (Fib 2))))) +(let prf3 + (Trans + prop0 + (Trans + (= (Fib 2) (Fib 1)) + prf2 + (Sym (= (Num 1) (Fib 1)) prf1)) + prf1)) +(let prf4 + (Rule + (= (Fib 3) (Num 2)) (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") (premises (Congr - (= (Fib 4) (Add (Num 2) (Num 1))) + (= (Fib 3) (Add (Num 1) (Num 1))) (Congr - (= (Fib 4) (Add (Num 2) (Fib 2))) - (Sym - (= (Fib 4) t7) + (= (Fib 3) (Add (Num 1) (Fib 2))) + (Congr + (= (Fib 3) (Add (Fib 2) (Fib 2))) (Rule - (= t7 (Fib 4)) + (= (Fib 3) (Add (Fib 2) (Fib 1))) (name "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - (premises - (Rule - (= (Fib 4) (Fib 4)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t5 - t6) - prf0) - (substitution - (@rewrite_var__1 (Fib 4)) - (x 4)))) - prf5 + t4 + t5) + (Trans + (= (Fib 1) (Fib 2)) + prf1 + (Sym (= (Num 1) (Fib 2)) prf2)) + 1) + prf3 0) - prf4 + prf3 1)) (substitution - (a 2) + (a 1) (b 1) - (@rewrite_var__ (Fib 4)))))) -(let prf7 - (Sym - (= (Fib 5) (Num 5)) - (Rule - (= (Num 5) (Fib 5)) - (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") - (premises - (Congr - (= (Fib 5) (Add (Num 3) (Num 2))) + (@rewrite_var__ (Fib 3))))) +(let prf5 + (Rule + (= (Fib 4) (Num 3)) + (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") + (premises (Congr - (= (Fib 5) (Add (Num 3) (Fib 3))) - (Sym - (= (Fib 5) t4) + (= (Fib 4) (Add (Num 2) (Num 1))) + (Congr + (= (Fib 4) (Add (Num 2) (Fib 2))) (Rule - (= t4 (Fib 5)) + (= (Fib 4) (Add (Fib 3) (Fib 2))) (name "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t5 - t6)) - prf6 - 0) - prf5 - 1)) - (substitution (a 3) (b 2) (@rewrite_var__ (Fib 5)))))) -(Sym - (= (Fib 7) (Num 13)) + (premises + (Rule + (= (Fib 4) (Fib 4)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t2 + t3) + prf0) + (substitution (@rewrite_var__1 (Fib 4)) (x 4))) + prf4 + 0) + prf3 + 1)) + (substitution (a 2) (b 1) (@rewrite_var__ (Fib 4))))) +(let prf6 (Rule - (= (Num 13) (Fib 7)) - (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") - (premises - (Congr - (= (Fib 7) (Add (Num 8) (Num 5))) - (Congr - (= (Fib 7) (Add (Num 8) (Fib 5))) - (Sym - (= (Fib 7) t0) - (Rule - (= t0 (Fib 7)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t1 - t2)) - (Sym - (= (Fib 6) (Num 8)) - (Rule - (= (Num 8) (Fib 6)) - (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") - (premises - (Congr - (= (Fib 6) (Add (Num 5) (Num 3))) - (Congr - (= (Fib 6) (Add (Num 5) (Fib 4))) - (Sym - (= (Fib 6) t3) - (Rule - (= t3 (Fib 6)) - (name - "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - (premises + (= (Fib 5) (Num 5)) + (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") + (premises + (Congr + (= (Fib 5) (Add (Num 3) (Num 2))) + (Congr + (= (Fib 5) (Add (Num 3) (Fib 3))) (Rule - (= (Fib 6) (Fib 6)) + (= (Fib 5) (Add (Fib 4) (Fib 3))) (name "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") - t1 - t2) - prf0) - (substitution (@rewrite_var__1 (Fib 6)) (x 6)))) - prf7 - 0) - prf6 - 1)) - (substitution (a 5) (b 3) (@rewrite_var__ (Fib 6))))) - 0) - prf7 - 1)) - (substitution (a 8) (b 5) (@rewrite_var__ (Fib 7))))) + t2 + t3) + prf5 + 0) + prf4 + 1)) + (substitution (a 3) (b 2) (@rewrite_var__ (Fib 5))))) +(Rule + (= (Fib 7) (Num 13)) + (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") + (premises + (Congr + (= (Fib 7) (Add (Num 8) (Num 5))) + (Congr + (= (Fib 7) (Add (Num 8) (Fib 5))) + (Rule + (= (Fib 7) (Add (Fib 6) (Fib 5))) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t0 + t1) + (Rule + (= (Fib 6) (Num 8)) + (name "(rewrite (Add (Num a) (Num b)) (Num (+ a b)))") + (premises + (Congr + (= (Fib 6) (Add (Num 5) (Num 3))) + (Congr + (= (Fib 6) (Add (Num 5) (Fib 4))) + (Rule + (= (Fib 6) (Add (Fib 5) (Fib 4))) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + (premises + (Rule + (= (Fib 6) (Fib 6)) + (name + "(rewrite (Fib x) (Add (Fib (- x 1)) (Fib (- x 2))) :when ((> x 1)))") + t0 + t1) + prf0) + (substitution (@rewrite_var__1 (Fib 6)) (x 6))) + prf6 + 0) + prf5 + 1)) + (substitution (a 5) (b 3) (@rewrite_var__ (Fib 6)))) + 0) + prf6 + 1)) + (substitution (a 8) (b 5) (@rewrite_var__ (Fib 7)))) diff --git a/egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap b/egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap index 2d10a3c6..707f2444 100644 --- a/egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap @@ -23,4 +23,4 @@ expression: proof_snapshot (= (@ExistsConstructor16) (@ExistsConstructor16)) (name "@prove_exists_rule16") (premises prf0 (Fiat (= (Val 2) (Val 2))) prf0 prf0 (Fiat (= () ()))) - (substitution (@v270 (Val 1)) (@v273 (Val 1)) (@v271 (Val 2)) (@v272 (Val 1)))) + (substitution (@v710 (Val 1)) (@v711 (Val 1)) (@v708 (Val 1)) (@v709 (Val 2)))) diff --git a/egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap b/egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap index 0c33500f..831300be 100644 --- a/egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap @@ -24,13 +24,11 @@ expression: proof_snapshot (Congr (= t3 (Add (Var "c") (Const 0))) (Fiat (= t3 t3)) - (Sym + (Rule (= t2 (Const 0)) - (Rule - (= (Const 0) t2) - (name "(rewrite (Sub a a) (Const 0))") - (premises (Fiat (= t2 t2))) - (substitution (a t1) (@rewrite_var__15 t2)))) + (name "(rewrite (Sub a a) (Const 0))") + (premises (Fiat (= t2 t2))) + (substitution (a t1) (@rewrite_var__15 t2))) 1)) (substitution (a (Var "c")) (@rewrite_var__12 t3))) 1) @@ -41,11 +39,13 @@ expression: proof_snapshot (Congr (= t8 (Div t0 t7)) (Fiat (= t8 t8)) - (Rule + (Sym (= t5 t0) - (name "(rewrite (Mul x (Pow (Const 2) y)) (LShift x y))") - (premises (Fiat (= t0 t0))) - (substitution (y (Const 3)) (@rewrite_var__24 t0) (x (Var "a")))) + (Rule + (= t0 t5) + (name "(rewrite (Mul x (Pow (Const 2) y)) (LShift x y))") + (premises (Fiat (= t0 t0))) + (substitution (y (Const 3)) (@rewrite_var__24 t0) (x (Var "a"))))) 0) (Rule (= t7 (Var "c")) diff --git a/egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap b/egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap index c2361d3f..603a738a 100644 --- a/egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap @@ -8,124 +8,112 @@ expression: proof_snapshot (let t3 (f (f (Var "a2")))) (let t4 (intersect t2 t3)) (let t5 (intersect (f (Var "a1")) (f (Var "a2")))) -(let t6 (f t5)) -(let t7 (intersect (Var "a1") (Var "a2"))) -(let t8 +(let t6 (intersect (Var "a1") (Var "a2"))) +(let t7 (premises - (Sym (= (Var "a3") t7) (Fiat (= t7 (Var "a3")))) - (Fiat (= (f (Var "a1")) (f (Var "a1")))) - (Fiat (= (f (Var "a2")) (f (Var "a2")))))) -(let t9 (f1 (f (Var "a1")))) -(let t10 + (Sym (= (Var "a3") t6) (Fiat (= t6 (Var "a3")))) + (Fiat (= (f (Var "a1")) (f (Var "a1")))) + (Fiat (= (f (Var "a2")) (f (Var "a2")))))) +(let t8 (f1 (f (Var "a1")))) +(let t9 (substitution - (x1 (Var "a1")) - (x2 (Var "a2")) - (f2 (f (Var "a2"))) - (x3 (Var "a3")) - t9)) + (x1 (Var "a1")) + (x2 (Var "a2")) + (f2 (f (Var "a2"))) + (x3 (Var "a3")) + t8)) (let prf0 (Fiat (= t2 t2))) -(let t11 (x1 (f (Var "a1")))) -(let t12 (f2 t3)) -(let t13 (f1 t2)) -(let t14 (intersect (f (Var "a1")) (f (Var "b2")))) -(let t15 (f t14)) -(let t16 (intersect (Var "b1") (Var "b2"))) -(let t17 +(let t10 (x1 (f (Var "a1")))) +(let t11 (f2 t3)) +(let t12 (f1 t2)) +(let t13 (intersect (f (Var "a1")) (f (Var "b2")))) +(let t14 (intersect (Var "b1") (Var "b2"))) +(let t15 (premises - (Sym (= (Var "b3") t16) (Fiat (= t16 (Var "b3")))) - (Sym - (= (f (Var "a1")) (f (Var "b1"))) - (Fiat (= (f (Var "b1")) (f (Var "a1"))))) - (Fiat (= (f (Var "b2")) (f (Var "b2")))))) -(let t18 + (Sym (= (Var "b3") t14) (Fiat (= t14 (Var "b3")))) + (Fiat (= (f (Var "a1")) (f (Var "b1")))) + (Fiat (= (f (Var "b2")) (f (Var "b2")))))) +(let t16 (substitution - (x1 (Var "b1")) - (x2 (Var "b2")) - (f2 (f (Var "b2"))) - (x3 (Var "b3")) - t9)) -(let t19 (f (f (Var "b2")))) + (x1 (Var "b1")) + (x2 (Var "b2")) + (f2 (f (Var "b2"))) + (x3 (Var "b3")) + t8)) +(let t17 (f (f (Var "b2")))) (Trans (= t0 t1) (Sym (= t0 t4) (Congr (= t4 t0) - (Sym - (= t4 t6) - (Rule - (= t6 t4) - (name - "(rule ((= x3 (intersect x1 x2)) + (Rule + (= t4 (f t5)) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - (premises - (Rule - (= t5 t5) - (name - "(rule ((= x3 (intersect x1 x2)) + (premises + (Rule + (= t5 t5) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - t8 - t10) - prf0 - (Fiat (= t3 t3))) - (substitution t11 (x2 (f (Var "a2"))) t12 (x3 t5) t13))) - (Sym + t7 + t9) + prf0 + (Fiat (= t3 t3))) + (substitution t10 (x2 (f (Var "a2"))) t11 (x3 t5) t12)) + (Rule (= t5 (f (Var "a3"))) - (Rule - (= (f (Var "a3")) t5) - (name - "(rule ((= x3 (intersect x1 x2)) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - t8 - t10)) + t7 + t9) 0)) (Congr (= t4 t1) - (Sym - (= t4 t15) - (Rule - (= t15 t4) - (name - "(rule ((= x3 (intersect x1 x2)) + (Rule + (= t4 (f t13)) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - (premises - (Rule - (= t14 t14) - (name - "(rule ((= x3 (intersect x1 x2)) + (premises + (Rule + (= t13 t13) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - t17 - t18) - prf0 - (Sym (= t3 t19) (Fiat (= t19 t3)))) - (substitution t11 (x2 (f (Var "b2"))) t12 (x3 t14) t13))) - (Sym - (= t14 (f (Var "b3"))) - (Rule - (= (f (Var "b3")) t14) - (name - "(rule ((= x3 (intersect x1 x2)) + t15 + t16) + prf0 + (Sym (= t3 t17) (Fiat (= t17 t3)))) + (substitution t10 (x2 (f (Var "b2"))) t11 (x3 t13) t12)) + (Rule + (= t13 (f (Var "b3"))) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - t17 - t18)) + t15 + t16) 0)) (let t0 (intersect (f (Var "a1")) (f (Var "a2")))) (let t1 (intersect (Var "a1") (Var "a2"))) @@ -136,43 +124,45 @@ expression: proof_snapshot (= (@ExistsConstructor1) (@ExistsConstructor1)) (name "@prove_exists_rule1") (premises - (Rule + (Sym (= (f (Var "a3")) t0) - (name - "(rule ((= x3 (intersect x1 x2)) + (Rule + (= t0 (f (Var "a3"))) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - (premises - (Sym (= (Var "a3") t1) (Fiat (= t1 (Var "a3")))) - (Fiat (= (f (Var "a1")) (f (Var "a1")))) - (Fiat (= (f (Var "a2")) (f (Var "a2"))))) - (substitution - (x1 (Var "a1")) - (x2 (Var "a2")) - (f2 (f (Var "a2"))) - (x3 (Var "a3")) - t2)) - (Rule + (premises + (Sym (= (Var "a3") t1) (Fiat (= t1 (Var "a3")))) + (Fiat (= (f (Var "a1")) (f (Var "a1")))) + (Fiat (= (f (Var "a2")) (f (Var "a2"))))) + (substitution + (x1 (Var "a1")) + (x2 (Var "a2")) + (f2 (f (Var "a2"))) + (x3 (Var "a3")) + t2))) + (Sym (= (f (Var "b3")) t3) - (name - "(rule ((= x3 (intersect x1 x2)) + (Rule + (= t3 (f (Var "b3"))) + (name + "(rule ((= x3 (intersect x1 x2)) (= f1 (f x1)) (= f2 (f x2))) ((union (intersect f1 f2) (f x3))) )") - (premises - (Sym (= (Var "b3") t4) (Fiat (= t4 (Var "b3")))) - (Sym - (= (f (Var "a1")) (f (Var "b1"))) - (Fiat (= (f (Var "b1")) (f (Var "a1"))))) - (Fiat (= (f (Var "b2")) (f (Var "b2"))))) - (substitution - (x1 (Var "b1")) - (x2 (Var "b2")) - (f2 (f (Var "b2"))) - (x3 (Var "b3")) - t2)) + (premises + (Sym (= (Var "b3") t4) (Fiat (= t4 (Var "b3")))) + (Fiat (= (f (Var "a1")) (f (Var "b1")))) + (Fiat (= (f (Var "b2")) (f (Var "b2"))))) + (substitution + (x1 (Var "b1")) + (x2 (Var "b2")) + (f2 (f (Var "b2"))) + (x3 (Var "b3")) + t2))) (Fiat (= () ()))) - (substitution (@v270 t3) (@v269 t0))) + (substitution (@v620 t3) (@v619 t0))) diff --git a/egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap b/egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap index c9f6c3c9..c6583938 100644 --- a/egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap @@ -10,29 +10,37 @@ expression: proof_snapshot (let t4 (nrows (MMul t0 t3))) (let prf0 (Rule - (= t1 t4) - (name "(rewrite (nrows (MMul A B)) (nrows A))") - (premises (Fiat (= t4 t4))) - (substitution (B t3) (@rewrite_var__6 t4) (A t0)))) + (= t4 t1) + (name "(rewrite (nrows (MMul A B)) (nrows A))") + (premises (Fiat (= t4 t4))) + (substitution (B t3) (@rewrite_var__6 t4) (A t0)))) (let t5 (nrows (Id (NamedDim "n")))) -(let t6 (Times t5 (nrows (NamedMat "B")))) -(let t7 (A (Id (NamedDim "n")))) +(let t6 (Times t5 (NamedDim "m"))) +(let t7 (Times t5 (nrows (NamedMat "B")))) +(let t8 (premises prf0)) +(let t9 (A (Id (NamedDim "n")))) +(let t10 (substitution (@rewrite_var__4 t4) t9 (B (NamedMat "B")))) (Trans (= t1 t2) - prf0 + (Sym (= t1 t4) prf0) (Congr (= t4 t2) - (Congr - (= t4 (Times t5 (NamedDim "m"))) - (Sym - (= t4 t6) + (Trans + (= t4 t6) + (Rule + (= t4 t7) + (name "(rewrite (nrows (Kron A B)) (Times (nrows A) (nrows B)))") + t8 + t10) + (Congr + (= t7 t6) (Rule - (= t6 t4) + (= t7 t7) (name "(rewrite (nrows (Kron A B)) (Times (nrows A) (nrows B)))") - (premises (Sym (= t4 t1) prf0)) - (substitution (@rewrite_var__4 t4) t7 (B (NamedMat "B"))))) - (Fiat (= (nrows (NamedMat "B")) (NamedDim "m"))) - 1) + t8 + t10) + (Fiat (= (nrows (NamedMat "B")) (NamedDim "m"))) + 1)) (Rule (= t5 (NamedDim "n")) (name "(rewrite (nrows (Id n)) n)") @@ -47,21 +55,22 @@ expression: proof_snapshot (nrows B)) )") (premises (Fiat (= t0 t0))) - (substitution (B (NamedMat "B")) (e t0) t7))) + (substitution (B (NamedMat "B")) (e t0) t9))) (substitution (n (NamedDim "n")) (@rewrite_var__8 t5))) 0)) (let t0 (Kron (Id (NamedDim "n")) (NamedMat "B"))) (let t1 (Kron (NamedMat "A") (Id (NamedDim "m")))) (let t2 (MMul t0 t1)) -(let t3 (MMul (Id (NamedDim "n")) (NamedMat "A"))) -(let t4 (MMul (NamedMat "B") (Id (NamedDim "m")))) -(let t5 (Kron t3 t4)) +(let prop0 (= t2 t2)) +(let t3 (Kron (NamedMat "A") (NamedMat "B"))) +(let t4 (MMul (Id (NamedDim "n")) (NamedMat "A"))) +(let t5 (MMul (NamedMat "B") (Id (NamedDim "m")))) (let t6 (ncols (Id (NamedDim "n")))) (let t7 (A (Id (NamedDim "n")))) (let t8 (nrows (Id (NamedDim "m")))) (let t9 (premises - (Fiat (= t2 t2)) + (Fiat prop0) (Trans (= t6 (nrows (NamedMat "A"))) (Rule @@ -114,39 +123,42 @@ expression: proof_snapshot (@rewrite_var__17 t2) (D (Id (NamedDim "m"))) (C (NamedMat "A")))) -(Congr - (= t2 (Kron (NamedMat "A") (NamedMat "B"))) +(let prf0 (Congr - (= t2 (Kron t3 (NamedMat "B"))) - (Sym - (= t2 t5) - (Rule - (= t5 t2) - (name - "(rewrite (MMul (Kron A B) (Kron C D)) (Kron (MMul A C) (MMul B D)) :when ((= (ncols A) (nrows C)) (= (ncols B) (nrows D))))") - t9 - t10)) - (Rule - (= t4 (NamedMat "B")) - (name "(rewrite (MMul A (Id n)) A)") - (premises + (= t2 t3) + (Congr + (= t2 (Kron t4 (NamedMat "B"))) (Rule - (= t4 t4) + (= t2 (Kron t4 t5)) (name "(rewrite (MMul (Kron A B) (Kron C D)) (Kron (MMul A C) (MMul B D)) :when ((= (ncols A) (nrows C)) (= (ncols B) (nrows D))))") t9 - t10)) - (substitution (n (NamedDim "m")) (@rewrite_var__11 t4) (A (NamedMat "B")))) - 1) - (Rule - (= t3 (NamedMat "A")) - (name "(rewrite (MMul (Id n) A) A)") - (premises + t10) + (Rule + (= t5 (NamedMat "B")) + (name "(rewrite (MMul A (Id n)) A)") + (premises + (Rule + (= t5 t5) + (name + "(rewrite (MMul (Kron A B) (Kron C D)) (Kron (MMul A C) (MMul B D)) :when ((= (ncols A) (nrows C)) (= (ncols B) (nrows D))))") + t9 + t10)) + (substitution + (n (NamedDim "m")) + (@rewrite_var__11 t5) + (A (NamedMat "B")))) + 1) (Rule - (= t3 t3) - (name - "(rewrite (MMul (Kron A B) (Kron C D)) (Kron (MMul A C) (MMul B D)) :when ((= (ncols A) (nrows C)) (= (ncols B) (nrows D))))") - t9 - t10)) - (substitution (@rewrite_var__10 t3) (A (NamedMat "A")) (n (NamedDim "n")))) - 0) + (= t4 (NamedMat "A")) + (name "(rewrite (MMul (Id n) A) A)") + (premises + (Rule + (= t4 t4) + (name + "(rewrite (MMul (Kron A B) (Kron C D)) (Kron (MMul A C) (MMul B D)) :when ((= (ncols A) (nrows C)) (= (ncols B) (nrows D))))") + t9 + t10)) + (substitution (@rewrite_var__10 t4) (A (NamedMat "A")) (n (NamedDim "n")))) + 0)) +(Trans prop0 prf0 (Sym (= t3 t2) prf0)) diff --git a/egglog/tests/snapshots/files__proofs__naturals_proof_testing.snap b/egglog/tests/snapshots/files__proofs__naturals_proof_testing.snap index d99b16ce..8a189ea8 100644 --- a/egglog/tests/snapshots/files__proofs__naturals_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__naturals_proof_testing.snap @@ -6,282 +6,250 @@ expression: proof_snapshot (let t1 (S t0)) (let t2 (Plus (S (S (Z))) (S (S (Z))))) (let t3 (Plus (S (Z)) (S (S (Z))))) -(let t4 (S t3)) -(let t5 (premises (Fiat (= t2 t2)))) -(let t6 (n (S (S (Z))))) -(let t7 (substitution (@rewrite_var__1 t2) (m (S (Z))) t6)) -(let t8 (Plus (Z) (S (S (Z))))) -(let t9 (S t8)) -(let t10 +(let t4 (premises (Fiat (= t2 t2)))) +(let t5 (n (S (S (Z))))) +(let t6 (substitution (@rewrite_var__1 t2) (m (S (Z))) t5)) +(let t7 (Plus (Z) (S (S (Z))))) +(let t8 (premises - (Rule - (= t3 t3) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t5 - t7))) -(let t11 (substitution (@rewrite_var__1 t3) (m (Z)) t6)) + (Rule + (= t3 t3) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t4 + t6))) +(let t9 (substitution (@rewrite_var__1 t3) (m (Z)) t5)) (Sym (= t1 t2) (Congr (= t2 t1) - (Sym - (= t2 t4) - (Rule (= t4 t2) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t5 t7)) + (Rule (= t2 (S t3)) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t4 t6) (Congr (= t3 t0) - (Sym - (= t3 t9) - (Rule - (= t9 t3) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t10 - t11)) (Rule - (= t8 (S (S (Z)))) + (= t3 (S t7)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t8 + t9) + (Rule + (= t7 (S (S (Z)))) (name "(rewrite (Plus (Z) n) n)") (premises (Rule - (= t8 t8) + (= t7 t7) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t10 - t11)) - (substitution t6 (@rewrite_var__ t8))) + t8 + t9)) + (substitution t5 (@rewrite_var__ t7))) 0) 0)) (let t0 (S (S (S (Z))))) (let t1 (Times (S (S (Z))) t0)) -(let t2 (Plus (S (S (Z))) (S (S (Z))))) +(let t2 (S t0)) (let t3 (Plus (S (S (Z))) t2)) -(let t4 (S t0)) -(let t5 (Plus (S (Z)) t4)) -(let t6 (S t5)) -(let t7 (Times (S (Z)) t0)) -(let t8 (Plus (S (S (Z))) t7)) -(let t9 (S t8)) -(let t10 (Plus t0 t7)) -(let t11 (premises (Fiat (= t1 t1)))) -(let t12 (n t0)) -(let t13 (substitution (m (S (Z))) t12 (@rewrite_var__3 t1))) -(let t14 +(let t4 (Plus (S (Z)) t2)) +(let t5 (S t4)) +(let t6 (Times (S (Z)) t0)) +(let t7 (Plus (S (S (Z))) t6)) +(let t8 (premises (Fiat (= t1 t1)))) +(let t9 (n t0)) +(let t10 (substitution (m (S (Z))) t9 (@rewrite_var__3 t1))) +(let t11 + (premises + (Rule + (= t1 (Plus t0 t6)) + (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") + t8 + t10))) +(let t12 (m (S (S (Z))))) +(let t13 (n t6)) +(let t14 (substitution (@rewrite_var__1 t1) t12 t13)) +(let t15 (S t2)) +(let t16 (Plus (S (Z)) t6)) +(let t17 (premises - (Sym - (= t1 t10) (Rule - (= t10 t1) - (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") + (= t7 t7) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t11 - t13)))) -(let t15 (m (S (S (Z))))) -(let t16 (n t7)) -(let t17 (substitution (@rewrite_var__1 t1) t15 t16)) -(let t18 (S t4)) -(let t19 (Plus (S (Z)) t7)) -(let t20 (S t19)) -(let t21 + t14))) +(let t18 (substitution (@rewrite_var__1 t7) (m (S (Z))) t13)) +(let t19 (Plus (Z) t6)) +(let t20 (premises - (Rule - (= t8 t8) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t14 - t17))) -(let t22 (substitution (@rewrite_var__1 t8) (m (S (Z))) t16)) -(let t23 (Plus (Z) t7)) -(let t24 (S t23)) -(let t25 + (Rule + (= t16 t16) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t17 + t18))) +(let t21 (substitution (@rewrite_var__1 t16) (m (Z)) t13)) +(let t22 (Times (Z) t0)) +(let t23 (Plus (S (S (Z))) t22)) +(let t24 (premises - (Rule - (= t19 t19) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t21 - t22))) -(let t26 (substitution (@rewrite_var__1 t19) (m (Z)) t16)) -(let t27 (Times (Z) t0)) -(let t28 (Plus (S (S (Z))) t27)) -(let t29 (S t28)) -(let t30 (Plus t0 t27)) -(let t31 + (Rule + (= t6 t6) + (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") + t8 + t10))) +(let t25 (substitution (m (Z)) t9 (@rewrite_var__3 t6))) +(let t26 + (premises + (Rule + (= t6 (Plus t0 t22)) + (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") + t24 + t25))) +(let t27 (substitution (@rewrite_var__1 t6) t12 (n t22))) +(let t28 (Plus (S (Z)) (Z))) +(let t29 (premises + (Congr + (= t23 (Plus (S (S (Z))) (Z))) + (Rule + (= t23 t23) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t26 + t27) + (Rule + (= t22 (Z)) + (name "(rewrite (Times (Z) n) (Z))") + (premises (Rule - (= t7 t7) + (= t22 t22) (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") - t11 - t13))) -(let t32 (substitution (m (Z)) t12 (@rewrite_var__3 t7))) -(let t33 + t24 + t25)) + (substitution (@rewrite_var__2 t22) t9)) + 1))) +(let t30 (substitution (@rewrite_var__1 t23) (m (S (Z))) (n (Z)))) +(let t31 (premises - (Sym - (= t7 t30) (Rule - (= t30 t7) - (name "(rewrite (Times (S m) n) (Plus n (Times m n)))") - t31 - t32)))) -(let t34 (substitution (@rewrite_var__1 t7) t15 (n t27))) -(let t35 (Plus (S (Z)) (Z))) -(let t36 (S t35)) -(let t37 - (premises - (Congr - (= t28 (Plus (S (S (Z))) (Z))) - (Rule - (= t28 t28) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t33 - t34) - (Rule - (= t27 (Z)) - (name "(rewrite (Times (Z) n) (Z))") - (premises - (Rule - (= t27 t27) - (name - "(rewrite (Times (S m) n) (Plus n (Times m n)))") - t31 - t32)) - (substitution (@rewrite_var__2 t27) t12)) - 1))) -(let t38 (substitution (@rewrite_var__1 t28) (m (S (Z))) (n (Z)))) -(let t39 (S (Plus (Z) (Z)))) -(let t40 - (premises - (Rule - (= t35 t35) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t37 - t38))) -(let t41 (substitution (@rewrite_var__1 t35) (m (Z)) (n (Z)))) -(let t42 (Plus (Z) t4)) -(let t43 (S t42)) -(let t44 (Plus (S (Z)) (S (S (Z))))) -(let t45 (S t44)) -(let t46 (premises (Fiat (= t2 t2)))) -(let t47 (n (S (S (Z))))) -(let t48 (substitution (@rewrite_var__1 t2) (m (S (Z))) t47)) -(let t49 (Plus (Z) (S (S (Z))))) -(let t50 (S t49)) -(let t51 + (= t28 t28) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t29 + t30))) +(let t32 (substitution (@rewrite_var__1 t28) (m (Z)) (n (Z)))) +(let t33 (Plus (Z) t2)) +(let t34 (Plus (S (S (Z))) (S (S (Z))))) +(let t35 (Plus (S (S (Z))) t34)) +(let t36 (Plus (S (Z)) (S (S (Z))))) +(let t37 (premises (Fiat (= t34 t34)))) +(let t38 (n (S (S (Z))))) +(let t39 (substitution (@rewrite_var__1 t34) (m (S (Z))) t38)) +(let t40 (Plus (Z) (S (S (Z))))) +(let t41 (premises (Rule - (= t44 t44) + (= t36 t36) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t46 - t48))) -(let t52 (substitution (@rewrite_var__1 t44) (m (Z)) t47)) -(let t53 - (premises - (Congr - (= t3 (Plus (S (S (Z))) t4)) - (Fiat (= t3 t3)) - (Congr - (= t2 t4) - (Sym - (= t2 t45) + t37 + t39))) +(let t42 (substitution (@rewrite_var__1 t36) (m (Z)) t38)) +(let prf0 + (Congr + (= t35 t3) + (Fiat (= t35 t35)) + (Congr + (= t34 t2) (Rule - (= t45 t2) + (= t34 (S t36)) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t46 - t48)) - (Congr - (= t44 t0) - (Sym - (= t44 t50) + t37 + t39) + (Congr + (= t36 t0) (Rule - (= t50 t44) + (= t36 (S t40)) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t51 - t52)) - (Rule - (= t49 (S (S (Z)))) - (name "(rewrite (Plus (Z) n) n)") - (premises - (Rule - (= t49 t49) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t51 - t52)) - (substitution t47 (@rewrite_var__ t49))) + t41 + t42) + (Rule + (= t40 (S (S (Z)))) + (name "(rewrite (Plus (Z) n) n)") + (premises + (Rule + (= t40 t40) + (name + "(rewrite (Plus (S m) n) (S (Plus m n)))") + t41 + t42)) + (substitution t38 (@rewrite_var__ t40))) + 0) 0) - 0) - 1))) -(let t54 (n t4)) -(let t55 (substitution (@rewrite_var__1 t3) (m (S (Z))) t54)) -(let t56 + 1)) +(let t43 (premises (Trans (= t3 t3) (Sym (= t3 t35) prf0) prf0))) +(let t44 (n t2)) +(let t45 (substitution (@rewrite_var__1 t3) (m (S (Z))) t44)) +(let t46 (premises - (Rule - (= t5 t5) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t53 - t55))) -(let t57 (substitution (@rewrite_var__1 t5) (m (Z)) t54)) + (Rule + (= t4 t4) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t43 + t45))) +(let t47 (substitution (@rewrite_var__1 t4) (m (Z)) t44)) (Trans (= t1 t3) (Congr - (= t1 t6) - (Sym - (= t1 t9) - (Rule (= t9 t1) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t14 t17)) + (= t1 t5) + (Rule + (= t1 (S t7)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t11 + t14) (Trans - (= t8 t5) + (= t7 t4) (Congr - (= t8 t18) - (Sym - (= t8 t20) - (Rule - (= t20 t8) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t21 - t22)) + (= t7 t15) + (Rule + (= t7 (S t16)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t17 + t18) (Congr - (= t19 t4) + (= t16 t2) (Congr - (= t19 (S t7)) - (Sym - (= t19 t24) - (Rule - (= t24 t19) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t25 - t26)) + (= t16 (S t6)) + (Rule + (= t16 (S t19)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t20 + t21) (Rule - (= t23 t7) + (= t19 t6) (name "(rewrite (Plus (Z) n) n)") (premises (Rule - (= t23 t23) + (= t19 t19) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t25 - t26)) - (substitution t16 (@rewrite_var__ t23))) + t20 + t21)) + (substitution t13 (@rewrite_var__ t19))) 0) (Congr - (= t7 t0) - (Sym - (= t7 t29) + (= t6 t0) + (Rule + (= t6 (S t23)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t26 + t27) + (Congr + (= t23 (S (S (Z)))) (Rule - (= t29 t7) + (= t23 (S t28)) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t33 - t34)) - (Congr - (= t28 (S (S (Z)))) - (Sym - (= t28 t36) + t29 + t30) + (Congr + (= t28 (S (Z))) (Rule - (= t36 t28) + (= t28 (S (Plus (Z) (Z)))) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t37 - t38)) - (Congr - (= t35 (S (Z))) - (Sym - (= t35 t39) - (Rule - (= t39 t35) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t40 - t41)) + t31 + t32) (Rule (= (Plus (Z) (Z)) (Z)) (name "(rewrite (Plus (Z) n) n)") @@ -289,8 +257,8 @@ expression: proof_snapshot (Rule (= (Plus (Z) (Z)) (Plus (Z) (Z))) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t40 - t41)) + t31 + t32)) (substitution (n (Z)) (@rewrite_var__ (Plus (Z) (Z))))) 0) 0) @@ -298,26 +266,26 @@ expression: proof_snapshot 0) 0) (Sym - (= t18 t5) + (= t15 t4) (Congr - (= t5 t18) - (Sym - (= t5 t43) - (Rule - (= t43 t5) - (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t56 - t57)) + (= t4 t15) + (Rule + (= t4 (S t33)) + (name "(rewrite (Plus (S m) n) (S (Plus m n)))") + t46 + t47) (Rule - (= t42 t4) + (= t33 t2) (name "(rewrite (Plus (Z) n) n)") (premises (Rule - (= t42 t42) + (= t33 t33) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") - t56 - t57)) - (substitution t54 (@rewrite_var__ t42))) + t46 + t47)) + (substitution t44 (@rewrite_var__ t33))) 0))) 0) - (Rule (= t6 t3) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t53 t55)) + (Sym + (= t5 t3) + (Rule (= t3 t5) (name "(rewrite (Plus (S m) n) (S (Plus m n)))") t43 t45))) diff --git a/egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap b/egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap index 4c513325..14ce2f5a 100644 --- a/egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap @@ -4,29 +4,27 @@ expression: proof_snapshot --- (let t0 (vec-of (vec-of (w (b))))) (let t1 (p t0)) -(Sym +(Rule (= t1 (b)) - (Rule - (= (b) t1) - (name "(rewrite (p (vec-of (vec-of (b)))) (b))") - (premises - (Fiat (= (b) (b))) - (Eval) + (name "(rewrite (p (vec-of (vec-of (b)))) (b))") + (premises + (Fiat (= (b) (b))) + (Eval) + (Congr + (= t1 (p (vec-of (vec-of (b))))) + (Fiat (= t1 t1)) (Congr - (= t1 (p (vec-of (vec-of (b))))) - (Fiat (= t1 t1)) + (= t0 (vec-of (vec-of (b)))) + (Fiat (= t0 t0)) (Congr - (= t0 (vec-of (vec-of (b)))) - (Fiat (= t0 t0)) - (Congr - (= (vec-of (w (b))) (vec-of (b))) - (Fiat (= (vec-of (w (b))) (vec-of (w (b))))) - (Rule - (= (w (b)) (b)) - (name "(rewrite (w x) x)") - (premises (Fiat (= (w (b)) (w (b))))) - (substitution (x (b)) (@rewrite_var__ (w (b))))) - 0) + (= (vec-of (w (b))) (vec-of (b))) + (Fiat (= (vec-of (w (b))) (vec-of (w (b))))) + (Rule + (= (w (b)) (b)) + (name "(rewrite (w x) x)") + (premises (Fiat (= (w (b)) (w (b))))) + (substitution (x (b)) (@rewrite_var__ (w (b))))) 0) - 0)) - (substitution (@rewrite_var__1 t1) (@v40 (b)) (@v41 (vec-of (vec-of (b))))))) + 0) + 0)) + (substitution (@rewrite_var__1 t1) (@v61 (b)) (@v62 (vec-of (vec-of (b)))))) diff --git a/egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap b/egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap index 3c47ec72..c6fdced8 100644 --- a/egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap @@ -3,7 +3,11 @@ source: egglog/tests/files.rs expression: proof_snapshot --- (let t0 (edge (mk 5) (mk 6))) -(Congr (= t0 (edge (mk 3) (mk 6))) (Fiat (= t0 t0)) (Fiat (= (mk 5) (mk 3))) 0) +(Congr + (= t0 (edge (mk 3) (mk 6))) + (Fiat (= t0 t0)) + (Sym (= (mk 5) (mk 3)) (Fiat (= (mk 3) (mk 5)))) + 0) (let t0 (path (mk 1) (mk 6))) (let t1 (path (mk 1) (mk 3))) (let t2 (path (mk 1) (mk 2))) @@ -39,6 +43,6 @@ expression: proof_snapshot (Congr (= t5 (edge (mk 3) (mk 6))) (Fiat (= t5 t5)) - (Fiat (= (mk 5) (mk 3))) + (Sym (= (mk 5) (mk 3)) (Fiat (= (mk 3) (mk 5)))) 0)) (substitution (z (mk 6)) (x (mk 1)) (y (mk 3)))) diff --git a/egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap b/egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap index 27a4e223..8e4da09f 100644 --- a/egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap @@ -2,5 +2,4 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(let t0 (S (S (S (ZeroConst))))) -(Sym (= (S (S (ZeroConst))) t0) (Fiat (= t0 (S (S (ZeroConst)))))) +(Fiat (= (S (S (ZeroConst))) (S (S (S (ZeroConst)))))) diff --git a/egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap b/egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap index 182cf240..f1e6accd 100644 --- a/egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap @@ -6,4 +6,4 @@ expression: proof_snapshot (= (@ExistsConstructor) (@ExistsConstructor)) (name "@prove_exists_rule") (premises (Fiat (= (foo 10) (foo 10))) (Fiat (= () ()))) - (substitution (@v8 10))) + (substitution (@v27 10))) diff --git a/egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap b/egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap index 4b401b80..79a510c0 100644 --- a/egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap @@ -2,5 +2,5 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(Sym (= (R 1) (R 2)) (Fiat (= (R 2) (R 1)))) -(Sym (= (R 1) (R 2)) (Fiat (= (R 2) (R 1)))) +(Fiat (= (R 1) (R 2))) +(Fiat (= (R 1) (R 2))) diff --git a/egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap b/egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap index 2861ca80..881b2925 100644 --- a/egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap @@ -2,5 +2,4 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(let t0 (Ass "x" (Const 10))) -(Sym (= (Prog (L 0)) t0) (Fiat (= t0 (Prog (L 0))))) +(Fiat (= (Prog (L 0)) (Ass "x" (Const 10)))) diff --git a/egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap b/egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap index 45cb8325..5090fb61 100644 --- a/egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap @@ -4,25 +4,22 @@ expression: proof_snapshot --- (let t0 (typeof (NilConst) (Lam "x" (TUnitConst) (Var "x")))) (let t1 (typeof (Cons "x" (TUnitConst) (NilConst)) (Var "x"))) -(let t2 (TArr (TUnitConst) t1)) -(let t3 (premises (Fiat (= t0 t0)))) -(let t4 +(let t2 (premises (Fiat (= t0 t0)))) +(let t3 (substitution - (e (Var "x")) - (ctx (NilConst)) - (@rewrite_var__3 t0) - (x "x") - (t1 (TUnitConst)))) + (e (Var "x")) + (ctx (NilConst)) + (@rewrite_var__3 t0) + (x "x") + (t1 (TUnitConst)))) (Congr (= t0 (TArr (TUnitConst) (TUnitConst))) - (Sym - (= t0 t2) - (Rule - (= t2 t0) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t3 - t4)) + (Rule + (= t0 (TArr (TUnitConst) t1)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t2 + t3) (Rule (= t1 (TUnitConst)) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") @@ -31,8 +28,8 @@ expression: proof_snapshot (= t1 t1) (name "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t3 - t4)) + t2 + t3)) (substitution (t (TUnitConst)) (ctx (NilConst)) diff --git a/egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap b/egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap index 6ee5d5fd..928ad872 100644 --- a/egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap @@ -2,10 +2,8 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(Sym +(Rule (= (A) (B)) - (Rule - (= (B) (A)) - (name "direct-union-with-heavy-tail") - (premises (Fiat (= (Trigger) (Trigger)))) - (substitution))) + (name "direct-union-with-heavy-tail") + (premises (Fiat (= (Trigger) (Trigger)))) + (substitution)) diff --git a/egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap b/egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap index 2090296d..03de7536 100644 --- a/egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap @@ -4,125 +4,173 @@ expression: proof_snapshot --- (let t0 (const-prop (L 14) (V "y") (I 10))) (let t1 (RProg (L 13) (Ass (V "y") (Const (I 10))))) -(let t2 (Ass (V "y") (Add (V "x") (V "zero")))) -(let prf0 (Sym (= (Prog (L 13)) t2) (Fiat (= t2 (Prog (L 13)))))) -(let t3 (const-prop (L 13) (V "x") (I 10))) +(let prf0 (Fiat (= (Prog (L 13)) (Ass (V "y") (Add (V "x") (V "zero")))))) +(let t2 (const-prop (L 13) (V "x") (I 10))) +(let t3 (RProg (L 5) (Prog (L 5)))) (let t4 (If (V "b") (L 6) (L 13))) (let t5 (RProg (L 5) t4)) -(let prop0 (= t5 t5)) -(let prf1 (Fiat (= t4 (Prog (L 5))))) -(let prf2 (Sym (= (Prog (L 5)) t4) prf1)) +(let prop0 (= t3 t5)) +(let prf1 (Fiat (= (Prog (L 5)) t4))) (let t6 (const-prop (L 5) (V "b") (TopConst))) -(let t7 (Ass (V "x") (Const (I 10)))) -(let t8 (RProg (L 4) t7)) -(let prop1 (= t8 t8)) -(let prf3 (Fiat (= t7 (Prog (L 4))))) -(let prf4 (Sym (= (Prog (L 4)) t7) prf3)) -(let prf5 +(let t7 (RProg (L 4) (Prog (L 4)))) +(let t8 (Ass (V "x") (Const (I 10)))) +(let t9 (RProg (L 4) t8)) +(let prop1 (= t7 t9)) +(let prf2 (Fiat (= (Prog (L 4)) t8))) +(let prf3 + (Congr + (= t9 t7) + (Rule + (= t9 t9) + (name + "(rule ((= (Prog l) (Ass x (Const val)))) + ((RProg l (Ass x (Const val)))) + )") + (premises prf2) + (substitution + (val (I 10)) + (l (L 4)) + (x (V "x")))) + (Sym (= t8 (Prog (L 4))) prf2) + 1)) +(let prf4 (Congr - prop1 - (Congr - (= t8 (RProg (L 4) (Prog (L 4)))) - (Rule prop1 - (name - "(rule ((= (Prog l) (Ass x (Const val)))) + (Trans (= t7 t7) (Sym prop1 prf3) prf3) + prf2 + 1)) +(let t10 (const-prop (L 4) (V "b") (TopConst))) +(let t11 (RProg (L 3) (Prog (L 3)))) +(let t12 (Ass (V "zero") (Const (I 0)))) +(let t13 (RProg (L 3) t12)) +(let prop2 (= t11 t13)) +(let prf5 (Fiat (= (Prog (L 3)) t12))) +(let prf6 + (Congr + (= t13 t11) + (Rule + (= t13 t13) + (name + "(rule ((= (Prog l) (Ass x (Const val)))) ((RProg l (Ass x (Const val)))) )") - (premises prf4) - (substitution - (val (I 10)) - (l (L 4)) - (x (V "x")))) - prf3 - 1) - prf4 - 1)) -(let t9 (const-prop (L 4) (V "b") (TopConst))) -(let t10 (Ass (V "zero") (Const (I 0)))) -(let t11 (RProg (L 3) t10)) -(let prop2 (= t11 t11)) -(let prf6 (Fiat (= t10 (Prog (L 3))))) -(let prf7 (Sym (= (Prog (L 3)) t10) prf6)) -(let prf8 + (premises prf5) + (substitution + (val (I 0)) + (l (L 3)) + (x (V "zero")))) + (Sym (= t12 (Prog (L 3))) prf5) + 1)) +(let prf7 (Congr - prop2 - (Congr - (= t11 (RProg (L 3) (Prog (L 3)))) - (Rule prop2 - (name - "(rule ((= (Prog l) (Ass x (Const val)))) + (Trans (= t11 t11) (Sym prop2 prf6) prf6) + prf5 + 1)) +(let t14 (const-prop (L 3) (V "b") (TopConst))) +(let t15 (RProg (L 2) (Prog (L 2)))) +(let t16 (Ass (V "one") (Const (I 1)))) +(let t17 (RProg (L 2) t16)) +(let prop3 (= t15 t17)) +(let prf8 (Fiat (= (Prog (L 2)) t16))) +(let prf9 + (Congr + (= t17 t15) + (Rule + (= t17 t17) + (name + "(rule ((= (Prog l) (Ass x (Const val)))) ((RProg l (Ass x (Const val)))) )") - (premises prf7) - (substitution - (val (I 0)) - (l (L 3)) - (x (V "zero")))) - prf6 - 1) - prf7 - 1)) -(let t12 (const-prop (L 3) (V "b") (TopConst))) -(let t13 (Ass (V "one") (Const (I 1)))) -(let t14 (RProg (L 2) t13)) -(let prop3 (= t14 t14)) -(let prf9 (Fiat (= t13 (Prog (L 2))))) -(let prf10 (Sym (= (Prog (L 2)) t13) prf9)) -(let t15 (const-prop (L 2) (V "b") (TopConst))) -(let t16 (Ass (V "ten") (Const (I 10)))) -(let t17 (RProg (L 1) t16)) -(let prop4 (= t17 t17)) -(let prf11 (Fiat (= t16 (Prog (L 1))))) -(let prf12 (Sym (= (Prog (L 1)) t16) prf11)) + (premises prf8) + (substitution + (val (I 1)) + (l (L 2)) + (x (V "one")))) + (Sym (= t16 (Prog (L 2))) prf8) + 1)) (let t18 (const-prop - (L 1) + (L 2) (V "b") (TopConst))) -(let t19 +(let t19 (RProg (L 1) (Prog (L 1)))) +(let t20 (Ass (V "ten") (Const (I 10)))) +(let t21 (RProg (L 1) t20)) +(let prop4 (= t19 t21)) +(let prf10 + (Fiat + (= (Prog (L 1)) t20))) +(let prf11 + (Congr + (= t21 t19) + (Rule + (= t21 t21) + (name + "(rule ((= (Prog l) (Ass x (Const val)))) + ((RProg l (Ass x (Const val)))) + )") + (premises prf10) + (substitution + (val (I 10)) + (l (L 1)) + (x (V "ten")))) + (Sym + (= t20 (Prog (L 1))) + prf10) + 1)) +(let t22 + (const-prop + (L 1) + (V "b") + (TopConst))) +(let t23 (RProg (L 0) (Prog (L 0)))) +(let t24 (Ass - (V "b") - (Const (TopConst)))) -(let t20 (RProg (L 0) t19)) -(let prop5 (= t20 t20)) -(let prf13 + (V "b") + (Const (TopConst)))) +(let t25 (RProg (L 0) t24)) +(let prop5 (= t23 t25)) +(let prf12 (Fiat - (= t19 (Prog (L 0))))) -(let prf14 - (Sym - (= (Prog (L 0)) t19) - prf13)) -(let prf15 (Fiat (= (TopConst) (TopConst)))) -(let prf16 (Fiat (= () ()))) -(let t21 (e (Const (I 10)))) -(let prf17 + (= + (Prog (L 0)) + t24))) +(let prf13 (Congr - prop0 - (Congr - (= t5 (RProg (L 5) (Prog (L 5)))) - (Rule - prop0 - (name - "(rule ((= (Prog l) (If b l1 l2)) - (= $Top (const-prop l b))) - ((RProg l (If b l1 l2))) + (= t25 t23) + (Rule + (= t25 t25) + (name + "(rule ((= (Prog l) (Ass x (Const val)))) + ((RProg l (Ass x (Const val)))) )") - (premises - prf2 + (premises prf12) + (substitution + (val (TopConst)) + (l (L 0)) + (x (V "b")))) + (Sym + (= t24 (Prog (L 0))) + prf12) + 1)) +(let prf14 (Fiat (= (TopConst) (TopConst)))) +(let prf15 (Fiat (= () ()))) +(let t26 (e (Const (I 10)))) +(let prf16 + (Congr + (= t5 t3) (Rule - (= t6 t6) + (= t5 t5) (name - "(rule ((RProg (L li) (Ass (V x) e)) - (= val (const-prop (L li) (V y))) - (!= x y)) - ((set (const-prop (L (+ li 1)) (V y)) val)) + "(rule ((= (Prog l) (If b l1 l2)) + (= $Top (const-prop l b))) + ((RProg l (If b l1 l2))) )") (premises - prf5 + prf1 (Rule - (= t9 t9) + (= t6 t6) (name "(rule ((RProg (L li) (Ass (V x) e)) (= val (const-prop (L li) (V y))) @@ -130,9 +178,9 @@ expression: proof_snapshot ((set (const-prop (L (+ li 1)) (V y)) val)) )") (premises - prf8 + prf4 (Rule - (= t12 t12) + (= t10 t10) (name "(rule ((RProg (L li) (Ass (V x) e)) (= val (const-prop (L li) (V y))) @@ -140,27 +188,9 @@ expression: proof_snapshot ((set (const-prop (L (+ li 1)) (V y)) val)) )") (premises - (Congr - prop3 - (Congr - (= t14 (RProg (L 2) (Prog (L 2)))) - (Rule - prop3 - (name - "(rule ((= (Prog l) (Ass x (Const val)))) - ((RProg l (Ass x (Const val)))) - )") - (premises prf10) - (substitution - (val (I 1)) - (l (L 2)) - (x (V "one")))) - prf9 - 1) - prf10 - 1) + prf7 (Rule - (= t15 t15) + (= t14 t14) (name "(rule ((RProg (L li) (Ass (V x) e)) (= val (const-prop (L li) (V y))) @@ -169,108 +199,100 @@ expression: proof_snapshot )") (premises (Congr - prop4 - (Congr - (= t17 (RProg (L 1) (Prog (L 1)))) - (Rule - prop4 - (name - "(rule ((= (Prog l) (Ass x (Const val)))) - ((RProg l (Ass x (Const val)))) - )") - (premises prf12) - (substitution - (val (I 10)) - (l (L 1)) - (x (V "ten")))) - prf11 - 1) - prf12 + prop3 + (Trans + (= t15 t15) + (Sym prop3 prf9) + prf9) + prf8 1) (Rule (= t18 t18) (name - "(rule ((RProg (L li) (Ass x (Const k)))) - ((set (const-prop (L (+ li 1)) x) k)) + "(rule ((RProg (L li) (Ass (V x) e)) + (= val (const-prop (L li) (V y))) + (!= x y)) + ((set (const-prop (L (+ li 1)) (V y)) val)) )") (premises (Congr - prop5 - (Congr - (= - t20 - (RProg (L 0) (Prog (L 0)))) - (Rule - prop5 - (name - "(rule ((= (Prog l) (Ass x (Const val)))) - ((RProg l (Ass x (Const val)))) + prop4 + (Trans + (= t19 t19) + (Sym prop4 prf11) + prf11) + prf10 + 1) + (Rule + (= t22 t22) + (name + "(rule ((RProg (L li) (Ass x (Const k)))) + ((set (const-prop (L (+ li 1)) x) k)) )") - (premises prf14) - (substitution - (val (TopConst)) - (l (L 0)) - (x (V "b")))) - prf13 - 1) - prf14 - 1)) + (premises + (Congr + prop5 + (Trans + (= t23 t23) + (Sym prop5 prf13) + prf13) + prf12 + 1)) + (substitution + (li 0) + (x (V "b")) + (k (TopConst)))) + prf14 + prf15) (substitution - (li 0) - (x (V "b")) - (k (TopConst)))) - prf15 - prf16) + t26 + (y "b") + (li 1) + (@n4 (TopConst)) + (x "ten") + (val (TopConst)))) + prf14 + prf15) (substitution - t21 + (e (Const (I 1))) (y "b") - (li 1) + (li 2) (@n4 (TopConst)) - (x "ten") + (x "one") (val (TopConst)))) - prf15 - prf16) + prf14 + prf15) (substitution - (e (Const (I 1))) + (e (Const (I 0))) (y "b") - (li 2) + (li 3) (@n4 (TopConst)) - (x "one") + (x "zero") (val (TopConst)))) - prf15 - prf16) + prf14 + prf15) (substitution - (e (Const (I 0))) + t26 (y "b") - (li 3) + (li 4) (@n4 (TopConst)) - (x "zero") + (x "x") (val (TopConst)))) - prf15 - prf16) + prf14) (substitution - t21 - (y "b") - (li 4) - (@n4 (TopConst)) - (x "x") - (val (TopConst)))) - prf15) - (substitution - (l1 (L 6)) - (l (L 5)) - (l2 (L 13)) - (@n21 (TopConst)) - (b (V "b")))) - prf1 - 1) - prf2 - 1)) -(let t22 (const-prop (L 5) (V "x") (I 10))) + (l1 (L 6)) + (l (L 5)) + (l2 (L 13)) + (@n21 (TopConst)) + (b (V "b")))) + (Sym (= t4 (Prog (L 5))) prf1) + 1)) +(let prf17 (Congr prop0 (Trans (= t3 t3) (Sym prop0 prf16) prf16) prf1 1)) +(let t27 (const-prop (L 5) (V "x") (I 10))) (let prf18 (Fiat (= (I 10) (I 10)))) (let prf19 (Rule - (= t3 t3) + (= t2 t2) (name "(rule ((RProg l (If b l1 l2)) (= val (const-prop l x))) @@ -280,12 +302,12 @@ expression: proof_snapshot (premises prf17 (Rule - (= t22 t22) + (= t27 t27) (name "(rule ((RProg (L li) (Ass x (Const k)))) ((set (const-prop (L (+ li 1)) x) k)) )") - (premises prf5) + (premises prf4) (substitution (li 4) (x (V "x")) (k (I 10)))) prf18) (substitution @@ -296,13 +318,13 @@ expression: proof_snapshot (val (I 10)) (b (V "b")) (@n18 (I 10))))) -(let t23 (const-prop (L 13) (V "zero") (I 0))) -(let t24 (const-prop (L 5) (V "zero") (I 0))) -(let t25 (const-prop (L 4) (V "zero") (I 0))) +(let t28 (const-prop (L 13) (V "zero") (I 0))) +(let t29 (const-prop (L 5) (V "zero") (I 0))) +(let t30 (const-prop (L 4) (V "zero") (I 0))) (let prf20 (Fiat (= (I 0) (I 0)))) (let prf21 (Rule - (= t23 t23) + (= t28 t28) (name "(rule ((RProg l (If b l1 l2)) (= val (const-prop l x))) @@ -312,7 +334,7 @@ expression: proof_snapshot (premises prf17 (Rule - (= t24 t24) + (= t29 t29) (name "(rule ((RProg (L li) (Ass (V x) e)) (= val (const-prop (L li) (V y))) @@ -320,19 +342,19 @@ expression: proof_snapshot ((set (const-prop (L (+ li 1)) (V y)) val)) )") (premises - prf5 + prf4 (Rule - (= t25 t25) + (= t30 t30) (name "(rule ((RProg (L li) (Ass x (Const k)))) ((set (const-prop (L (+ li 1)) x) k)) )") - (premises prf8) + (premises prf7) (substitution (li 3) (x (V "zero")) (k (I 0)))) prf20 - prf16) + prf15) (substitution - t21 + t26 (y "zero") (li 4) (@n4 (I 0)) @@ -347,7 +369,7 @@ expression: proof_snapshot (val (I 0)) (b (V "b")) (@n18 (I 0))))) -(let t26 (add-val (I 10) (I 0))) +(let t31 (add-val (I 10) (I 0))) (Rule (= (@ExistsConstructor) (@ExistsConstructor)) (name "@prove_exists_rule") @@ -371,13 +393,13 @@ expression: proof_snapshot prf19 prf21 (Sym - (= (I 10) t26) + (= (I 10) t31) (Rule - (= t26 (I 10)) + (= t31 (I 10)) (name "(rewrite (add-val (I x) (I y)) (I (+ x y)))") (premises (Rule - (= t26 t26) + (= t31 t31) (name "(rule ((= (Prog l) (Ass x (Add x1 x2))) (= v1 (const-prop l x1)) @@ -394,7 +416,7 @@ expression: proof_snapshot (@n6 (I 0)) (x (V "y")) (v1 (I 10))))) - (substitution (@rewrite_var__14 t26) (x 10) (y 0))))) + (substitution (@rewrite_var__14 t31) (x 10) (y 0))))) (substitution (x1 (V "x")) (x2 (V "zero")) diff --git a/egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap b/egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap index 066c2254..e0df6228 100644 --- a/egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap @@ -4,25 +4,22 @@ expression: proof_snapshot --- (let t0 (typeof (NilConst) (Lam "x" (TUnitConst) (Var "x")))) (let t1 (typeof (Cons "x" (TUnitConst) (NilConst)) (Var "x"))) -(let t2 (TArr (TUnitConst) t1)) -(let t3 (premises (Fiat (= t0 t0)))) -(let t4 +(let t2 (premises (Fiat (= t0 t0)))) +(let t3 (substitution - (e (Var "x")) - (ctx (NilConst)) - (@rewrite_var__3 t0) - (x "x") - (t1 (TUnitConst)))) + (e (Var "x")) + (ctx (NilConst)) + (@rewrite_var__3 t0) + (x "x") + (t1 (TUnitConst)))) (Congr (= t0 (TArr (TUnitConst) (TUnitConst))) - (Sym - (= t0 t2) - (Rule - (= t2 t0) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t3 - t4)) + (Rule + (= t0 (TArr (TUnitConst) t1)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t2 + t3) (Rule (= t1 (TUnitConst)) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") @@ -31,8 +28,8 @@ expression: proof_snapshot (= t1 t1) (name "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t3 - t4)) + t2 + t3)) (substitution (t (TUnitConst)) (ctx (NilConst)) @@ -66,90 +63,88 @@ expression: proof_snapshot (premises prf0) (substitution (t2 t5) t13 (ctx (NilConst)) t14))) (let t15 (typeof (NilConst) t2)) -(let t16 (TArr (TUnitConst) t12)) -(let t17 (premises prf1)) -(let t18 (f t2)) -(let t19 +(let t16 (premises prf1)) +(let t17 (f t2)) +(let t18 (substitution - (t2 t9) - (e (MyUnitConst)) - (ctx (NilConst)) - t18)) -(let t20 + (t2 t9) + (e (MyUnitConst)) + (ctx (NilConst)) + t17)) +(let t19 (premises - (Rule - (= t15 t15) - (name - "(rule ((= (typeof ctx (App f e)) t2)) + (Rule + (= t15 t15) + (name + "(rule ((= (typeof ctx (App f e)) t2)) ((typeof ctx f) (typeof ctx e)) )") - t17 - t19))) -(let t21 + t16 + t18))) +(let t20 (substitution - (e t1) - (ctx (NilConst)) - (@rewrite_var__3 t15) - (x "x") - (t1 (TUnitConst)))) -(let t22 + (e t1) + (ctx (NilConst)) + (@rewrite_var__3 t15) + (x "x") + (t1 (TUnitConst)))) +(let t21 (premises - (Rule - (= t12 t12) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t20 - t21))) -(let t23 (ctx t6)) -(let t24 + (Rule + (= t12 t12) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t19 + t20))) +(let t22 (ctx t6)) +(let t23 (substitution - (e t0) - t23 - (@rewrite_var__3 t12) - (x "f") - (t1 (TArr (TUnitConst) (TUnitConst))))) -(let t25 (typeof t6 (Var "x"))) -(let t26 (TArr (TUnitConst) t25)) -(let t27 (premises (Fiat (= t10 t10)))) -(let t28 + (e t0) + t22 + (@rewrite_var__3 t12) + (x "f") + (t1 (TArr (TUnitConst) (TUnitConst))))) +(let t24 (typeof t6 (Var "x"))) +(let t25 (premises (Fiat (= t10 t10)))) +(let t26 (substitution - (e (Var "x")) - (ctx (NilConst)) - (@rewrite_var__3 t10) - (x "x") - (t1 (TUnitConst)))) + (e (Var "x")) + (ctx (NilConst)) + (@rewrite_var__3 t10) + (x "x") + (t1 (TUnitConst)))) (let prf2 (Rule - (= t25 (TUnitConst)) + (= t24 (TUnitConst)) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") (premises (Rule - (= t25 t25) + (= t24 t24) (name "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t27 - t28)) + t25 + t26)) (substitution (t (TUnitConst)) (ctx (NilConst)) - (@rewrite_var__1 t25) + (@rewrite_var__1 t24) (x "x")))) -(let t29 (t2 t8)) -(let t30 (t1 t5)) -(let t31 (typeof t7 (Var "f"))) -(let t32 (typeof t7 (Var "x"))) -(let t33 (TArr t32 (TUnitConst))) -(let t34 +(let t27 (t2 t8)) +(let t28 (t1 t5)) +(let t29 (typeof t7 (Var "f"))) +(let t30 (typeof t7 (Var "x"))) +(let t31 (TArr t30 (TUnitConst))) +(let t32 (premises (Rule (= t8 t8) (name "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t22 - t24))) -(let t35 (ctx t7)) -(let t36 (substitution t29 (e (Var "x")) t35 (f (Var "f")))) + t21 + t23))) +(let t33 (ctx t7)) +(let t34 (substitution t27 (e (Var "x")) t33 (f (Var "f")))) (Rule (= t5 (TUnitConst)) (name @@ -184,14 +179,12 @@ expression: proof_snapshot prf1 (Congr (= t15 (TArr (typeof (NilConst) (MyUnitConst)) t12)) - (Sym - (= t15 t16) - (Rule - (= t16 t15) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t20 - t21)) + (Rule + (= t15 (TArr (TUnitConst) t12)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t19 + t20) (Sym (= (TUnitConst) (typeof (NilConst) (MyUnitConst))) (Rule @@ -207,8 +200,8 @@ expression: proof_snapshot ((typeof ctx f) (typeof ctx e)) )") - t17 - t19)) + t16 + t18)) (substitution (ctx (NilConst)) (@rewrite_var__ (typeof (NilConst) (MyUnitConst)))))) @@ -217,132 +210,117 @@ expression: proof_snapshot (t2 t12) (e (MyUnitConst)) (ctx (NilConst)) - t18 + t17 (t1 t9)))) - (Sym + (Rule (= t12 t11) - (Rule - (= t11 t12) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t22 - t24))) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t21 + t23)) (Sym (= (TArr (TUnitConst) (TUnitConst)) t10) (Congr (= t10 (TArr (TUnitConst) (TUnitConst))) - (Sym - (= t10 t26) - (Rule - (= t26 t10) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t27 - t28)) + (Rule + (= t10 (TArr (TUnitConst) t24)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t25 + t26) prf2 1)) 0)) - (substitution t29 t13 (ctx (NilConst)) t14 t30)) + (substitution t27 t13 (ctx (NilConst)) t14 t28)) (Trans - (= t31 t33) + (= t29 t31) (Rule - (= t31 (TArr (TUnitConst) (TUnitConst))) + (= t29 (TArr (TUnitConst) (TUnitConst))) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") (premises (Rule - (= t31 t31) + (= t29 t29) (name "(rule ((= (typeof ctx (App f e)) t2)) ((typeof ctx f) (typeof ctx e)) )") - t34 - t36)) + t32 + t34)) (substitution (t (TArr (TUnitConst) (TUnitConst))) - t23 - (@rewrite_var__1 t31) + t22 + (@rewrite_var__1 t29) (x "f"))) (Congr - (= (TArr (TUnitConst) (TUnitConst)) t33) + (= (TArr (TUnitConst) (TUnitConst)) t31) (Fiat (= (TArr (TUnitConst) (TUnitConst)) (TArr (TUnitConst) (TUnitConst)))) (Trans - (= (TUnitConst) t32) - (Sym (= (TUnitConst) t25) prf2) + (= (TUnitConst) t30) + (Sym (= (TUnitConst) t24) prf2) (Sym - (= t25 t32) + (= t24 t30) (Rule - (= t32 t25) + (= t30 t24) (name "(rewrite (typeof (Cons y ty ctx) (Var x)) (typeof ctx (Var x)) :when ((!= x y)))") (premises (Rule - (= t32 t32) + (= t30 t30) (name "(rule ((= (typeof ctx (App f e)) t2)) ((typeof ctx f) (typeof ctx e)) )") - t34 - t36) + t32 + t34) (Fiat (= () ()))) (substitution - (@rewrite_var__2 t32) + (@rewrite_var__2 t30) (ty (TArr (TUnitConst) (TUnitConst))) (y "f") - t23 + t22 (x "x"))))) 0))) - (substitution (t2 (TUnitConst)) (e (Var "x")) t35 (f (Var "f")) t30)) + (substitution (t2 (TUnitConst)) (e (Var "x")) t33 (f (Var "f")) t28)) (let t0 (Cons "y" (TUnitConst) (NilConst))) (let t1 (typeof t0 (Lam "x" (TUnitConst) (Var "y")))) (let t2 (typeof (Cons "x" (TUnitConst) t0) (Var "y"))) -(let t3 (TArr (TUnitConst) t2)) -(let t4 (premises (Fiat (= t1 t1)))) -(let t5 (ctx t0)) -(let t6 +(let t3 (premises (Fiat (= t1 t1)))) +(let t4 (ctx t0)) +(let t5 (substitution - (e (Var "y")) - t5 - (@rewrite_var__3 t1) - (x "x") - (t1 (TUnitConst)))) -(let t7 (typeof t0 (Var "y"))) + (e (Var "y")) + t4 + (@rewrite_var__3 t1) + (x "x") + (t1 (TUnitConst)))) (Congr (= t1 (TArr (TUnitConst) (TUnitConst))) - (Sym - (= t1 t3) - (Rule - (= t3 t1) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t4 - t6)) + (Rule + (= t1 (TArr (TUnitConst) t2)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t3 + t5) (Rule (= t2 (TUnitConst)) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") (premises - (Sym - (= t2 t7) - (Rule - (= t7 t2) - (name - "(rewrite (typeof (Cons y ty ctx) (Var x)) (typeof ctx (Var x)) :when ((!= x y)))") - (premises - (Rule - (= t2 t2) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t4 - t6) - (Fiat (= () ()))) - (substitution - (@rewrite_var__2 t2) - (ty (TUnitConst)) - (y "x") - t5 - (x "y"))))) + (Rule + (= t2 (typeof t0 (Var "y"))) + (name + "(rewrite (typeof (Cons y ty ctx) (Var x)) (typeof ctx (Var x)) :when ((!= x y)))") + (premises + (Rule + (= t2 t2) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t3 + t5) + (Fiat (= () ()))) + (substitution (@rewrite_var__2 t2) (ty (TUnitConst)) (y "x") t4 (x "y")))) (substitution (t (TUnitConst)) (ctx (NilConst)) @@ -353,50 +331,39 @@ expression: proof_snapshot (let t1 (Cons "y" t0 (NilConst))) (let t2 (typeof t1 (Lam "x" (TUnitConst) (Var "y")))) (let t3 (typeof (Cons "x" (TUnitConst) t1) (Var "y"))) -(let t4 (TArr (TUnitConst) t3)) -(let t5 (premises (Fiat (= t2 t2)))) -(let t6 (ctx t1)) -(let t7 +(let t4 (premises (Fiat (= t2 t2)))) +(let t5 (ctx t1)) +(let t6 (substitution - (e (Var "y")) - t6 - (@rewrite_var__3 t2) - (x "x") - (t1 (TUnitConst)))) -(let t8 (typeof t1 (Var "y"))) + (e (Var "y")) + t5 + (@rewrite_var__3 t2) + (x "x") + (t1 (TUnitConst)))) (Congr (= t2 (TArr (TUnitConst) t0)) - (Sym - (= t2 t4) - (Rule - (= t4 t2) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t5 - t7)) + (Rule + (= t2 (TArr (TUnitConst) t3)) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t4 + t6) (Rule (= t3 t0) (name "(rewrite (typeof (Cons x t ctx) (Var x)) t)") (premises - (Sym - (= t3 t8) - (Rule - (= t8 t3) - (name - "(rewrite (typeof (Cons y ty ctx) (Var x)) (typeof ctx (Var x)) :when ((!= x y)))") - (premises - (Rule - (= t3 t3) - (name - "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") - t5 - t7) - (Fiat (= () ()))) - (substitution - (@rewrite_var__2 t3) - (ty (TUnitConst)) - (y "x") - t6 - (x "y"))))) + (Rule + (= t3 (typeof t1 (Var "y"))) + (name + "(rewrite (typeof (Cons y ty ctx) (Var x)) (typeof ctx (Var x)) :when ((!= x y)))") + (premises + (Rule + (= t3 t3) + (name + "(rewrite (typeof ctx (Lam x t1 e)) (TArr t1 (typeof (Cons x t1 ctx) e)))") + t4 + t6) + (Fiat (= () ()))) + (substitution (@rewrite_var__2 t3) (ty (TUnitConst)) (y "x") t5 (x "y")))) (substitution (t t0) (ctx (NilConst)) (@rewrite_var__1 t3) (x "y"))) 1) diff --git a/egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap b/egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap index f1244f25..64a8b7e4 100644 --- a/egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap @@ -4,20 +4,20 @@ expression: proof_snapshot --- (let t0 (assign - (Stmt "int *v = malloc(sizeof(int))") - (Type "int *") - (Expr "v") - (Expr "malloc(sizeof(int))"))) + (Stmt "int *v = malloc(sizeof(int))") + (Type "int *") + (Expr "v") + (Expr "malloc(sizeof(int))"))) (let t1 (malloc (Expr "malloc(sizeof(int))") (Type "int"))) (let prf0 (Fiat (= t1 t1))) (let t2 (premises (Fiat (= t0 t0)) prf0)) (let t3 (substitution - (v (Expr "v")) - (t2 (Type "int")) - (s (Stmt "int *v = malloc(sizeof(int))")) - (c (Expr "malloc(sizeof(int))")) - (t1 (Type "int *")))) + (v (Expr "v")) + (t2 (Type "int")) + (s (Stmt "int *v = malloc(sizeof(int))")) + (c (Expr "malloc(sizeof(int))")) + (t1 (Type "int *")))) (let t4 (struct-lit-field (Expr "(struct s){u, v}") @@ -46,15 +46,17 @@ expression: proof_snapshot (= (AllocVar (Expr "v")) (AllocVar (Expr "u"))) (Trans (= (AllocVar (Expr "v")) (expr-points-to (Expr "u"))) - (Rule + (Sym (= (AllocVar (Expr "v")) (expr-points-to (Expr "v"))) - (name - "(rule ((assign s t1 v c) + (Rule + (= (expr-points-to (Expr "v")) (AllocVar (Expr "v"))) + (name + "(rule ((assign s t1 v c) (malloc c t2)) ((union (expr-points-to v) (AllocVar v))) )") - t2 - t3) + t2 + t3)) (Trans (= (expr-points-to (Expr "v")) (expr-points-to (Expr "u"))) (Sym @@ -111,33 +113,31 @@ expression: proof_snapshot (f (Field "x")) (x (Expr "u")) (b (expr-points-to (Expr "u"))))))) - (Sym + (Rule (= (expr-points-to (Expr "u")) (AllocVar (Expr "u"))) - (Rule - (= (AllocVar (Expr "u")) (expr-points-to (Expr "u"))) - (name - "(rule ((assign s t1 v c) + (name + "(rule ((assign s t1 v c) (malloc c t2)) ((union (expr-points-to v) (AllocVar v))) )") - t7 - t8))) + t7 + t8)) (let t0 (assign - (Stmt "int *v = malloc(sizeof(int))") - (Type "int *") - (Expr "v") - (Expr "malloc(sizeof(int))"))) + (Stmt "int *v = malloc(sizeof(int))") + (Type "int *") + (Expr "v") + (Expr "malloc(sizeof(int))"))) (let t1 (malloc (Expr "malloc(sizeof(int))") (Type "int"))) (let prf0 (Fiat (= t1 t1))) (let t2 (premises (Fiat (= t0 t0)) prf0)) (let t3 (substitution - (v (Expr "v")) - (t2 (Type "int")) - (s (Stmt "int *v = malloc(sizeof(int))")) - (c (Expr "malloc(sizeof(int))")) - (t1 (Type "int *")))) + (v (Expr "v")) + (t2 (Type "int")) + (s (Stmt "int *v = malloc(sizeof(int))")) + (c (Expr "malloc(sizeof(int))")) + (t1 (Type "int *")))) (let t4 (struct-lit-field (Expr "(struct s){u, v}") @@ -156,10 +156,10 @@ expression: proof_snapshot (Expr "malloc(sizeof(int))"))) (let t7 (assign - (Stmt "struct s *sp = malloc(sizeof(struct s))") - (Type "struct s*") - (Expr "sp") - (Expr "malloc(sizeof(struct s))"))) + (Stmt "struct s *sp = malloc(sizeof(struct s))") + (Type "struct s*") + (Expr "sp") + (Expr "malloc(sizeof(struct s))"))) (let t8 (malloc (Expr "malloc(sizeof(struct s))") (Type "struct s"))) (Rule (= (@ExistsConstructor1) (@ExistsConstructor1)) @@ -167,15 +167,17 @@ expression: proof_snapshot (premises (Trans (= (AllocVar (Expr "v")) (expr-points-to (Expr "u"))) - (Rule + (Sym (= (AllocVar (Expr "v")) (expr-points-to (Expr "v"))) - (name - "(rule ((assign s t1 v c) + (Rule + (= (expr-points-to (Expr "v")) (AllocVar (Expr "v"))) + (name + "(rule ((assign s t1 v c) (malloc c t2)) ((union (expr-points-to v) (AllocVar v))) )") - t2 - t3) + t2 + t3)) (Trans (= (expr-points-to (Expr "v")) (expr-points-to (Expr "u"))) (Sym @@ -237,21 +239,23 @@ expression: proof_snapshot (f (Field "x")) (x (Expr "u")) (b (expr-points-to (Expr "u"))))))) - (Rule + (Sym (= (AllocVar (Expr "sp")) (expr-points-to (Expr "sp"))) - (name - "(rule ((assign s t1 v c) + (Rule + (= (expr-points-to (Expr "sp")) (AllocVar (Expr "sp"))) + (name + "(rule ((assign s t1 v c) (malloc c t2)) ((union (expr-points-to v) (AllocVar v))) )") - (premises (Fiat (= t7 t7)) (Fiat (= t8 t8))) - (substitution - (v (Expr "sp")) - (t2 (Type "struct s")) - (s (Stmt "struct s *sp = malloc(sizeof(struct s))")) - (c (Expr "malloc(sizeof(struct s))")) - (t1 (Type "struct s*")))) + (premises (Fiat (= t7 t7)) (Fiat (= t8 t8))) + (substitution + (v (Expr "sp")) + (t2 (Type "struct s")) + (s (Stmt "struct s *sp = malloc(sizeof(struct s))")) + (c (Expr "malloc(sizeof(struct s))")) + (t1 (Type "struct s*"))))) (Fiat (= () ()))) (substitution - (@v920 (expr-points-to (Expr "u"))) - (@v921 (expr-points-to (Expr "sp"))))) + (@v3017 (expr-points-to (Expr "sp"))) + (@v3016 (expr-points-to (Expr "u"))))) diff --git a/egglog/tests/snapshots/files__proofs__unify_proof_testing.snap b/egglog/tests/snapshots/files__proofs__unify_proof_testing.snap index fa13c080..ad075110 100644 --- a/egglog/tests/snapshots/files__proofs__unify_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__unify_proof_testing.snap @@ -2,8 +2,6 @@ source: egglog/tests/files.rs expression: proof_snapshot --- -(let t0 (Mul (Var "a") (Var "a"))) -(let t1 (Mul (Lit 1) (Lit 2))) (Sym (= (Var "a") (Lit 1)) (Rule @@ -13,12 +11,10 @@ expression: proof_snapshot ((union a c) (union b d)) )") - (premises (Sym (= t0 t1) (Fiat (= t1 t0)))) + (premises (Fiat (= (Mul (Var "a") (Var "a")) (Mul (Lit 1) (Lit 2))))) (substitution (d (Lit 2)) (a (Var "a")) (b (Var "a")) (c (Lit 1))))) -(let t0 (Mul (Var "a") (Var "a"))) -(let t1 (Mul (Lit 1) (Lit 2))) -(let t2 (premises (Sym (= t0 t1) (Fiat (= t1 t0))))) -(let t3 (substitution (d (Lit 2)) (a (Var "a")) (b (Var "a")) (c (Lit 1)))) +(let t0 (premises (Fiat (= (Mul (Var "a") (Var "a")) (Mul (Lit 1) (Lit 2)))))) +(let t1 (substitution (d (Lit 2)) (a (Var "a")) (b (Var "a")) (c (Lit 1)))) (Trans (= (Lit 2) (Lit 1)) (Rule @@ -28,8 +24,8 @@ expression: proof_snapshot ((union a c) (union b d)) )") - t2 - t3) + t0 + t1) (Sym (= (Var "a") (Lit 1)) (Rule @@ -39,5 +35,5 @@ expression: proof_snapshot ((union a c) (union b d)) )") - t2 - t3))) + t0 + t1))) diff --git a/egglog/tests/snapshots/files__proofs__until_proof_testing.snap b/egglog/tests/snapshots/files__proofs__until_proof_testing.snap index d186e857..e4408b9a 100644 --- a/egglog/tests/snapshots/files__proofs__until_proof_testing.snap +++ b/egglog/tests/snapshots/files__proofs__until_proof_testing.snap @@ -4,23 +4,20 @@ expression: proof_snapshot --- (let t0 (g* (g* (AConst) (AConst)) (g* (AConst) (AConst)))) (let t1 (g* t0 t0)) -(let t2 (g* (AConst) (g* (AConst) (g* (AConst) (AConst))))) (let prf0 (Rule (= t0 (IConst)) (name "(rewrite (g* $A (g* $A (g* $A $A))) $I)") (premises - (Sym - (= t0 t2) - (Rule - (= t2 t0) - (name "(birewrite (g* (g* a b) c) (g* a (g* b c)))=>") - (premises (Fiat (= t0 t0))) - (substitution - (c (g* (AConst) (AConst))) - (a (AConst)) - (b (AConst)) - (@rewrite_var__ t0))))) + (Rule + (= t0 (g* (AConst) (g* (AConst) (g* (AConst) (AConst))))) + (name "(birewrite (g* (g* a b) c) (g* a (g* b c)))=>") + (premises (Fiat (= t0 t0))) + (substitution + (c (g* (AConst) (AConst))) + (a (AConst)) + (b (AConst)) + (@rewrite_var__ t0)))) (substitution (@rewrite_var__4 t0)))) (Rule (= t1 (IConst))