Skip to content

Commit c74be19

Browse files
refactor(dfir_lang): remove stratum, add push codegen, test reduce, reduce_keyed, reduce_no_replay
PR: #2966 Handle outputs.is_empty() in reduce_keyed push path Restructured the write_iterator logic in reduce_keyed.rs to follow the standard pattern: `if is_pull { ... } else if outputs.is_empty() { ... } else { ... }`. Previously, the `!is_pull` branch unconditionally accessed `outputs[0]`, which would panic if reduce_keyed was used as a terminal push operator with no downstream outputs (e.g., as a singleton reference target). The new `outputs.is_empty()` branch generates a `for_each`-style sink that accumulates into the hashtable without forwarding downstream, matching the pattern used by reduce, fold, reduce_no_replay, and fold_no_replay. Co-authored-by: Infinity 🤖 <infinity@hydro.run> PR: #2966
1 parent 7321ac2 commit c74be19

26 files changed

Lines changed: 201 additions & 182 deletions

dfir_lang/src/graph/ops/reduce.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use quote::quote_spanned;
22

33
use super::{
4-
DelayType, OperatorCategory, OperatorConstraints, OperatorWriteOutput, Persistence, RANGE_0,
4+
OperatorCategory, OperatorConstraints, OperatorWriteOutput, Persistence, RANGE_0,
55
RANGE_1, WriteContextArgs,
66
};
77

@@ -44,14 +44,15 @@ pub const REDUCE: OperatorConstraints = OperatorConstraints {
4444
flo_type: None,
4545
ports_inn: None,
4646
ports_out: None,
47-
input_delaytype_fn: |_| Some(DelayType::Stratum),
47+
input_delaytype_fn: |_| None,
4848
write_fn: |wc @ &WriteContextArgs {
4949
root,
5050
op_span,
5151
work_fn,
5252
work_fn_async,
5353
ident,
5454
inputs,
55+
outputs,
5556
is_pull,
5657
arguments,
5758
..
@@ -117,15 +118,30 @@ pub const REDUCE: OperatorConstraints = OperatorConstraints {
117118
)
118119
);
119120
}
120-
} else {
121-
// Is only push when used as a singleton, so no need to push to `outputs[0]`.
121+
} else if outputs.is_empty() {
122+
// Terminal push: reduce is a singleton reference target with no downstream.
122123
quote_spanned! {op_span=>
123124
let #ident = #root::dfir_pipes::push::for_each(|#item_ident| {
124125
#assign_accum_ident
125126

126127
#foreach_body
127128
});
128129
}
130+
} else {
131+
let output = &outputs[0];
132+
quote_spanned! {op_span=>
133+
let #ident = #root::dfir_pipes::push::reduce_ref(
134+
&mut #singleton_output_ident,
135+
|#accumulator_ident: &mut _, #item_ident| {
136+
#[allow(clippy::redundant_closure_call)]
137+
(#func)(#accumulator_ident, #item_ident);
138+
},
139+
#root::dfir_pipes::push::map(
140+
|__val: &mut _| ::std::clone::Clone::clone(&*__val),
141+
#output,
142+
),
143+
);
144+
}
129145
};
130146

131147
Ok(OperatorWriteOutput {

dfir_lang/src/graph/ops/reduce_keyed.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use quote::{ToTokens, quote_spanned};
22

33
use super::{
4-
DelayType, OpInstGenerics, OperatorCategory, OperatorConstraints, OperatorInstance,
4+
OpInstGenerics, OperatorCategory, OperatorConstraints, OperatorInstance,
55
OperatorWriteOutput, Persistence, RANGE_1, WriteContextArgs,
66
};
77

@@ -68,15 +68,15 @@ pub const REDUCE_KEYED: OperatorConstraints = OperatorConstraints {
6868
flo_type: None,
6969
ports_inn: None,
7070
ports_out: None,
71-
input_delaytype_fn: |_| Some(DelayType::Stratum),
71+
input_delaytype_fn: |_| None,
7272
write_fn: |wc @ &WriteContextArgs {
7373
op_span,
7474
ident,
7575
inputs,
76+
outputs,
7677
is_pull,
7778
work_fn_async,
7879
root,
79-
op_name,
8080
op_inst:
8181
OperatorInstance {
8282
generics: OpInstGenerics { type_args, .. },
@@ -86,8 +86,6 @@ pub const REDUCE_KEYED: OperatorConstraints = OperatorConstraints {
8686
..
8787
},
8888
diagnostics| {
89-
assert!(is_pull, "TODO(mingwei): `{}` only supports pull.", op_name);
90-
9189
let [persistence] = wc.persistence_args_disallow_mutable(diagnostics);
9290

9391
let generic_type_args = [
@@ -118,7 +116,7 @@ pub const REDUCE_KEYED: OperatorConstraints = OperatorConstraints {
118116
_ => Default::default(),
119117
};
120118

121-
let write_iterator = {
119+
let write_iterator = if is_pull {
122120
let iter_expr = match persistence {
123121
Persistence::None | Persistence::Tick => quote_spanned! {op_span=>
124122
#hashtable_ident.drain()
@@ -184,6 +182,33 @@ pub const REDUCE_KEYED: OperatorConstraints = OperatorConstraints {
184182
let #ident = #iter_expr;
185183
let #ident = #root::dfir_pipes::pull::iter(#ident);
186184
}
185+
} else if outputs.is_empty() {
186+
// Terminal push: reduce_keyed is a singleton reference target with no downstream.
187+
quote_spanned! {op_span=>
188+
let #ident = #root::dfir_pipes::push::for_each(|kv: (#( #generic_type_args ),*)| {
189+
match #singleton_output_ident.entry(kv.0) {
190+
::std::collections::hash_map::Entry::Vacant(vacant) => {
191+
vacant.insert(kv.1);
192+
}
193+
::std::collections::hash_map::Entry::Occupied(mut occupied) => {
194+
#[inline(always)]
195+
fn call_comb_type<A>(acc: &mut A, item: A, f: impl Fn(&mut A, A)) {
196+
let () = (f)(acc, item);
197+
}
198+
call_comb_type(occupied.get_mut(), kv.1, #aggfn);
199+
}
200+
}
201+
});
202+
}
203+
} else {
204+
let output = &outputs[0];
205+
quote_spanned! {op_span=>
206+
let #ident = #root::dfir_pipes::push::ReduceKeyed::new(
207+
&mut #singleton_output_ident,
208+
#aggfn,
209+
#output,
210+
);
211+
}
187212
};
188213

189214
Ok(OperatorWriteOutput {

dfir_lang/src/graph/ops/reduce_no_replay.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use quote::quote_spanned;
22

33
use super::{
4-
DelayType, OperatorCategory, OperatorConstraints, OperatorWriteOutput, Persistence, RANGE_0,
5-
RANGE_1, WriteContextArgs,
4+
OperatorCategory, OperatorConstraints, OperatorWriteOutput, Persistence, RANGE_0, RANGE_1,
5+
WriteContextArgs,
66
};
77

88
/// > 1 input stream, 1 output stream
@@ -27,7 +27,7 @@ pub const REDUCE_NO_REPLAY: OperatorConstraints = OperatorConstraints {
2727
flo_type: None,
2828
ports_inn: None,
2929
ports_out: None,
30-
input_delaytype_fn: |_| Some(DelayType::Stratum),
30+
input_delaytype_fn: |_| None,
3131
write_fn: |wc @ &WriteContextArgs {
3232
root,
3333
context,
@@ -36,6 +36,7 @@ pub const REDUCE_NO_REPLAY: OperatorConstraints = OperatorConstraints {
3636
work_fn_async,
3737
ident,
3838
inputs,
39+
outputs,
3940
is_pull,
4041
arguments,
4142
..
@@ -108,15 +109,40 @@ pub const REDUCE_NO_REPLAY: OperatorConstraints = OperatorConstraints {
108109
)
109110
};
110111
}
111-
} else {
112-
// Is only push when used as a singleton, so no need to push to `outputs[0]`.
112+
} else if outputs.is_empty() {
113+
// Terminal push: reduce_no_replay is a singleton reference target with no downstream.
113114
quote_spanned! {op_span=>
114115
let #ident = #root::dfir_pipes::push::for_each(|#item_ident| {
115116
#assign_accum_ident
116117

117118
#foreach_body
118119
});
119120
}
121+
} else {
122+
let output = &outputs[0];
123+
let was_updated_ident = wc.make_ident("was_updated");
124+
quote_spanned! {op_span=>
125+
let #was_updated_ident = ::std::cell::Cell::new(false);
126+
let #ident = #root::dfir_pipes::push::reduce_ref(
127+
&mut #singleton_output_ident,
128+
|#accumulator_ident: &mut _, #item_ident| {
129+
#was_updated_ident.set(true);
130+
#[allow(clippy::redundant_closure_call)]
131+
(#func)(#accumulator_ident, #item_ident);
132+
},
133+
#root::dfir_pipes::push::filter(
134+
{
135+
let __was_updated = &#was_updated_ident;
136+
let __context: &_ = #context;
137+
move |_| __was_updated.get() || __context.current_tick().0 == 0
138+
},
139+
#root::dfir_pipes::push::map(
140+
|__val: &mut _| ::std::clone::Clone::clone(&*__val),
141+
#output,
142+
),
143+
),
144+
);
145+
}
120146
};
121147

122148
Ok(OperatorWriteOutput {

dfir_rs/tests/compile-fail/stable/surface_fold_keyed_badtype_int.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error[E0271]: type mismatch resolving `<Iter<Drain<'_, '_, {integer}>> as Pull>::Item == (_, _)`
1+
error[E0271]: type mismatch resolving `<impl Pull<Item = {integer}, Meta = (), CanPend = <Iter<&mut impl Iterator<Item = {integer}>> as Pull>::CanPend, CanEnd = <Iter<&mut impl Iterator<Item = {integer}>> as Pull>::CanEnd> as Pull>::Item == (_, _)`
22
--> tests/compile-fail/stable/surface_fold_keyed_badtype_int.rs:3:9
33
|
44
3 | source_iter(0..1)
@@ -14,7 +14,7 @@ note: required by a bound in `check_input`
1414
4 | -> reduce_keyed(|old: &mut u32, val: u32| { *old += val; })
1515
| ^^^^^^^^^^^^ required by this bound in `check_input`
1616

17-
error[E0271]: type mismatch resolving `<Iter<Drain<'_, '_, {integer}>> as Pull>::Item == (_, _)`
17+
error[E0271]: type mismatch resolving `<impl Pull<Item = {integer}, Meta = (), CanPend = <Iter<&mut impl Iterator<Item = {integer}>> as Pull>::CanPend, CanEnd = <Iter<&mut impl Iterator<Item = {integer}>> as Pull>::CanEnd> as Pull>::Item == (_, _)`
1818
--> tests/compile-fail/stable/surface_fold_keyed_badtype_int.rs:4:16
1919
|
2020
4 | -> reduce_keyed(|old: &mut u32, val: u32| { *old += val; })

dfir_rs/tests/compile-fail/stable/surface_fold_keyed_badtype_option.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error[E0271]: type mismatch resolving `<Iter<Drain<'_, '_, Option<{integer}>>> as Pull>::Item == (_, _)`
1+
error[E0271]: type mismatch resolving `<impl Pull<Item = Option<{integer}>, Meta = (), CanPend = <Iter<&mut impl Iterator<Item = Option<{integer}>>> as Pull>::CanPend, CanEnd = <Iter<&mut impl Iterator<Item = Option<{integer}>>> as Pull>::CanEnd> as Pull>::Item == (_, _)`
22
--> tests/compile-fail/stable/surface_fold_keyed_badtype_option.rs:3:9
33
|
44
3 | source_iter([ Some(5), None, Some(12) ])
@@ -14,7 +14,7 @@ note: required by a bound in `check_input`
1414
4 | -> reduce_keyed(|old: &mut u32, val: u32| { *old += val; })
1515
| ^^^^^^^^^^^^ required by this bound in `check_input`
1616

17-
error[E0271]: type mismatch resolving `<Iter<Drain<'_, '_, Option<{integer}>>> as Pull>::Item == (_, _)`
17+
error[E0271]: type mismatch resolving `<impl Pull<Item = Option<{integer}>, Meta = (), CanPend = <Iter<&mut impl Iterator<Item = Option<{integer}>>> as Pull>::CanPend, CanEnd = <Iter<&mut impl Iterator<Item = Option<{integer}>>> as Pull>::CanEnd> as Pull>::Item == (_, _)`
1818
--> tests/compile-fail/stable/surface_fold_keyed_badtype_option.rs:4:16
1919
|
2020
4 | -> reduce_keyed(|old: &mut u32, val: u32| { *old += val; })

dfir_rs/tests/snapshots/surface_reduce__reduce_static@graphvis_dot.snap

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,14 @@ digraph {
88
n1v1 [label="(n1v1) source_stream(items_recv)", shape=invhouse, fillcolor="#88aaff"]
99
n2v1 [label="(n2v1) reduce::<'static>(|acc: &mut u32, next: u32| *acc += next)", shape=invhouse, fillcolor="#88aaff"]
1010
n3v1 [label="(n3v1) for_each(|v| result_send.send(v).unwrap())", shape=house, fillcolor="#ffff88"]
11-
n4v1 [label="(n4v1) handoff", shape=parallelogram, fillcolor="#ddddff"]
1211
n2v1 -> n3v1
13-
n1v1 -> n4v1
14-
n4v1 -> n2v1 [color=red]
12+
n1v1 -> n2v1
1513
subgraph sg_1v1 {
1614
cluster=true
1715
fillcolor="#dddddd"
1816
style=filled
1917
label = "sg_1v1"
2018
n1v1
21-
}
22-
subgraph sg_2v1 {
23-
cluster=true
24-
fillcolor="#dddddd"
25-
style=filled
26-
label = "sg_2v1"
2719
n2v1
2820
n3v1
2921
}

dfir_rs/tests/snapshots/surface_reduce__reduce_static@graphvis_mermaid.snap

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,10 @@ linkStyle default stroke:#aaa
1111
1v1[\"(1v1) <code>source_stream(items_recv)</code>"/]:::pullClass
1212
2v1[\"(2v1) <code>reduce::&lt;'static&gt;(|acc: &amp;mut u32, next: u32| *acc += next)</code>"/]:::pullClass
1313
3v1[/"(3v1) <code>for_each(|v| result_send.send(v).unwrap())</code>"\]:::pushClass
14-
4v1["(4v1) <code>handoff</code>"]:::otherClass
1514
2v1-->3v1
16-
1v1-->4v1
17-
4v1--x2v1; linkStyle 2 stroke:red
15+
1v1-->2v1
1816
subgraph sg_1v1 ["sg_1v1"]
1917
1v1
20-
end
21-
subgraph sg_2v1 ["sg_2v1"]
2218
2v1
2319
3v1
2420
end

dfir_rs/tests/snapshots/surface_reduce__reduce_sum@graphvis_dot.snap

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,14 @@ digraph {
88
n1v1 [label="(n1v1) source_stream(items_recv)", shape=invhouse, fillcolor="#88aaff"]
99
n2v1 [label="(n2v1) reduce(|a: &mut _, b| *a += b)", shape=invhouse, fillcolor="#88aaff"]
1010
n3v1 [label="(n3v1) for_each(|v| print!(\"{:?}\", v))", shape=house, fillcolor="#ffff88"]
11-
n4v1 [label="(n4v1) handoff", shape=parallelogram, fillcolor="#ddddff"]
1211
n2v1 -> n3v1
13-
n1v1 -> n4v1
14-
n4v1 -> n2v1 [color=red]
12+
n1v1 -> n2v1
1513
subgraph sg_1v1 {
1614
cluster=true
1715
fillcolor="#dddddd"
1816
style=filled
1917
label = "sg_1v1"
2018
n1v1
21-
}
22-
subgraph sg_2v1 {
23-
cluster=true
24-
fillcolor="#dddddd"
25-
style=filled
26-
label = "sg_2v1"
2719
n2v1
2820
n3v1
2921
}

dfir_rs/tests/snapshots/surface_reduce__reduce_sum@graphvis_mermaid.snap

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,10 @@ linkStyle default stroke:#aaa
1111
1v1[\"(1v1) <code>source_stream(items_recv)</code>"/]:::pullClass
1212
2v1[\"(2v1) <code>reduce(|a: &amp;mut _, b| *a += b)</code>"/]:::pullClass
1313
3v1[/"(3v1) <code>for_each(|v| print!(&quot;{:?}&quot;, v))</code>"\]:::pushClass
14-
4v1["(4v1) <code>handoff</code>"]:::otherClass
1514
2v1-->3v1
16-
1v1-->4v1
17-
4v1--x2v1; linkStyle 2 stroke:red
15+
1v1-->2v1
1816
subgraph sg_1v1 ["sg_1v1"]
1917
1v1
20-
end
21-
subgraph sg_2v1 ["sg_2v1"]
2218
2v1
2319
3v1
2420
end

dfir_rs/tests/snapshots/surface_reduce__reduce_tick@graphvis_dot.snap

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,14 @@ digraph {
88
n1v1 [label="(n1v1) source_stream(items_recv)", shape=invhouse, fillcolor="#88aaff"]
99
n2v1 [label="(n2v1) reduce::<'tick>(|acc: &mut u32, next: u32| *acc += next)", shape=invhouse, fillcolor="#88aaff"]
1010
n3v1 [label="(n3v1) for_each(|v| result_send.send(v).unwrap())", shape=house, fillcolor="#ffff88"]
11-
n4v1 [label="(n4v1) handoff", shape=parallelogram, fillcolor="#ddddff"]
1211
n2v1 -> n3v1
13-
n1v1 -> n4v1
14-
n4v1 -> n2v1 [color=red]
12+
n1v1 -> n2v1
1513
subgraph sg_1v1 {
1614
cluster=true
1715
fillcolor="#dddddd"
1816
style=filled
1917
label = "sg_1v1"
2018
n1v1
21-
}
22-
subgraph sg_2v1 {
23-
cluster=true
24-
fillcolor="#dddddd"
25-
style=filled
26-
label = "sg_2v1"
2719
n2v1
2820
n3v1
2921
}

0 commit comments

Comments
 (0)