1use std::collections::BTreeMap;
47
48use crate::core::{Const, Core, CoreKind, VarId};
49use crate::plan::{Fun, Op, OpId, Plan};
50use crate::ty::{Tier, Ty};
51
52pub const RULES: &[&str] = &[
58 "map_list over map_list",
59 "filter_list over filter_list",
60 "flatten over map_list",
61 "a count over a cardinality-preserving operator",
62 "concat_lists of one list",
63];
64
65#[derive(Clone, Debug)]
67pub struct Fusion {
68 pub rule: &'static str,
70 pub at: OpId,
72 pub became: &'static str,
74 pub why: &'static str,
76}
77
78#[derive(Clone, Debug)]
83pub struct Refusal {
84 pub rule: &'static str,
85 pub at: OpId,
87 pub kept: OpId,
89 pub why: String,
90}
91
92#[derive(Clone, Debug, Default)]
94pub struct Fusions {
95 pub fired: Vec<Fusion>,
96 pub refused: Vec<Refusal>,
97 pub operators: (usize, usize),
100 pub arrangements: (usize, usize),
101}
102
103pub fn fuse(mut plan: Plan) -> (Plan, Fusions) {
105 let mut rec = Fusions {
106 operators: (plan.nodes.len(), 0),
107 arrangements: (arrangements(&plan), 0),
108 ..Fusions::default()
109 };
110 let mut fired: Vec<(OpId, Fusion)> = Vec::new();
113 let mut refused: Vec<(OpId, OpId, Refusal)> = Vec::new();
114
115 for _ in 0..plan.nodes.len() + 1 {
118 let Some((absorbed, survivor)) = round(&mut plan, &mut fired, &mut refused) else {
119 break;
120 };
121 refused.retain(|(at, kept, _)| !(*at == survivor && *kept == absorbed));
126 for (at, kept, _) in refused.iter_mut() {
127 if *at == absorbed {
128 *at = survivor;
129 }
130 if *kept == absorbed {
131 *kept = survivor;
132 }
133 }
134 let map = plan.prune();
135 remap(&mut fired, &mut refused, &map);
136 }
137
138 rec.operators.1 = plan.nodes.len();
139 rec.arrangements.1 = arrangements(&plan);
140 rec.fired = fired
141 .into_iter()
142 .map(|(at, f)| Fusion { at, ..f })
143 .collect();
144 rec.refused = refused
145 .into_iter()
146 .map(|(at, kept, r)| Refusal { at, kept, ..r })
147 .collect();
148 rec.refused.sort_by_key(|r| (r.at, r.kept));
149 rec.refused.dedup_by_key(|r| (r.at, r.kept, r.rule));
150 (plan, rec)
151}
152
153fn arrangements(plan: &Plan) -> usize {
154 plan.nodes.iter().filter(|n| n.op.is_arrangement()).count()
155}
156
157fn round(
162 plan: &mut Plan,
163 fired: &mut Vec<(OpId, Fusion)>,
164 refused: &mut Vec<(OpId, OpId, Refusal)>,
165) -> Option<(OpId, OpId)> {
166 for i in 0..plan.nodes.len() {
167 if matches!(plan.nodes[i].op, Op::Concat) && plan.nodes[i].inputs.len() == 1 {
172 let input = plan.nodes[i].inputs[0];
173 if plan.nodes[input].op.is_arrangement() && i != plan.state && i != plan.session {
174 substitute(plan, i, input);
175 fired.push((
176 input,
177 Fusion {
178 rule: "concat_lists of one list",
179 at: input,
180 became: plan.nodes[input].op.name(),
181 why: "a union of one delta stream is that delta stream, and every entry \
182 gained the same key prefix",
183 },
184 ));
185 return Some((i, input));
186 }
187 }
188
189 let Some(&p) = plan.nodes[i].inputs.first() else {
190 continue;
191 };
192 let Some(rule) = matching(&plan.nodes[i].op, &plan.nodes[p].op) else {
193 continue;
194 };
195 if let Some(why) = refuses(plan, i, p, rule) {
196 refused.push((
197 i,
198 p,
199 Refusal {
200 rule: rule.name,
201 at: i,
202 kept: p,
203 why,
204 },
205 ));
206 continue;
207 }
208 (rule.apply)(plan, i, p);
209 fired.push((
210 i,
211 Fusion {
212 rule: rule.name,
213 at: i,
214 became: plan.nodes[i].op.name(),
215 why: rule.why,
216 },
217 ));
218 return Some((p, i));
219 }
220 None
221}
222
223struct Rule {
225 name: &'static str,
226 why: &'static str,
227 carries_work: bool,
230 apply: fn(&mut Plan, OpId, OpId),
231}
232
233fn matching(consumer: &Op, producer: &Op) -> Option<&'static Rule> {
234 match (consumer, producer) {
235 (Op::MapList { .. }, Op::MapList { .. }) => Some(&MAP_OVER_MAP),
236 (Op::FilterList { .. }, Op::FilterList { .. }) => Some(&FILTER_OVER_FILTER),
237 (Op::Flatten, Op::MapList { .. }) => Some(&FLATTEN_OVER_MAP),
238 (Op::Count | Op::IsEmpty, Op::MapList { .. } | Op::SortBy { .. }) => Some(&COUNT_OVER),
239 _ => None,
240 }
241}
242
243static MAP_OVER_MAP: Rule = Rule {
244 name: "map_list over map_list",
245 why: "neither moves an element, so both arrangements have the input's key and the composition \
246 has it too",
247 carries_work: true,
248 apply: |plan, i, p| {
249 let inner = fun_of(&plan.nodes[p].op)
250 .expect("the rule matched a map_list")
251 .clone();
252 let outer = fun_of(&plan.nodes[i].op)
253 .expect("the rule matched a map_list")
254 .clone();
255 plan.nodes[i].op = Op::MapList {
256 f: compose(&outer, &inner),
257 };
258 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
259 },
260};
261
262static FILTER_OVER_FILTER: Rule = Rule {
263 name: "filter_list over filter_list",
264 why: "a conjunction, and it short-circuits — the outer predicate is applied to exactly the \
265 elements the inner one kept, which is what the pair did",
266 carries_work: true,
267 apply: |plan, i, p| {
268 let inner = fun_of(&plan.nodes[p].op)
269 .expect("the rule matched a filter_list")
270 .clone();
271 let outer = fun_of(&plan.nodes[i].op)
272 .expect("the rule matched a filter_list")
273 .clone();
274 plan.nodes[i].op = Op::FilterList {
275 f: conjoin(&outer, &inner),
276 };
277 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
278 },
279};
280
281static FLATTEN_OVER_MAP: Rule = Rule {
282 name: "flatten over map_list",
283 why: "the map's key is the input's and the flatten's is the map's followed by a position, so \
284 one operator keyed by the input's key and a position is the same order",
285 carries_work: true,
286 apply: |plan, i, p| {
287 let f = fun_of(&plan.nodes[p].op)
288 .expect("the rule matched a map_list")
289 .clone();
290 plan.nodes[i].op = Op::FlatMap { f };
291 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
292 },
293};
294
295static COUNT_OVER: Rule = Rule {
296 name: "a count over a cardinality-preserving operator",
297 why: "`map_list` and `sort_by` produce one entry per entry, so how many there are is a \
298 question about the input and the arrangement between them is never read",
299 carries_work: false,
302 apply: |plan, i, p| {
303 plan.nodes[i].inputs = vec![plan.nodes[p].inputs[0]];
304 },
305};
306
307fn refuses(plan: &Plan, i: OpId, p: OpId, rule: &Rule) -> Option<String> {
309 if p == plan.state || p == plan.session || p == plan.root {
310 return Some("it is the plan's root or one of its sources".to_string());
311 }
312 if plan.nodes[p].consumers > 1 {
313 return Some(format!(
314 "#{p} is read by {} operators, and fusing it into one of them would compute it {} \
315 times (docs/26)",
316 plan.nodes[p].consumers, plan.nodes[p].consumers
317 ));
318 }
319 let names = plan.names_of(p);
320 if !names.is_empty() {
321 return Some(format!(
322 "`{}` is a declared signal, so the read model projects it as a table (docs/88)",
323 names.join("`, `")
324 ));
325 }
326 if rule.carries_work && !plan.nodes[p].per_session && plan.nodes[i].per_session {
327 return Some(format!(
328 "#{p} is shared and #{i} is per session, so fusing would move work the process does \
329 once per event to work it does once per event per subscriber (docs/26 §5.3)"
330 ));
331 }
332 None
333}
334
335fn fun_of(op: &Op) -> Option<&Fun> {
336 match op {
337 Op::MapList { f } | Op::FilterList { f } | Op::SortBy { f } | Op::FlatMap { f } => Some(f),
338 _ => None,
339 }
340}
341
342fn compose(outer: &Fun, inner: &Fun) -> Fun {
353 let (params, inner_args, outer_caps, x) = frame(outer, inner);
354 let applied = apply(&inner.code, inner_args);
355 let mut outer_args: Vec<Core> = outer_caps;
356 outer_args.push(applied);
357 let _ = x;
358 Fun {
359 code: lam(params, apply(&outer.code, outer_args)),
360 captures: inner
361 .captures
362 .iter()
363 .chain(outer.captures.iter())
364 .copied()
365 .collect(),
366 }
367}
368
369fn conjoin(outer: &Fun, inner: &Fun) -> Fun {
376 let (params, inner_args, outer_caps, x) = frame(outer, inner);
377 let mut outer_args: Vec<Core> = outer_caps;
378 outer_args.push(var(x));
379 Fun {
380 code: lam(
381 params,
382 Core {
383 kind: CoreKind::If {
384 cond: Box::new(apply(&inner.code, inner_args)),
385 then: Box::new(apply(&outer.code, outer_args)),
386 alt: Box::new(Core {
387 kind: CoreKind::Const(Const::Bool(false)),
388 ty: Ty::bool_(),
389 tier: Tier::Any,
390 span: beck_diag::Span::NONE,
391 last_use: false,
392 order: crate::fields::UNORDERED,
393 locals: 0,
394 }),
395 },
396 ty: Ty::bool_(),
397 tier: Tier::Any,
398 span: beck_diag::Span::NONE,
399 last_use: false,
400 order: crate::fields::UNORDERED,
401 locals: 0,
402 },
403 ),
404 captures: inner
405 .captures
406 .iter()
407 .chain(outer.captures.iter())
408 .copied()
409 .collect(),
410 }
411}
412
413fn frame(outer: &Fun, inner: &Fun) -> (Vec<VarId>, Vec<Core>, Vec<Core>, VarId) {
416 let n = inner.captures.len() + outer.captures.len() + 1;
417 let base = 1 + max_var(&inner.code).max(max_var(&outer.code));
421 let params: Vec<VarId> = (0..n as VarId).map(|k| base + k).collect();
422 let inner_args: Vec<Core> = params[..inner.captures.len()]
423 .iter()
424 .copied()
425 .chain(std::iter::once(params[n - 1]))
426 .map(var)
427 .collect();
428 let outer_caps: Vec<Core> = params[inner.captures.len()..n - 1]
429 .iter()
430 .copied()
431 .map(var)
432 .collect();
433 let x = params[n - 1];
434 (params, inner_args, outer_caps, x)
435}
436
437fn max_var(c: &Core) -> VarId {
438 let mut top = 0;
439 walk(c, &mut |x| {
440 if let CoreKind::Var(v) = &x.kind {
441 top = top.max(*v);
442 }
443 if let CoreKind::Lam { params, .. } = &x.kind {
444 for p in params.iter() {
445 top = top.max(*p);
446 }
447 }
448 if let CoreKind::Let { var, .. } = &x.kind {
449 top = top.max(*var);
450 }
451 });
452 top
453}
454
455fn walk(c: &Core, f: &mut impl FnMut(&Core)) {
456 f(c);
457 match &c.kind {
458 CoreKind::Var(_) | CoreKind::Const(_) | CoreKind::Global(_) => {}
459 CoreKind::Lam { body, .. } => walk(body, f),
460 CoreKind::App { func, args } => {
461 walk(func, f);
462 args.iter().for_each(|a| walk(a, f));
463 }
464 CoreKind::Prim { args, .. } => args.iter().for_each(|a| walk(a, f)),
465 CoreKind::Let { value, body, .. } => {
466 walk(value, f);
467 walk(body, f);
468 }
469 CoreKind::If { cond, then, alt } => {
470 walk(cond, f);
471 walk(then, f);
472 walk(alt, f);
473 }
474 CoreKind::Match { scrutinee, arms } => {
475 walk(scrutinee, f);
476 arms.iter().flat_map(|a| a.exprs()).for_each(|e| walk(e, f));
477 }
478 CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, v)| walk(v, f)),
479 CoreKind::Field { base, .. } => walk(base, f),
480 CoreKind::With { base, fields } => {
481 walk(base, f);
482 fields.iter().for_each(|(_, v)| walk(v, f));
483 }
484 CoreKind::ListLit(items) => items.iter().for_each(|i| walk(i, f)),
485 CoreKind::MapLit(pairs) => pairs.iter().for_each(|(k, v)| {
486 walk(k, f);
487 walk(v, f);
488 }),
489 }
490}
491
492fn apply(f: &Core, args: Vec<Core>) -> Core {
493 let ty = match &f.ty {
494 Ty::Fun(_, ret, _) => (**ret).clone(),
495 _ => Ty::unit(),
496 };
497 Core {
498 kind: CoreKind::App {
499 func: Box::new(f.clone()),
500 args,
501 },
502 ty,
503 tier: Tier::Any,
504 span: f.span,
505 last_use: false,
506 order: crate::fields::UNORDERED,
507 locals: 0,
508 }
509}
510
511fn lam(params: Vec<VarId>, body: Core) -> Core {
512 Core {
513 ty: Ty::fun(params.iter().map(|_| Ty::unit()).collect(), body.ty.clone()),
514 tier: body.tier,
515 span: body.span,
516 kind: CoreKind::Lam {
517 params: params.into(),
518 body: std::sync::Arc::new(body),
519 },
520 last_use: false,
521 order: crate::fields::UNORDERED,
522 locals: 0,
523 }
524}
525
526fn var(v: VarId) -> Core {
527 Core {
528 kind: CoreKind::Var(v),
529 ty: Ty::unit(),
530 tier: Tier::Any,
531 span: beck_diag::Span::NONE,
532 last_use: false,
533 order: crate::fields::UNORDERED,
534 locals: 0,
535 }
536}
537
538fn substitute(plan: &mut Plan, from: OpId, to: OpId) {
544 let swap = |id: &mut OpId| {
545 if *id == from {
546 *id = to;
547 }
548 };
549 for node in &mut plan.nodes {
550 node.inputs.iter_mut().for_each(swap);
551 if let Op::MapList { f } | Op::FilterList { f } | Op::SortBy { f } | Op::FlatMap { f } =
552 &mut node.op
553 {
554 f.captures.iter_mut().for_each(swap);
555 }
556 }
557 swap(&mut plan.root);
558 for (_, id) in &mut plan.signals {
559 swap(id);
560 }
561}
562
563fn remap(
564 fired: &mut [(OpId, Fusion)],
565 refused: &mut Vec<(OpId, OpId, Refusal)>,
566 map: &BTreeMap<OpId, OpId>,
567) {
568 for (at, _) in fired.iter_mut() {
569 if let Some(&n) = map.get(at) {
570 *at = n;
571 }
572 }
573 refused.retain(|(at, kept, _)| map.contains_key(at) && map.contains_key(kept));
575 for (at, kept, _) in refused.iter_mut() {
576 *at = map[at];
577 *kept = map[kept];
578 }
579}
580
581pub fn report(f: &Fusions) -> String {
587 use std::fmt::Write;
588 let mut out = String::new();
589 let _ = writeln!(out, "\nwhat fused (§5.3)");
590 if f.fired.is_empty() {
591 let _ = writeln!(
592 out,
593 " nothing.{}",
594 if f.arrangements.0 == 0 {
595 " This view holds no collection, so there is no pair of collection\n \
596 operators for a rule to match."
597 } else {
598 " No operator here is read by exactly one operator that could absorb\n it."
599 }
600 );
601 }
602 for fusion in &f.fired {
603 let _ = writeln!(
604 out,
605 " #{:<3} {:<38} → {}",
606 fusion.at, fusion.rule, fusion.became
607 );
608 let _ = writeln!(out, " {}", fusion.why);
609 }
610 if !f.refused.is_empty() {
611 let _ = writeln!(out, "\nwhat matched a rule and did not fuse");
612 for r in &f.refused {
613 let _ = writeln!(out, " #{:<3} {:<38} kept #{}", r.at, r.rule, r.kept);
614 let _ = writeln!(out, " {}", r.why);
615 }
616 }
617 let _ = writeln!(
618 out,
619 "\n {} operators before, {} after; {} arrangements before, {} after. An arrangement is \n \
620 memory per subscriber as well as work per event (docs/26 §26.7), which is why the second \n \
621 pair is the one to read.",
622 f.operators.0, f.operators.1, f.arrangements.0, f.arrangements.1
623 );
624 out
625}