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 if plan.nodes[i].relate.is_none() {
217 plan.nodes[i].relate = plan.nodes[p].relate.take();
218 }
219 fired.push((
220 i,
221 Fusion {
222 rule: rule.name,
223 at: i,
224 became: plan.nodes[i].op.name(),
225 why: rule.why,
226 },
227 ));
228 return Some((p, i));
229 }
230 None
231}
232
233struct Rule {
235 name: &'static str,
236 why: &'static str,
237 carries_work: bool,
240 apply: fn(&mut Plan, OpId, OpId),
241}
242
243fn matching(consumer: &Op, producer: &Op) -> Option<&'static Rule> {
244 match (consumer, producer) {
245 (Op::MapList { .. }, Op::MapList { .. }) => Some(&MAP_OVER_MAP),
246 (Op::FilterList { .. }, Op::FilterList { .. }) => Some(&FILTER_OVER_FILTER),
247 (Op::Flatten, Op::MapList { .. }) => Some(&FLATTEN_OVER_MAP),
248 (Op::Count | Op::IsEmpty, Op::MapList { .. } | Op::SortBy { .. }) => Some(&COUNT_OVER),
249 _ => None,
250 }
251}
252
253static MAP_OVER_MAP: Rule = Rule {
254 name: "map_list over map_list",
255 why: "neither moves an element, so both arrangements have the input's key and the composition \
256 has it too",
257 carries_work: true,
258 apply: |plan, i, p| {
259 let inner = fun_of(&plan.nodes[p].op)
260 .expect("the rule matched a map_list")
261 .clone();
262 let outer = fun_of(&plan.nodes[i].op)
263 .expect("the rule matched a map_list")
264 .clone();
265 plan.nodes[i].op = Op::MapList {
266 f: compose(&outer, &inner),
267 };
268 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
269 },
270};
271
272static FILTER_OVER_FILTER: Rule = Rule {
273 name: "filter_list over filter_list",
274 why: "a conjunction, and it short-circuits — the outer predicate is applied to exactly the \
275 elements the inner one kept, which is what the pair did",
276 carries_work: true,
277 apply: |plan, i, p| {
278 let inner = fun_of(&plan.nodes[p].op)
279 .expect("the rule matched a filter_list")
280 .clone();
281 let outer = fun_of(&plan.nodes[i].op)
282 .expect("the rule matched a filter_list")
283 .clone();
284 plan.nodes[i].op = Op::FilterList {
285 f: conjoin(&outer, &inner),
286 };
287 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
288 },
289};
290
291static FLATTEN_OVER_MAP: Rule = Rule {
292 name: "flatten over map_list",
293 why: "the map's key is the input's and the flatten's is the map's followed by a position, so \
294 one operator keyed by the input's key and a position is the same order",
295 carries_work: true,
296 apply: |plan, i, p| {
297 let f = fun_of(&plan.nodes[p].op)
298 .expect("the rule matched a map_list")
299 .clone();
300 plan.nodes[i].op = Op::FlatMap { f };
301 plan.nodes[i].inputs = plan.nodes[p].inputs.clone();
302 },
303};
304
305static COUNT_OVER: Rule = Rule {
306 name: "a count over a cardinality-preserving operator",
307 why: "`map_list` and `sort_by` produce one entry per entry, so how many there are is a \
308 question about the input and the arrangement between them is never read",
309 carries_work: false,
312 apply: |plan, i, p| {
313 plan.nodes[i].inputs = vec![plan.nodes[p].inputs[0]];
314 },
315};
316
317fn refuses(plan: &Plan, i: OpId, p: OpId, rule: &Rule) -> Option<String> {
319 if p == plan.state || p == plan.session || p == plan.root {
320 return Some("it is the plan's root or one of its sources".to_string());
321 }
322 if plan.nodes[p].consumers > 1 {
323 return Some(format!(
324 "#{p} is read by {} operators, and fusing it into one of them would compute it {} \
325 times (docs/23)",
326 plan.nodes[p].consumers, plan.nodes[p].consumers
327 ));
328 }
329 let names = plan.names_of(p);
330 if !names.is_empty() {
331 return Some(format!(
332 "`{}` is a declared signal, so the read model projects it as a table (docs/23)",
333 names.join("`, `")
334 ));
335 }
336 if rule.carries_work && !plan.nodes[p].per_session && plan.nodes[i].per_session {
337 return Some(format!(
338 "#{p} is shared and #{i} is per session, so fusing would move work the process does \
339 once per event to work it does once per event per subscriber (docs/23 §5.3)"
340 ));
341 }
342 None
343}
344
345fn fun_of(op: &Op) -> Option<&Fun> {
346 match op {
347 Op::MapList { f } | Op::FilterList { f } | Op::SortBy { f } | Op::FlatMap { f } => Some(f),
348 _ => None,
349 }
350}
351
352fn compose(outer: &Fun, inner: &Fun) -> Fun {
363 let (params, inner_args, outer_caps, x) = frame(outer, inner);
364 let applied = apply(&inner.code, inner_args);
365 let mut outer_args: Vec<Core> = outer_caps;
366 outer_args.push(applied);
367 let _ = x;
368 Fun {
369 code: lam(params, apply(&outer.code, outer_args)),
370 captures: inner
371 .captures
372 .iter()
373 .chain(outer.captures.iter())
374 .copied()
375 .collect(),
376 }
377}
378
379fn conjoin(outer: &Fun, inner: &Fun) -> Fun {
386 let (params, inner_args, outer_caps, x) = frame(outer, inner);
387 let mut outer_args: Vec<Core> = outer_caps;
388 outer_args.push(var(x));
389 Fun {
390 code: lam(
391 params,
392 Core {
393 kind: CoreKind::If {
394 cond: Box::new(apply(&inner.code, inner_args)),
395 then: Box::new(apply(&outer.code, outer_args)),
396 alt: Box::new(Core {
397 kind: CoreKind::Const(Const::Bool(false)),
398 ty: Ty::bool_(),
399 tier: Tier::Any,
400 span: beck_diag::Span::NONE,
401 last_use: false,
402 order: crate::fields::UNORDERED,
403 locals: 0,
404 }),
405 },
406 ty: Ty::bool_(),
407 tier: Tier::Any,
408 span: beck_diag::Span::NONE,
409 last_use: false,
410 order: crate::fields::UNORDERED,
411 locals: 0,
412 },
413 ),
414 captures: inner
415 .captures
416 .iter()
417 .chain(outer.captures.iter())
418 .copied()
419 .collect(),
420 }
421}
422
423fn frame(outer: &Fun, inner: &Fun) -> (Vec<VarId>, Vec<Core>, Vec<Core>, VarId) {
426 let n = inner.captures.len() + outer.captures.len() + 1;
427 let base = 1 + max_var(&inner.code).max(max_var(&outer.code));
431 let params: Vec<VarId> = (0..n as VarId).map(|k| base + k).collect();
432 let inner_args: Vec<Core> = params[..inner.captures.len()]
433 .iter()
434 .copied()
435 .chain(std::iter::once(params[n - 1]))
436 .map(var)
437 .collect();
438 let outer_caps: Vec<Core> = params[inner.captures.len()..n - 1]
439 .iter()
440 .copied()
441 .map(var)
442 .collect();
443 let x = params[n - 1];
444 (params, inner_args, outer_caps, x)
445}
446
447fn max_var(c: &Core) -> VarId {
448 let mut top = 0;
449 walk(c, &mut |x| {
450 if let CoreKind::Var(v) = &x.kind {
451 top = top.max(*v);
452 }
453 if let CoreKind::Lam { params, .. } = &x.kind {
454 for p in params.iter() {
455 top = top.max(*p);
456 }
457 }
458 if let CoreKind::Let { var, .. } = &x.kind {
459 top = top.max(*var);
460 }
461 });
462 top
463}
464
465fn walk(c: &Core, f: &mut impl FnMut(&Core)) {
466 f(c);
467 match &c.kind {
468 CoreKind::Var(_) | CoreKind::Const(_) | CoreKind::Global(_) => {}
469 CoreKind::Lam { body, .. } => walk(body, f),
470 CoreKind::App { func, args } => {
471 walk(func, f);
472 args.iter().for_each(|a| walk(a, f));
473 }
474 CoreKind::Prim { args, .. } => args.iter().for_each(|a| walk(a, f)),
475 CoreKind::Let { value, body, .. } => {
476 walk(value, f);
477 walk(body, f);
478 }
479 CoreKind::If { cond, then, alt } => {
480 walk(cond, f);
481 walk(then, f);
482 walk(alt, f);
483 }
484 CoreKind::Match { scrutinee, arms } => {
485 walk(scrutinee, f);
486 arms.iter().flat_map(|a| a.exprs()).for_each(|e| walk(e, f));
487 }
488 CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, v)| walk(v, f)),
489 CoreKind::Field { base, .. } => walk(base, f),
490 CoreKind::With { base, fields } => {
491 walk(base, f);
492 fields.iter().for_each(|(_, v)| walk(v, f));
493 }
494 CoreKind::ListLit(items) => items.iter().for_each(|i| walk(i, f)),
495 CoreKind::MapLit(pairs) => pairs.iter().for_each(|(k, v)| {
496 walk(k, f);
497 walk(v, f);
498 }),
499 }
500}
501
502fn apply(f: &Core, args: Vec<Core>) -> Core {
503 let ty = match &f.ty {
504 Ty::Fun(_, ret, _) => (**ret).clone(),
505 _ => Ty::unit(),
506 };
507 Core {
508 kind: CoreKind::App {
509 func: Box::new(f.clone()),
510 args,
511 },
512 ty,
513 tier: Tier::Any,
514 span: f.span,
515 last_use: false,
516 order: crate::fields::UNORDERED,
517 locals: 0,
518 }
519}
520
521fn lam(params: Vec<VarId>, body: Core) -> Core {
522 Core {
523 ty: Ty::fun(params.iter().map(|_| Ty::unit()).collect(), body.ty.clone()),
524 tier: body.tier,
525 span: body.span,
526 kind: CoreKind::Lam {
527 params: params.into(),
528 body: std::sync::Arc::new(body),
529 },
530 last_use: false,
531 order: crate::fields::UNORDERED,
532 locals: 0,
533 }
534}
535
536fn var(v: VarId) -> Core {
537 Core {
538 kind: CoreKind::Var(v),
539 ty: Ty::unit(),
540 tier: Tier::Any,
541 span: beck_diag::Span::NONE,
542 last_use: false,
543 order: crate::fields::UNORDERED,
544 locals: 0,
545 }
546}
547
548fn substitute(plan: &mut Plan, from: OpId, to: OpId) {
554 let swap = |id: &mut OpId| {
555 if *id == from {
556 *id = to;
557 }
558 };
559 for node in &mut plan.nodes {
560 node.inputs.iter_mut().for_each(swap);
561 for f in node.op.funs_mut() {
562 f.captures.iter_mut().for_each(swap);
563 }
564 }
565 swap(&mut plan.root);
566 for (_, id) in &mut plan.signals {
567 swap(id);
568 }
569}
570
571fn remap(
572 fired: &mut [(OpId, Fusion)],
573 refused: &mut Vec<(OpId, OpId, Refusal)>,
574 map: &BTreeMap<OpId, OpId>,
575) {
576 for (at, _) in fired.iter_mut() {
577 if let Some(&n) = map.get(at) {
578 *at = n;
579 }
580 }
581 refused.retain(|(at, kept, _)| map.contains_key(at) && map.contains_key(kept));
583 for (at, kept, _) in refused.iter_mut() {
584 *at = map[at];
585 *kept = map[kept];
586 }
587}
588
589pub fn report(f: &Fusions) -> String {
595 use std::fmt::Write;
596 let mut out = String::new();
597 let _ = writeln!(out, "\nwhat fused (§5.3)");
598 if f.fired.is_empty() {
599 let _ = writeln!(
600 out,
601 " nothing.{}",
602 if f.arrangements.0 == 0 {
603 " This view holds no collection, so there is no pair of collection\n \
604 operators for a rule to match."
605 } else {
606 " No operator here is read by exactly one operator that could absorb\n it."
607 }
608 );
609 }
610 for fusion in &f.fired {
611 let _ = writeln!(
612 out,
613 " #{:<3} {:<38} → {}",
614 fusion.at, fusion.rule, fusion.became
615 );
616 let _ = writeln!(out, " {}", fusion.why);
617 }
618 if !f.refused.is_empty() {
619 let _ = writeln!(out, "\nwhat matched a rule and did not fuse");
620 for r in &f.refused {
621 let _ = writeln!(out, " #{:<3} {:<38} kept #{}", r.at, r.rule, r.kept);
622 let _ = writeln!(out, " {}", r.why);
623 }
624 }
625 let _ = writeln!(
626 out,
627 "\n {} operators before, {} after; {} arrangements before, {} after. An arrangement is \n \
628 memory per subscriber as well as work per event (docs/23 §23.14), which is why the second \n \
629 pair is the one to read.",
630 f.operators.0, f.operators.1, f.arrangements.0, f.arrangements.1
631 );
632 out
633}