From 04d7d182457b23950a000ab73bc49aab66907d41 Mon Sep 17 00:00:00 2001 From: Frank McSherry Date: Tue, 10 Nov 2026 08:30:09 -0500 Subject: [PATCH] DDIR: a projected reducer keeps its spelling's graph, so equal spellings merge `Reducer::project` replaced a projected reducer's source with a readable spelling, once that spelling compiled to the same graph at its input type, but kept the composed graph. That graph still holds the original reducer's work, and lambdas compare by graph, so two reducers projected to the same spelling stayed apart in the plan and merged only when the printed plan was planned again. A sum and a collect, both masked to `()`, printed as `reduce(_ -> ((), 1) enlist)` over one arrangement and stayed two reduces. The projected reducer now keeps the spelling's parsed graph, as `canonical.rs` already does for composed maps. The test that asserted the composed graph now asserts the spelling's graph, and the same compiled graph as the composition. The minimized case joins the round-trip repros, checked under every pass subset. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01H6wB4H99TLw4tqu4x48pXL --- interactive/src/reducers.rs | 22 +++++++++++++--------- interactive/tests/pass_roundtrip.rs | 8 +++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/interactive/src/reducers.rs b/interactive/src/reducers.rs index f33f27921..bfa65a4c7 100644 --- a/interactive/src/reducers.rs +++ b/interactive/src/reducers.rs @@ -133,9 +133,10 @@ impl Reducer { let mut composed = Lambda::compose("arg", &[(&base, "arg".into())], |names| { format!("{} map ((_value, _diff) -> ({}, _diff))", names[0], projection.text("_value")) }); - // Source is only a spelling. Keep the composed graph, and accept the graph - // printer's spelling only when reparsing gives the same graph compiled at - // this input type, which proves and removes the patterns in either spelling. + // Accept the graph printer's spelling only when reparsing gives the same graph + // compiled at this input type, which proves and removes the patterns in either + // spelling. Then keep the spelling's graph, as reparsing it would build: lambdas + // equal as printed stay equal in the plan, so structural merging sees them. let input = corgi::Shape::Prod(vec![key.clone(), corgi::Shape::List(Box::new(corgi::Shape::Prod(vec![val.clone(), corgi::Shape::Int])))]); let spelling = (|| { let printed = crate::lambda::print::print_composed(&composed, &input); @@ -146,11 +147,14 @@ impl Reducer { for f in crate::lambda::registered() { words.word(f.name(), crate::lambda::function_word(&f)).ok()?; } let parsed = corgi::parse_ml_in(&src, &words).ok()?; let graph = composed.graph().ok()?; - if corgi::compile(parsed, &input).ok()?.graph() + if corgi::compile(parsed.clone(), &input).ok()?.graph() != corgi::compile(graph.as_ref().clone(), &input).ok()?.graph() { return None; } - Some(src) + Some((src, parsed)) })(); - if let Some(src) = spelling { composed.src = src; } + if let Some((src, parsed)) = spelling { + composed.src = src; + composed.graph = Ok(std::sync::Arc::new(parsed)); + } Self::Expr(composed) } } @@ -162,7 +166,7 @@ mod tests { use corgi::{Bounds, Shape, Value}; #[test] - fn composed_spelling_preserves_the_exact_graph() { + fn composed_spelling_is_its_own_graph() { let base = Sugar::Min.lambda(); let projection = Projection::Tuple(vec![Projection::Unit, Projection::Field(vec![1])]); let mechanical = Lambda::compose("arg", &[(&base, "arg".into())], |names| { @@ -171,11 +175,11 @@ mod tests { let Reducer::Expr(readable) = Reducer::Expr(base).project(&projection, &Shape::Int, &Shape::Prod(vec![Shape::Int, Shape::Int])); assert!(!readable.src.contains("_call0"), "{}", readable.src); assert!(readable.src.contains("head")); - assert_eq!(readable.graph().unwrap(), mechanical.graph().unwrap()); let parsed = Lambda::new(readable.src.clone(), &corgi::Scope::default()); + assert_eq!(readable.graph().unwrap(), parsed.graph().unwrap()); let val = Shape::Prod(vec![Shape::Int, Shape::Int]); assert_eq!(crate::lambda::compile_reduce(&readable, &Shape::Int, &val).unwrap().graph(), - crate::lambda::compile_reduce(&parsed, &Shape::Int, &val).unwrap().graph()); + crate::lambda::compile_reduce(&mechanical, &Shape::Int, &val).unwrap().graph()); // The known input type also proves authored nested pattern checks. let base = Lambda::new("let ((k, _), vals) = input in (vals head.0, 1) enlist".into(), &corgi::Scope::default()); let Reducer::Expr(kept) = Reducer::Expr(base).project(&projection, &Shape::Prod(vec![Shape::Int, Shape::Int]), &Shape::Prod(vec![Shape::Int, Shape::Int])); diff --git a/interactive/tests/pass_roundtrip.rs b/interactive/tests/pass_roundtrip.rs index c54fe9f9c..4aeb15be8 100644 --- a/interactive/tests/pass_roundtrip.rs +++ b/interactive/tests/pass_roundtrip.rs @@ -3,7 +3,7 @@ use interactive::{explain, lower, parse, plan}; use interactive::passes::Config; -const REPROS: [(&str, &str); 7] = [ +const REPROS: [(&str, &str); 8] = [ ("F1", r#"let rows = input 0 : (int, ()); s: { set x = rows; } @@ -52,6 +52,12 @@ const REPROS: [(&str, &str); 7] = [ let reduced = both | min; let dead = reduced + reduced; output "out" = rows;"#), + ("reductions masked to the same spelling merge before printing", + r#"let rows = input 0 : ((int, int), ()); + let sum = rows | reduce((_, vals) -> ((0, vals) fold ((acc, (_, d)) -> (acc, d) add), 1) enlist) | map((k, _) -> (k, ())); + let all = rows | collect | map((k, _) -> (k, ())); + output "a" = sum; + output "b" = all;"#), ]; fn roundtrip(first: plan::Report, config: Config, context: &str) {