[DBSP] Implement RecursiveStreams on Vec<Stream<_, _>>#6577
Conversation
mythical-fred
left a comment
There was a problem hiding this comment.
Nice, clean addition — the Vec<Stream> impl mirrors the tuple impl cleanly and the two new tests give solid coverage of the arity-1 and arity-2 cases against their tuple counterparts.
A few things to consider, all soft: (1) the arity checks in the Vec impl are debug_assert_eq!, and the rustdoc even flags that a release build silently constructs a wrong circuit — I'd make those unconditional. (2) the pre-PR reachability2 test used FallbackZSet for the recursive streams; after consolidation the whole file only exercises OrdZSet. (3) small prose/naming/commit-message nits inline.
No blockers. Approving pending your take on the arity assertions.
| } | ||
|
|
||
| fn connect(&self, vars: Self::Feedback) { | ||
| debug_assert_eq!(self.len(), vars.len()); |
There was a problem hiding this comment.
The three debug_assert_eq! arity checks in this impl (here, line 131, line 153, line 164) are the only guard against a user closure returning a Vec of the wrong length. Your own rustdoc on recursive_variadic calls it out: "panics in debug builds and produces an incorrect circuit otherwise." Silently building a wrong circuit in release is worse than a panic. These checks fire once per circuit build (not per fixpoint iteration), so making them unconditional assert_eq! has effectively zero cost — and it turns a silent correctness bug into a loud one. Please promote all four.
| // Somewhat lame multiple recursion example to test RecursiveStreams impl for | ||
| // tuples: compute forward and backward reachability at the same time. | ||
| #[test] | ||
| fn reachability2() { |
There was a problem hiding this comment.
Coverage nit: the pre-PR reachability2 typed the two recursive streams as Stream<_, FallbackZSet<Tup2<u64, u64>>> and the use at the top of the module still pulled in FallbackZSet. After the refactor the whole file only exercises OrdZSet, so we've quietly dropped the FallbackZSet code path in the recursion tests. Either flip one of the four reachability* tests back to FallbackZSet<Edge> or add a short comment noting the backing store is covered elsewhere — whichever you prefer.
There was a problem hiding this comment.
I've restored reachability2 to use the FallbackZSet instead of OrdZSet. That wasn't on purpose, I do not really know the difference between the two.
| /// The closure `f` receives a vector of `arity` recursive input streams and | ||
| /// must return a vector of exactly `arity` output streams, one per recursive | ||
| /// relation. Returning a vector of a different length panics in debug | ||
| /// builds and produces an incorrect circuit otherwise. |
There was a problem hiding this comment.
"panics in debug builds and produces an incorrect circuit otherwise" is a self-diagnosed footgun — see the inline comment on the trait impl. If you promote those debug_asserts to asserts, this sentence should read "panics if the returned vector has a different length."
| /// // ------ |5| <----- | ||
| /// vec![Tup2(Tup2(2, 5), 1), Tup2(Tup2(5, 0), 1)], | ||
| /// // And we introduce an odd-length cycle, rendering the graph | ||
| /// // non-biparite anymore (all nodes are red _and_ blue): |
There was a problem hiding this comment.
Typo: non-biparite → non-bipartite.
There was a problem hiding this comment.
you have thumbed-up this, but you haven't fixed it
Let me know when this is ready for a review
| /// Ok::<(), DbspError>(()) | ||
| /// ``` | ||
| #[track_caller] | ||
| pub fn recursive_variadic<F, K, V, B>( |
There was a problem hiding this comment.
Naming, minor: recursive_variadic reads to me more like "variadic arguments" (heterogeneous, arity known at compile time — i.e. the tuple impl) than "runtime-known homogeneous arity." recursive_dynamic, recursive_vec, or recursive_n would all be clearer to a reader who hasn't read this docstring. Not a blocker — flag for consideration.
There was a problem hiding this comment.
I've settled on recursive_dynamic now.
mihaibudiu
left a comment
There was a problem hiding this comment.
This looks fine to me, but @ryzhyk is the definitive authority.
Thank you!
| Ok((edges.plus(&paths_indexed.join(&edges_indexed, |_via, from, to| Tup2(*from, *to))), | ||
| reverse_edges.plus(&reverse_paths_indexed.join(&reverse_edges_indexed, |_via, from, to| Tup2(*from, *to))) | ||
| )) | ||
| zset! { Tup2(1, 3) => 1, Tup2(2, 3) => 1, Tup2(2, 4) => 1, |
There was a problem hiding this comment.
I assume these are not changed; reformatting makes it hard to tell.
There was a problem hiding this comment.
Their input output behavior is exactly the same as before. I've just restructured them a bit and accidentally used OrdZSet instead of FallbackZSet as pointed out by the AI above but this is now reverted to still use FallbackZSet in reachability2. What is the difference between the two?
There was a problem hiding this comment.
The documentation for FallbackZSet explains that it is a wrapper which allows the ZSet to reside either in-memory or on-disk.
| Ok(()) | ||
| }) | ||
| .unwrap().0; | ||
| /// The `Vec` counterpart of [`reachability()`]: a single recursive relation |
There was a problem hiding this comment.
A rewrite of reachability() using recursive_variadic
| .map(|streams| unsafe { S::typed_exports(&streams) }) | ||
| } | ||
|
|
||
| /// Like [`ChildCircuit::recursive`], but for a group of mutually recursive |
There was a problem hiding this comment.
So you have a use case in which you generate programs dynamically in Rust?
There was a problem hiding this comment.
Exactly! I'm building an interpreter on top of DBSP.
| /// | ||
| /// The closure `f` receives a vector of `arity` recursive input streams and | ||
| /// must return a vector of exactly `arity` output streams, one per recursive | ||
| /// relation. Returning a vector of a different length panics in debug |
There was a problem hiding this comment.
Why not always panic, as Fred asked?
|
Our bot wants you to change this: |
mythical-fred
left a comment
There was a problem hiding this comment.
Nits addressed — assert_eq! in the Vec impl, recursive_dynamic naming, reachability2 restored on FallbackZSet, typo fixed. LGTM.
| /// vec![Tup2(Tup2(2, 5), 1), Tup2(Tup2(5, 0), 1)], | ||
| /// // And we introduce an odd-length cycle, rendering the graph | ||
| /// // non-biparite anymore (all nodes are red _and_ blue): | ||
| /// // non-bipartite anymore (all nodes are red _and_ blue): |
There was a problem hiding this comment.
@mihaibudiu It is fixed, I think. From my end, I'm ready to get this merged or for a final review!
|
I think something is broken in the new github UI, the comments look different depending on where you look at them. |
|
I think this is safe to merge, even without @ryzhyk's review. |
This implements recursion for a variadic number of recursive streams which has been impossible with the existing API and happened to be a marked as TODO in the codebase already. Previously you were required to know the number of recursive streams at compile time (with the benefit of supporting heterogeneous stream types). With this change the number of recursive streams can be specified at runtime (via
recursive_variadic(); with the downside of requiring the stream types to be homogeneous).API Changes
I've implemented this as an additive change to the outward facing API such that no existing code is broken with this change. Users who want a variadic amount of recursive streams at runtime simply use the new
recursive_variadicfunction.Testing and Documentation
The change is covered by both extensive documentation and unit testing. The diff of the unit test is a bit bigger than what it actually is because I restructured it such that the input and output data is shared among both variants (static and variadic) of the two test cases
reachableandreachable2, respectively.Moreover, this PR also fixes doc tests for existing code with
rustc >= 1.96.0.Hope this matches what you've had in mind for the (missing) implementation, too.