1use std::collections::{BTreeMap, BTreeSet};
56use std::sync::Arc;
57
58use crate::check::Program;
59use crate::core::{Core, CoreKind, Prim};
60use crate::plan::Plan;
61use crate::signal::{Op, SigId};
62use crate::split::Placed;
63use crate::ty::Effect;
64
65pub const RULES: &[(Prim, &str)] = &[
71 (Prim::MapList, "a delta in, the same delta mapped out"),
72 (
73 Prim::FilterList,
74 "a delta in, kept or dropped by the predicate",
75 ),
76 (
77 Prim::ListLen,
78 "±1 per delta — §3.8's `remaining`, never a recount",
79 ),
80 (Prim::ListIsEmpty, "a count, thresholded"),
81 (Prim::MapValues, "the arrangement, read by value"),
82 (Prim::MapLen, "±1 per insert or remove"),
83 (Prim::MapGet, "a point lookup into the arrangement"),
84 (Prim::MapContains, "a point lookup into the arrangement"),
85 (
86 Prim::SortBy,
87 "an ordered arrangement, maintained by insertion",
88 ),
89 (Prim::ConcatLists, "a union of delta streams"),
90 (Prim::Add, "pointwise"),
92 (Prim::Sub, "pointwise"),
93 (Prim::Mul, "pointwise"),
94 (Prim::Div, "pointwise"),
95 (Prim::Rem, "pointwise"),
96 (Prim::Neg, "pointwise"),
97 (Prim::Eq, "pointwise"),
98 (Prim::Ne, "pointwise"),
99 (Prim::Lt, "pointwise"),
100 (Prim::Le, "pointwise"),
101 (Prim::Gt, "pointwise"),
102 (Prim::Ge, "pointwise"),
103 (Prim::And, "pointwise"),
104 (Prim::Or, "pointwise"),
105 (Prim::Not, "pointwise"),
106 (Prim::ToStr, "pointwise"),
107 (Prim::StrTrim, "pointwise"),
108 (Prim::StrIsEmpty, "pointwise"),
109 (Prim::OptionIsSome, "pointwise"),
110 (Prim::OptionUnwrapOr, "pointwise"),
111 (
114 Prim::HtmlEl,
115 "a subtree delta — what the patch protocol already streams",
116 ),
117 (Prim::HtmlText, "a text patch"),
118 (Prim::HtmlAttr, "an attribute patch"),
119 (Prim::HtmlOn, "an attribute patch"),
120 (Prim::HtmlKey, "the key a keyed-children diff is by"),
121];
122
123fn rule(op: Prim) -> Option<&'static str> {
124 RULES.iter().find(|(p, _)| *p == op).map(|(_, r)| *r)
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum Verdict {
130 Incremental,
132 Trivial,
141 Recompute { because: String },
144 Effectful { effects: Vec<Effect> },
146}
147
148impl Verdict {
149 pub fn name(&self) -> &'static str {
150 match self {
151 Verdict::Incremental => "incremental",
152 Verdict::Trivial => "no collection work",
153 Verdict::Recompute { .. } => "recompute",
154 Verdict::Effectful { .. } => "not a candidate",
155 }
156 }
157}
158
159#[derive(Clone, Debug)]
161pub struct Assessment {
162 pub node: SigId,
163 pub label: Arc<str>,
164 pub verdict: Verdict,
165 pub ops: Vec<(Prim, &'static str)>,
168 pub shared: bool,
171 pub per_session: bool,
174}
175
176pub fn assess(placed: &Placed) -> Vec<Assessment> {
182 let g = &placed.graph;
183 let below = per_session_closure(placed);
184 let mut out = Vec::new();
185 for id in g.order() {
186 let node = g.node(id);
187 let f = match &node.op {
188 Op::Map { f } | Op::Map2 { f } | Op::PerSession { f } => f,
189 _ => continue,
192 };
193 let (verdict, ops) = judge(f, &placed.program);
194 out.push(Assessment {
195 node: id,
196 label: node.label.clone(),
197 verdict,
198 ops,
199 shared: g.consumers(id).len() > 1,
200 per_session: below.contains(&id),
201 });
202 }
203 out
204}
205
206fn per_session_closure(placed: &Placed) -> BTreeSet<SigId> {
211 let g = &placed.graph;
212 let mut below = BTreeSet::new();
213 for id in g.order() {
215 let node = g.node(id);
216 if matches!(node.op, Op::PerSession { .. } | Op::Presence)
220 || node.inputs.iter().any(|i| below.contains(i))
221 {
222 below.insert(id);
223 }
224 }
225 below
226}
227
228fn judge(f: &Core, program: &Program) -> (Verdict, Vec<(Prim, &'static str)>) {
230 let mut found: Vec<(Prim, &'static str)> = Vec::new();
231 let mut blocker: Option<String> = None;
232 let mut seen: BTreeSet<Arc<str>> = BTreeSet::new();
233
234 let mut effects = Vec::new();
236 f.effects(&globals_of(program), &mut effects);
237 effects.retain(|e| !e.is_ambient());
238 if !effects.is_empty() {
239 return (Verdict::Effectful { effects }, found);
240 }
241
242 walk_through(f, program, &mut seen, &mut |c| {
243 if blocker.is_some() {
244 return;
245 }
246 match &c.kind {
247 CoreKind::Prim { op, .. } => match rule(*op) {
248 Some(r) => {
249 if !found.iter().any(|(p, _)| p == op) {
250 found.push((*op, r));
251 }
252 }
253 None => {
254 blocker = Some(format!(
255 "`{}` has no delta rule: a change to its input can change all of its \
256 output",
257 op.name()
258 ))
259 }
260 },
261 CoreKind::Match { .. } => {
266 blocker = Some(
267 "a `match` on the input picks which computation runs, and a delta can move it \
268 between arms"
269 .to_string(),
270 )
271 }
272 CoreKind::Global(name) => {
273 if program.defs.contains_key(name) {
274 return;
275 }
276 blocker = Some(format!(
277 "`{name}` is not a definition this analysis can see into"
278 ));
279 }
280 _ => {}
281 }
282 });
283
284 match blocker {
285 Some(because) => (Verdict::Recompute { because }, found),
286 None if found.is_empty() => (Verdict::Trivial, found),
287 None => (Verdict::Incremental, found),
288 }
289}
290
291fn globals_of(program: &Program) -> impl Fn(&str) -> Vec<Effect> + '_ {
292 move |name: &str| {
293 program
294 .defs
295 .get(name)
296 .map(|d| d.effects.clone())
297 .unwrap_or_default()
298 }
299}
300
301fn walk_through(
307 c: &Core,
308 program: &Program,
309 seen: &mut BTreeSet<Arc<str>>,
310 f: &mut impl FnMut(&Core),
311) {
312 f(c);
313 if let CoreKind::Global(name) = &c.kind {
314 if seen.insert(name.clone()) {
315 if let Some(def) = program.defs.get(name) {
316 walk_through(&def.body, program, seen, f);
317 }
318 }
319 return;
320 }
321 children(c, &mut |k| walk_through(k, program, seen, f));
322}
323
324fn children(c: &Core, f: &mut impl FnMut(&Core)) {
325 match &c.kind {
326 CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => {}
327 CoreKind::Lam { body, .. } => f(body),
328 CoreKind::App { func, args } => {
329 f(func);
330 args.iter().for_each(f);
331 }
332 CoreKind::Prim { args, .. } => args.iter().for_each(f),
333 CoreKind::Let { value, body, .. } => {
334 f(value);
335 f(body);
336 }
337 CoreKind::If { cond, then, alt } => {
338 f(cond);
339 f(then);
340 f(alt);
341 }
342 CoreKind::Match { scrutinee, arms } => {
343 f(scrutinee);
344 for e in arms.iter().flat_map(|a| a.exprs()) {
345 f(e);
346 }
347 }
348 CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, v)| f(v)),
349 CoreKind::Field { base, .. } => f(base),
350 CoreKind::With { base, fields } => {
351 f(base);
352 fields.iter().for_each(|(_, v)| f(v));
353 }
354 CoreKind::ListLit(items) => items.iter().for_each(f),
355 CoreKind::MapLit(pairs) => pairs.iter().for_each(|(k, v)| {
356 f(k);
357 f(v);
358 }),
359 }
360}
361
362pub fn report(placed: &Placed, only: Option<&str>) -> String {
364 use std::fmt::Write;
365 let all = assess(placed);
366 let rows: Vec<&Assessment> = match only {
367 None => all.iter().collect(),
368 Some(name) => all.iter().filter(|a| a.label.as_ref() == name).collect(),
369 };
370 let mut out = String::new();
371
372 if let Some(name) = only {
373 if rows.is_empty() {
374 let known: Vec<&str> = all.iter().map(|a| a.label.as_ref()).collect();
375 let _ = writeln!(
376 out,
377 "`{name}` is not a view in this program.\nviews: {}",
378 if known.is_empty() {
379 "none — every signal is the fold, the chokepoint or the ingress".to_string()
380 } else {
381 known.join(", ")
382 }
383 );
384 return out;
385 }
386 }
387
388 let plan = Plan::compile(placed);
392 let (maintained, recomputed) = plan.counts();
393 let _ = writeln!(out, "{}\n", headline(maintained, recomputed));
394
395 if rows.is_empty() {
396 let _ = writeln!(
397 out,
398 "This program has no views: the page reads the accumulator directly, so there is\n\
399 nothing between the fold and the browser that is a view in §3.8's sense. What the\n\
400 page itself does is still decomposed — see the operators below."
401 );
402 let _ = write!(out, "{}", plan_section(&plan));
403 return out;
404 }
405
406 let w = rows
407 .iter()
408 .map(|a| a.label.chars().count())
409 .max()
410 .unwrap_or(0);
411 for a in &rows {
412 let mut tags = Vec::new();
413 if a.shared {
414 tags.push("shared");
415 }
416 if a.per_session {
417 tags.push("per session");
418 }
419 let _ = writeln!(
420 out,
421 " {:<w$} {:<15}{}",
422 a.label,
423 a.verdict.name(),
424 if tags.is_empty() {
425 String::new()
426 } else {
427 format!("({})", tags.join(", "))
428 },
429 );
430 match &a.verdict {
431 Verdict::Incremental => {
432 for (op, r) in &a.ops {
433 let _ = writeln!(out, " {:w$} {:<14} {r}", "", op.name());
434 }
435 }
436 Verdict::Trivial => {
437 let _ = writeln!(
438 out,
439 " {:w$} applies no collection operation: the value is rebuilt from its \
440 inputs",
441 ""
442 );
443 }
444 Verdict::Recompute { because } => {
445 let _ = writeln!(out, " {:w$} {because}", "");
446 if !a.ops.is_empty() {
447 let _ = writeln!(
448 out,
449 " {:w$} the rest would have been: {}",
450 "",
451 a.ops
452 .iter()
453 .map(|(p, _)| p.name())
454 .collect::<Vec<_>>()
455 .join(", ")
456 );
457 }
458 }
459 Verdict::Effectful { effects } => {
460 let _ = writeln!(
461 out,
462 " {:w$} performs {{{}}}, so §3.8's precondition — an empty row — does not \
463 hold",
464 "",
465 effects
466 .iter()
467 .map(|e| e.name())
468 .collect::<Vec<_>>()
469 .join(", ")
470 );
471 }
472 }
473 }
474
475 if only.is_none() {
476 let shared: Vec<&str> = rows
477 .iter()
478 .filter(|a| a.shared)
479 .map(|a| a.label.as_ref())
480 .collect();
481 let fanout: Vec<&str> = rows
482 .iter()
483 .filter(|a| a.per_session)
484 .map(|a| a.label.as_ref())
485 .collect();
486 let _ = write!(out, "{}", plan_section(&plan));
487 let _ = writeln!(out, "\nthe shape of the signal graph (§5.3)");
488 let _ = writeln!(
489 out,
490 " shared arrangement: {}",
491 if shared.is_empty() {
492 "nothing is read twice, so there is no prefix to share".to_string()
493 } else {
494 shared.join(", ")
495 }
496 );
497 let _ = writeln!(
498 out,
499 " per subscriber: {}",
500 if fanout.is_empty() {
501 "nothing — this program broadcasts one view to every connection".to_string()
502 } else {
503 format!(
504 "{} (one plan, these operators per connected session)",
505 fanout.join(", ")
506 )
507 }
508 );
509 let shared_ops = plan.shared().len();
510 let _ = writeln!(
511 out,
512 " in the plan: {shared_ops} of {} operators read neither the session nor who \n\
513 \x20 is connected, and the runtime holds those once for every \n\
514 \x20 subscriber — one shared dataflow, advanced per event rather \n\
515 \x20 than per connection (docs/26). The other {} run per \n\
516 \x20 subscriber.",
517 plan.nodes.len(),
518 plan.nodes.len() - shared_ops,
519 );
520 let n = rows.len();
521 let inc = rows
522 .iter()
523 .filter(|a| a.verdict == Verdict::Incremental)
524 .count();
525 let eff = rows
526 .iter()
527 .filter(|a| matches!(a.verdict, Verdict::Effectful { .. }))
528 .count();
529 let _ = write!(
530 out,
531 "\n{inc} of {n} view{} could be maintained by delta",
532 if n == 1 { "" } else { "s" },
533 );
534 if n - inc - eff > 0 {
535 let _ = write!(out, "; {} would be recomputed", n - inc - eff);
536 }
537 if eff > 0 {
538 let _ = write!(
539 out,
540 "; {eff} {} not a candidate, because an effect decides when it runs",
541 if eff == 1 { "is" } else { "are" }
542 );
543 }
544 let _ = writeln!(out, ".");
545 }
546 out
547}
548
549fn headline(maintained: usize, recomputed: usize) -> String {
555 if maintained == 0 {
556 return format!(
557 "**Nothing in this view is maintained by delta.** The plan found no collection for a\n\
558 delta to flow through, so all {recomputed} of its operators are recomputed — each one\n\
559 only when an input actually moved, which is what a plan buys even here."
560 );
561 }
562 format!(
563 "Views are **maintained by delta** as far as the plan can decompose them: {maintained} of\n\
564 this view's {} operators update from the change itself, {recomputed} are recomputed when\n\
565 an input moves, and the page's children are still assembled in full every time\n\
566 (docs/24 §24.6).",
567 maintained + recomputed
568 )
569}
570
571fn plan_section(plan: &Plan) -> String {
578 use std::fmt::Write;
579 let mut out = String::new();
580 let (maintained, recomputed) = plan.counts();
581 let _ = writeln!(out, "\nthe operators the view compiles to");
582
583 let mut kinds: BTreeMap<&str, (usize, usize)> = BTreeMap::new();
584 for node in &plan.nodes {
585 let e = kinds.entry(node.op.name()).or_default();
586 e.0 += 1;
587 if !node.per_session {
588 e.1 += 1;
589 }
590 }
591 for (name, (n, shared)) in &kinds {
592 let example = plan
593 .nodes
594 .iter()
595 .find(|x| x.op.name() == *name)
596 .map(|x| &x.op);
597 let kind = match example {
598 Some(op) if op.is_source() => "source",
599 Some(op) if op.maintained() => "maintained",
600 _ => "recomputed",
601 };
602 let _ = writeln!(
603 out,
604 " {:<14} ×{:<4} {:<11} {}",
605 name,
606 n,
607 kind,
608 if *shared == 0 {
609 "per session".to_string()
610 } else if shared == n {
611 "shared".to_string()
612 } else {
613 format!("{shared} of {n} shared")
614 }
615 );
616 }
617
618 let mut reasons: Vec<&str> = plan
621 .nodes
622 .iter()
623 .filter_map(|n| n.because.as_deref())
624 .collect();
625 reasons.sort();
626 reasons.dedup();
627 if !reasons.is_empty() {
628 let _ = writeln!(out, "\n what could not be pushed a delta through");
629 for r in reasons {
630 let _ = writeln!(out, " {r}");
631 }
632 }
633 let _ = writeln!(
634 out,
635 "\n {maintained} maintained, {recomputed} recomputed. A recomputed operator is\n \
636 re-evaluated only when one of its inputs moved, which is what a plan buys even where a\n \
637 delta rule does not exist."
638 );
639 out
640}
641
642pub fn verdicts(placed: &Placed) -> BTreeMap<Arc<str>, Verdict> {
644 assess(placed)
645 .into_iter()
646 .map(|a| (a.label, a.verdict))
647 .collect()
648}