1use std::collections::{BTreeMap, BTreeSet};
49use std::sync::Arc;
50
51use beck_diag::{Diagnostic, Diagnostics, Span};
52
53use crate::check::Program;
54use crate::core::{Core, CoreKind, Prim, VarId};
55use crate::signal::{signal_elem, Cut, Graph, Op, SigId, FUSED_STATE};
56use crate::ty::{Tier, Ty};
57
58#[derive(Clone, Debug)]
60pub struct Placed {
61 pub program: Program,
62 pub roles: Roles,
63 pub wire_id: String,
67 pub placement: crate::place::Solution,
70 pub graph: Graph,
73 pub render: crate::render::Decision,
77 pub kind: Kind,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum Kind {
90 Application,
92 Library,
95}
96
97impl Placed {
98 pub fn is_application(&self) -> bool {
99 self.kind == Kind::Application
100 }
101
102 pub fn library(program: Program, graph: Graph, wire_id: String) -> Placed {
111 let span = beck_diag::Span::NONE;
112 let unit = || Core::new(CoreKind::Const(crate::core::Const::Unit), Ty::unit(), span);
113 let lam = |n: usize, body: Core| {
114 Core::new(
115 CoreKind::Lam {
116 params: (0..n as VarId).collect(),
117 body: Arc::new(body),
118 },
119 Ty::fun((0..n).map(|_| Ty::unit()).collect(), Ty::unit()),
120 span,
121 )
122 };
123 let roles = Roles {
124 validate: lam(2, unit()),
125 fold: lam(2, Core::new(CoreKind::Var(0), Ty::unit(), span)),
126 init: unit(),
127 view: lam(3, unit()),
128 state_ty: Ty::unit(),
129 event_ty: Ty::unit(),
130 command_ty: Ty::unit(),
131 proposals_name: Arc::from(""),
132 events_name: Arc::from(""),
133 state_name: Arc::from(""),
134 page_name: Arc::from(""),
135 inlined: Vec::new(),
136 shared: Vec::new(),
137 states: Vec::new(),
138 view_is_per_session: false,
139 view_reads_presence: false,
140 };
141 Placed {
142 program,
143 wire_id,
144 placement: crate::place::Solution {
145 tiers: Default::default(),
146 explanations: Vec::new(),
147 method: crate::place::Method::Exhaustive,
148 total: 0,
149 churn: Vec::new(),
150 ties: Vec::new(),
151 },
152 render: crate::render::Decision::of(&roles, false, None, span),
153 roles,
154 graph,
155 kind: Kind::Library,
156 }
157 }
158}
159
160#[derive(Clone, Debug)]
162pub struct StateRole {
163 pub name: Arc<str>,
164 pub ty: Ty,
165 pub field: Option<Arc<str>>,
168 pub node: SigId,
169}
170
171#[derive(Clone, Debug)]
178pub struct Roles {
179 pub validate: Core,
181 pub fold: Core,
183 pub init: Core,
185 pub view: Core,
192 pub state_ty: Ty,
193 pub event_ty: Ty,
194 pub command_ty: Ty,
195 pub proposals_name: Arc<str>,
197 pub events_name: Arc<str>,
198 pub state_name: Arc<str>,
199 pub page_name: Arc<str>,
200 pub inlined: Vec<Arc<str>>,
202 pub shared: Vec<Arc<str>>,
205 pub states: Vec<StateRole>,
208 pub view_is_per_session: bool,
209 pub view_reads_presence: bool,
211}
212
213impl Roles {
214 pub fn is_fused(&self) -> bool {
216 self.states.len() > 1
217 }
218}
219
220pub fn split(mut program: Program, diags: &mut Diagnostics) -> Option<Placed> {
222 let graph = Graph::build(&program, diags)?;
223
224 let ingress = graph.ingress();
227 let Some(&proposals) = ingress.first() else {
228 diags.push(
229 Diagnostic::error("B0500", "this program has no merge point", Span::NONE)
230 .with_note(
231 "a Beck application is a fold over an event stream, and the stream starts at \
232 `merge_clients()` — the one place time enters",
233 )
234 .with_fix("add `@on(server)` and `proposals: Stream[Proposal] = merge_clients()`"),
235 );
236 return None;
237 };
238
239 let states = graph.states();
240 if states.is_empty() {
241 diags.push(
242 Diagnostic::error("B0501", "this program has no durable state", Span::NONE)
243 .with_note("`durable(fold(f, init, s))` is what makes the log a database")
244 .with_fix("wrap the fold: `@on(data)` and `durable(fold(apply_event, …, events))`"),
245 );
246 return None;
247 }
248
249 let mut folds: Vec<(SigId, SigId)> = Vec::new(); for &s in &states {
252 let inner = follow_alias(&graph, graph.node(s).inputs[0]);
253 if !matches!(graph.node(inner).op, Op::Fold { .. }) {
254 diags.push(
255 Diagnostic::error("B0502", "`durable` must wrap a `fold`", graph.node(s).span)
256 .with_primary_label("only a fold has an accumulator to persist")
257 .with_label(
258 graph.node(inner).span,
259 format!("this is a `{}`", graph.node(inner).op.name()),
260 ),
261 );
262 return None;
263 }
264 folds.push((s, inner));
265 }
266
267 let decides = graph.decides();
270 let Some(&decide) = decides.first() else {
271 diags.push(
272 Diagnostic::error(
273 "B0504",
274 "events must come from `decide`",
275 graph.node(folds[0].1).span,
276 )
277 .with_primary_label("this fold has no chokepoint upstream of it")
278 .with_note(
279 "`decide` is the sole consumer of ingress and the one place a command becomes \
280 an event — §3.5's \"authority is one chokepoint\"",
281 ),
282 );
283 return None;
284 };
285 if decides.len() > 1 {
286 diags.push(
287 Diagnostic::error(
288 "B0511",
289 "a program has one authority chokepoint",
290 graph.node(decides[1]).span,
291 )
292 .with_primary_label("a second `decide`")
293 .with_label(graph.node(decide).span, "the first one is here")
294 .with_note(
295 "§3.5 rests on validation being one place: two of them are two answers to \"may \
296 this actor do this\", and the log would record whichever ran",
297 ),
298 );
299 return None;
300 }
301
302 if let Some(&here) = graph
306 .presences()
307 .iter()
308 .find(|&&p| reaches(&graph, decide, p))
309 {
310 diags.push(
311 Diagnostic::error(
312 "B0515",
313 "the chokepoint reads `presence`, which is not in the log",
314 graph.node(decide).span,
315 )
316 .with_primary_label(format!(
317 "`{}` decides from `{}`",
318 graph.label(decide),
319 graph.label(here)
320 ))
321 .with_label(graph.node(here).span, "who is connected is decided here")
322 .with_note(
323 "an event is what a replay reproduces, and who was connected when it was recorded \
324 is not written down anywhere. A `validate` that read the roster would decide one \
325 thing today and another on replay, and the log would no longer be the whole \
326 history",
327 )
328 .with_fix(
329 "record the fact instead: propose a command when a client arrives, and decide from \
330 the state that fold produces",
331 ),
332 );
333 return None;
334 }
335
336 let mut fold_filters: Vec<Option<Core>> = Vec::new();
339 for &(_, f) in &folds {
340 let mut node = follow_alias(&graph, graph.node(f).inputs[0]);
341 let mut filter = None;
342 if let Op::FilterMap { f: pred } = &graph.node(node).op {
343 filter = Some(pred.clone());
344 node = follow_alias(&graph, graph.node(node).inputs[0]);
345 }
346 if node != decide {
347 diags.push(
348 Diagnostic::error(
349 "B0504",
350 "events must come from `decide`",
351 graph.node(f).span,
352 )
353 .with_primary_label(format!(
354 "this fold reads `{}`",
355 graph.label(graph.node(f).inputs[0])
356 ))
357 .with_note(
358 "the log holds what the chokepoint decided, so a fold reads `decide` — \
359 optionally through one `filter_map`, which is how two folds take \
360 different slices of one stream",
361 ),
362 );
363 return None;
364 }
365 fold_filters.push(filter);
366 }
367
368 let pages: Vec<SigId> = graph
371 .sinks
372 .iter()
373 .copied()
374 .filter(|&s| graph.node(s).tier == Tier::Client && is_html(&graph.node(s).ty))
375 .collect();
376 let Some(&page) = pages.first() else {
377 diags.push(
378 Diagnostic::error("B0505", "no signal is placed on the client", Span::NONE)
379 .with_note(
380 "`page` is the tier crossing: a `Signal[Html]` the browser subscribes to",
381 )
382 .with_fix("add `@on(client)` and `page: Signal[Html] = per_session(todos, view)`"),
383 );
384 return None;
385 };
386 if pages.len() > 1 {
387 diags.push(
388 Diagnostic::error(
389 "B0510",
390 "two signals are the page, and there is no router yet",
391 graph.node(pages[1]).span,
392 )
393 .with_primary_label(format!("`{}`", graph.label(pages[1])))
394 .with_label(graph.node(page).span, format!("`{}`", graph.label(page)))
395 .with_note(
396 "the slicer will slice both; the runtime serves one document per connection, and \
397 choosing between them is routing — a Phase 3 client bullet that is not built",
398 )
399 .with_fix("combine them in one view, or read one from the other"),
400 );
401 return None;
402 }
403
404 let fused = states.len() > 1;
407 let mut vars = Vars(max_var(&program));
408 let state_var = vars.fresh();
409 let session_var = vars.fresh();
410 let presence_var = vars.fresh();
411
412 let state_roles: Vec<StateRole> = folds
413 .iter()
414 .map(|&(d, _)| {
415 let n = graph.node(d);
416 StateRole {
417 name: n.label.clone(),
418 ty: signal_elem(&n.ty),
419 field: fused.then(|| n.label.clone()),
420 node: d,
421 }
422 })
423 .collect();
424
425 let mut slicer = Slicer {
426 graph: &graph,
427 states: &state_roles,
428 state_var,
429 session_var,
430 presence_var,
431 bound: BTreeMap::new(),
432 lets: Vec::new(),
433 inlined: Vec::new(),
434 shared: Vec::new(),
435 per_session: false,
436 reads_presence: false,
437 vars: &mut vars,
438 diags,
439 };
440 let view_body = slicer.lower_sink(page)?;
441 let view_body = slicer.wrap(view_body);
442 let inlined = slicer.inlined.clone();
443 let shared = slicer.shared.clone();
444 let per_session = slicer.per_session;
445 let reads_presence = slicer.reads_presence;
446
447 let state_ty = if fused {
448 Ty::con(FUSED_STATE)
449 } else {
450 state_roles[0].ty.clone()
451 };
452
453 let view = Core {
454 kind: CoreKind::Lam {
455 params: vec![state_var, session_var, presence_var].into(),
456 body: Arc::new(view_body),
457 },
458 ty: Ty::fun(
459 vec![
460 state_ty.clone(),
461 Ty::con("Session"),
462 Ty::map(Ty::str_(), Ty::int()),
463 ],
464 Ty::html(),
465 ),
466 tier: Tier::Client,
467 span: graph.node(page).span,
468 last_use: false,
469 order: crate::fields::UNORDERED,
470 locals: 0,
471 };
472
473 let (fold, init) = if fused {
476 program.types.insert(
477 Arc::from(FUSED_STATE),
478 crate::signal::fused_state_decl(
479 &state_roles
480 .iter()
481 .map(|s| (s.name.clone(), s.ty.clone()))
482 .collect::<Vec<_>>(),
483 ),
484 );
485 fuse(
486 &graph,
487 &folds,
488 &fold_filters,
489 &state_roles,
490 &state_ty,
491 &mut vars,
492 graph.node(page).span,
493 )
494 } else {
495 let Op::Fold { step, init } = &graph.node(folds[0].1).op else {
496 return None;
497 };
498 match &fold_filters[0] {
499 None => (step.clone(), init.clone()),
500 Some(pred) => (
501 filtered_step(
502 step,
503 pred,
504 &state_ty,
505 &mut vars,
506 graph.node(folds[0].1).span,
507 ),
508 init.clone(),
509 ),
510 }
511 };
512
513 let Op::Decide { validate } = &graph.node(decide).op else {
516 return None;
517 };
518 let validate = if fused {
519 let src = follow_alias(&graph, graph.node(decide).inputs[1]);
520 let Some(role) = state_roles.iter().find(|s| s.node == src) else {
521 diags.push(
522 Diagnostic::error(
523 "B0512",
524 "the chokepoint does not read a durable fold",
525 graph.node(decide).span,
526 )
527 .with_primary_label(format!("it reads `{}`", graph.label(src)))
528 .with_note(
529 "`decide` threads the accumulator through validation, so what it reads has to \
530 be one — that is what makes first-writer-wins and ownership decidable (§3.7)",
531 ),
532 );
533 return None;
534 };
535 let p = vars.fresh();
536 let s = vars.fresh();
537 let span = graph.node(decide).span;
538 Core {
539 kind: CoreKind::Lam {
540 params: vec![s, p].into(),
541 body: Arc::new(Core {
542 kind: CoreKind::App {
543 func: Box::new(validate.clone()),
544 args: vec![
545 field(var(s, state_ty.clone(), span), role, span),
546 var(p, Ty::con("Proposal"), span),
547 ],
548 },
549 ty: Ty::unit(),
550 tier: Tier::Server,
551 span,
552 last_use: false,
553 order: crate::fields::UNORDERED,
554 locals: 0,
555 }),
556 },
557 ty: Ty::unit(),
558 tier: Tier::Server,
559 span,
560 last_use: false,
561 order: crate::fields::UNORDERED,
562 locals: 0,
563 }
564 } else {
565 validate.clone()
566 };
567
568 let event_ty = signal_elem(&graph.node(decide).ty);
569 let command_ty = program
570 .types
571 .get("Command")
572 .map(|_| Ty::con("Command"))
573 .unwrap_or_else(Ty::unit);
574
575 let mut hasher = blake3::Hasher::new();
582 hasher.update(program.name.as_bytes());
583 for t in [&command_ty, &event_ty, &state_ty] {
584 hasher.update(crate::iface::structural(t, &program.types).as_bytes());
585 hasher.update(b"\x00");
586 }
587 let wire_id = hasher.finalize().to_hex()[..16].to_string();
588
589 let roles = Roles {
590 validate,
591 fold,
592 init,
593 view,
594 state_ty,
595 event_ty,
596 command_ty,
597 proposals_name: graph.node(proposals).label.clone(),
598 events_name: graph.node(decide).label.clone(),
599 state_name: state_roles[0].name.clone(),
600 page_name: graph.node(page).label.clone(),
601 inlined,
602 shared,
603 states: state_roles,
604 view_is_per_session: per_session,
605 view_reads_presence: reads_presence,
606 };
607
608 let declared = program
613 .signals
614 .iter()
615 .find(|s| s.name == graph.node(page).label)
616 .and_then(|s| s.render);
617 let render = crate::render::Decision::of(&roles, true, declared, graph.node(page).span);
618 render.refuse(diags);
619 if diags.has_errors() {
620 return None;
621 }
622
623 Some(Placed {
624 kind: Kind::Application,
625 placement: crate::place::Solution {
626 tiers: Default::default(),
627 explanations: Vec::new(),
628 method: crate::place::Method::Exhaustive,
629 total: 0,
630 churn: Vec::new(),
631 ties: Vec::new(),
632 },
633 render,
634 roles,
635 wire_id,
636 program,
637 graph,
638 })
639}
640
641fn is_html(t: &Ty) -> bool {
642 signal_elem(t).con_name() == Some(Ty::HTML)
643}
644
645fn reaches(graph: &Graph, from: SigId, target: SigId) -> bool {
650 let mut seen = BTreeSet::new();
651 let mut stack = vec![from];
652 while let Some(id) = stack.pop() {
653 if id == target {
654 return true;
655 }
656 if !seen.insert(id) {
657 continue;
658 }
659 stack.extend(graph.node(id).inputs.iter().copied());
660 }
661 false
662}
663
664fn follow_alias(graph: &Graph, mut id: SigId) -> SigId {
666 let mut guard = 0;
667 while matches!(graph.node(id).op, Op::Alias) && guard < graph.nodes.len() {
668 id = graph.node(id).inputs[0];
669 guard += 1;
670 }
671 id
672}
673
674fn var(v: VarId, ty: Ty, span: Span) -> Core {
675 Core {
676 kind: CoreKind::Var(v),
677 ty,
678 tier: Tier::Any,
679 span,
680 last_use: false,
681 order: crate::fields::UNORDERED,
682 locals: 0,
683 }
684}
685
686fn field(base: Core, role: &StateRole, span: Span) -> Core {
688 match &role.field {
689 None => base,
690 Some(f) => Core {
691 kind: CoreKind::Field {
692 base: Box::new(base),
693 name: f.clone(),
694 },
695 ty: role.ty.clone(),
696 tier: Tier::Any,
697 span,
698 last_use: false,
699 order: crate::fields::UNORDERED,
700 locals: 0,
701 },
702 }
703}
704
705fn call(func: Core, args: Vec<Core>, ty: Ty, span: Span) -> Core {
707 Core {
708 kind: CoreKind::App {
709 func: Box::new(func),
710 args,
711 },
712 ty,
713 tier: Tier::Any,
714 span,
715 last_use: false,
716 order: crate::fields::UNORDERED,
717 locals: 0,
718 }
719}
720
721fn fold_field(
724 step: &Core,
725 filter: &Option<Core>,
726 acc: Core,
727 env: Core,
728 ty: &Ty,
729 vars: &mut Vars,
730 span: Span,
731) -> Core {
732 let applied = call(
733 step.clone(),
734 vec![acc.clone(), env.clone()],
735 ty.clone(),
736 span,
737 );
738 let Some(pred) = filter else {
739 return applied;
740 };
741 let o = vars.fresh();
750 let opt_ty = Ty::option(Ty::unit());
751 let body = Core {
752 kind: CoreKind::Field {
753 base: Box::new(env.clone()),
754 name: Arc::from("body"),
755 },
756 ty: Ty::unit(),
757 tier: Tier::Any,
758 span,
759 last_use: false,
760 order: crate::fields::UNORDERED,
761 locals: 0,
762 };
763 let inner = Core {
764 kind: CoreKind::Field {
765 base: Box::new(var(o, opt_ty.clone(), span)),
766 name: Arc::from("value"),
767 },
768 ty: Ty::unit(),
769 tier: Tier::Any,
770 span,
771 last_use: false,
772 order: crate::fields::UNORDERED,
773 locals: 0,
774 };
775 let narrowed = Core {
776 kind: CoreKind::With {
777 base: Box::new(env),
778 fields: vec![(Arc::from("body"), inner)],
779 },
780 ty: Ty::unit(),
781 tier: Tier::Any,
782 span,
783 last_use: false,
784 order: crate::fields::UNORDERED,
785 locals: 0,
786 };
787 Core {
788 kind: CoreKind::Let {
789 var: o,
790 value: Box::new(call(pred.clone(), vec![body], opt_ty.clone(), span)),
791 body: Box::new(Core {
792 kind: CoreKind::If {
793 cond: Box::new(Core {
794 kind: CoreKind::Prim {
795 op: Prim::OptionIsSome,
796 args: vec![var(o, opt_ty, span)],
797 },
798 ty: Ty::bool_(),
799 tier: Tier::Any,
800 span,
801 last_use: false,
802 order: crate::fields::UNORDERED,
803 locals: 0,
804 }),
805 then: Box::new(call(
806 step.clone(),
807 vec![acc.clone(), narrowed],
808 ty.clone(),
809 span,
810 )),
811 alt: Box::new(acc),
812 },
813 ty: ty.clone(),
814 tier: Tier::Any,
815 span,
816 last_use: false,
817 order: crate::fields::UNORDERED,
818 locals: 0,
819 }),
820 },
821 ty: ty.clone(),
822 tier: Tier::Any,
823 span,
824 last_use: false,
825 order: crate::fields::UNORDERED,
826 locals: 0,
827 }
828}
829
830fn filtered_step(step: &Core, pred: &Core, state_ty: &Ty, vars: &mut Vars, span: Span) -> Core {
832 let s = vars.fresh();
833 let e = vars.fresh();
834 let body = fold_field(
835 step,
836 &Some(pred.clone()),
837 var(s, state_ty.clone(), span),
838 var(e, Ty::unit(), span),
839 state_ty,
840 vars,
841 span,
842 );
843 Core {
844 kind: CoreKind::Lam {
845 params: vec![s, e].into(),
846 body: Arc::new(body),
847 },
848 ty: Ty::fun(vec![state_ty.clone(), Ty::unit()], state_ty.clone()),
849 tier: Tier::Data,
850 span,
851 last_use: false,
852 order: crate::fields::UNORDERED,
853 locals: 0,
854 }
855}
856
857fn fuse(
864 graph: &Graph,
865 folds: &[(SigId, SigId)],
866 filters: &[Option<Core>],
867 roles: &[StateRole],
868 state_ty: &Ty,
869 vars: &mut Vars,
870 span: Span,
871) -> (Core, Core) {
872 let s = vars.fresh();
873 let e = vars.fresh();
874
875 let mut step_fields = Vec::new();
876 let mut init_fields = Vec::new();
877 for (i, &(_, f)) in folds.iter().enumerate() {
878 let Op::Fold { step, init } = &graph.node(f).op else {
879 continue;
880 };
881 let role = &roles[i];
882 let acc = field(var(s, state_ty.clone(), span), role, span);
883 step_fields.push((
884 role.name.clone(),
885 fold_field(
886 step,
887 &filters[i],
888 acc,
889 var(e, Ty::unit(), span),
890 &role.ty,
891 vars,
892 span,
893 ),
894 ));
895 init_fields.push((role.name.clone(), init.clone()));
896 }
897
898 let make = |fields: Vec<(Arc<str>, Core)>| {
901 let mut c = Core {
902 kind: CoreKind::Make {
903 ty: Arc::from(FUSED_STATE),
904 variant: None,
905 fields,
906 },
907 ty: state_ty.clone(),
908 tier: Tier::Data,
909 span,
910 last_use: false,
911 order: crate::fields::UNORDERED,
912 locals: 0,
913 };
914 crate::fields::order_here(&mut c);
915 c
916 };
917
918 (
919 Core {
920 kind: CoreKind::Lam {
921 params: vec![s, e].into(),
922 body: Arc::new(make(step_fields)),
923 },
924 ty: Ty::fun(vec![state_ty.clone(), Ty::unit()], state_ty.clone()),
925 tier: Tier::Data,
926 span,
927 last_use: false,
928 order: crate::fields::UNORDERED,
929 locals: 0,
930 },
931 make(init_fields),
932 )
933}
934
935struct Vars(VarId);
942
943impl Vars {
944 fn fresh(&mut self) -> VarId {
945 self.0 += 1;
946 self.0
947 }
948}
949
950fn max_var(program: &Program) -> VarId {
952 fn go(c: &Core, max: &mut VarId) {
953 match &c.kind {
954 CoreKind::Const(_) | CoreKind::Global(_) => {}
955 CoreKind::Var(v) => *max = (*max).max(*v),
956 CoreKind::Lam { params, body } => {
957 for p in params.iter() {
958 *max = (*max).max(*p);
959 }
960 go(body, max);
961 }
962 CoreKind::App { func, args } => {
963 go(func, max);
964 args.iter().for_each(|a| go(a, max));
965 }
966 CoreKind::Prim { args, .. } => args.iter().for_each(|a| go(a, max)),
967 CoreKind::Let { var, value, body } => {
968 *max = (*max).max(*var);
969 go(value, max);
970 go(body, max);
971 }
972 CoreKind::If { cond, then, alt } => {
973 go(cond, max);
974 go(then, max);
975 go(alt, max);
976 }
977 CoreKind::Match { scrutinee, arms } => {
978 go(scrutinee, max);
979 for a in arms {
980 for v in a.pattern.binders() {
981 *max = (*max).max(v);
982 }
983 for e in a.exprs() {
984 go(e, max);
985 }
986 }
987 }
988 CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, f)| go(f, max)),
989 CoreKind::Field { base, .. } => go(base, max),
990 CoreKind::With { base, fields } => {
991 go(base, max);
992 fields.iter().for_each(|(_, f)| go(f, max));
993 }
994 CoreKind::ListLit(items) => items.iter().for_each(|i| go(i, max)),
995 CoreKind::MapLit(pairs) => pairs.iter().for_each(|(k, v)| {
996 go(k, max);
997 go(v, max);
998 }),
999 }
1000 }
1001 let mut max = 0;
1002 for d in program.defs.values() {
1003 go(&d.body, &mut max);
1004 }
1005 for s in &program.signals {
1006 go(&s.expr, &mut max);
1007 }
1008 for t in &program.tests {
1009 max = max
1010 .max(t.bindings.state)
1011 .max(t.bindings.events)
1012 .max(t.bindings.result);
1013 }
1014 max
1015}
1016
1017struct Slicer<'a, 'd> {
1030 graph: &'a Graph,
1031 states: &'a [StateRole],
1032 state_var: VarId,
1033 session_var: VarId,
1034 presence_var: VarId,
1035 vars: &'d mut Vars,
1036 bound: BTreeMap<SigId, VarId>,
1038 lets: Vec<(VarId, Core)>,
1040 inlined: Vec<Arc<str>>,
1041 shared: Vec<Arc<str>>,
1042 per_session: bool,
1043 reads_presence: bool,
1044 diags: &'d mut Diagnostics,
1045}
1046
1047impl Slicer<'_, '_> {
1048 fn lower_sink(&mut self, id: SigId) -> Option<Core> {
1051 let body = self.lower(id)?;
1052 if let Some(name) = &self.graph.node(follow_alias(self.graph, id)).name {
1053 self.inlined.retain(|n| n != name);
1054 }
1055 Some(body)
1056 }
1057
1058 fn lower(&mut self, id: SigId) -> Option<Core> {
1059 let id = follow_alias(self.graph, id);
1060 let node = self.graph.node(id);
1061
1062 if let Some(role) = self.states.iter().find(|s| s.node == id) {
1065 return Some(field(
1066 var(self.state_var, Ty::unit(), node.span),
1067 role,
1068 node.span,
1069 ));
1070 }
1071 if matches!(node.op, Op::Presence) {
1074 self.reads_presence = true;
1075 return Some(var(self.presence_var, signal_elem(&node.ty), node.span));
1076 }
1077 if let Some(&v) = self.bound.get(&id) {
1078 return Some(var(v, signal_elem(&node.ty), node.span));
1079 }
1080
1081 let span = node.span;
1082 let ty = signal_elem(&node.ty);
1083 let body = match &node.op {
1084 Op::Map { f } => {
1085 let input = self.lower(node.inputs[0])?;
1086 call(f.clone(), vec![input], ty.clone(), span)
1087 }
1088 Op::Map2 { f } => {
1089 let a = self.lower(node.inputs[0])?;
1090 let b = self.lower(node.inputs[1])?;
1091 call(f.clone(), vec![a, b], ty.clone(), span)
1092 }
1093 Op::PerSession { f } => {
1094 self.per_session = true;
1095 let input = self.lower(node.inputs[0])?;
1096 let session = var(self.session_var, Ty::con("Session"), span);
1097 call(f.clone(), vec![input, session], ty.clone(), span)
1098 }
1099 Op::Fold { .. } => {
1100 self.diags.push(
1104 Diagnostic::error(
1105 "B0513",
1106 format!("`{}` is a fold that is not durable", self.graph.label(id)),
1107 span,
1108 )
1109 .with_primary_label("its accumulator has nowhere to live across a restart")
1110 .with_note(
1111 "the log is what survives, and `durable` is what says an accumulator is \
1112 folded from it — a fold outside one would be rebuilt from nothing on \
1113 every deploy",
1114 )
1115 .with_fix("wrap it: `durable(fold(…))`"),
1116 );
1117 return None;
1118 }
1119 Op::Ingress | Op::Decide { .. } | Op::FilterMap { .. } => {
1120 self.diags.push(
1121 Diagnostic::error(
1122 "B0507",
1123 format!(
1124 "a view cannot read `{}`, which is a stream",
1125 self.graph.label(id)
1126 ),
1127 span,
1128 )
1129 .with_primary_label(format!("`{}` produces occurrences", node.op.name()))
1130 .with_note(
1131 "§3.7: a `Stream` is discrete occurrences and a `Signal` is a value \
1132 defined at all times. A view renders a value, so it reads what a stream \
1133 was folded into",
1134 ),
1135 );
1136 return None;
1137 }
1138 Op::Durable | Op::Alias | Op::Presence => unreachable!("handled above"),
1139 };
1140
1141 if let Some(name) = &node.name {
1142 if !self.inlined.contains(name) {
1143 self.inlined.push(name.clone());
1144 }
1145 }
1146
1147 if self.graph.consumers(id).len() > 1 {
1150 let v = self.vars.fresh();
1151 self.bound.insert(id, v);
1152 self.lets.push((v, body));
1153 if let Some(name) = &node.name {
1154 self.shared.push(name.clone());
1155 }
1156 return Some(var(v, ty, span));
1157 }
1158 Some(body)
1159 }
1160
1161 fn wrap(&self, body: Core) -> Core {
1163 self.lets.iter().rev().fold(body, |acc, (v, value)| Core {
1164 kind: CoreKind::Let {
1165 var: *v,
1166 value: Box::new(value.clone()),
1167 body: Box::new(acc.clone()),
1168 },
1169 ty: acc.ty.clone(),
1170 tier: Tier::Client,
1171 span: acc.span,
1172 last_use: false,
1173 order: crate::fields::UNORDERED,
1174 locals: 0,
1175 })
1176 }
1177}
1178
1179pub fn flow_report(placed: &Placed) -> String {
1187 use std::fmt::Write;
1188 let g = &placed.graph;
1189 let r = &placed.roles;
1190 let mut out = String::new();
1191 let cycles = g.dep.cycles().count();
1192 let _ = writeln!(
1193 out,
1194 "signal graph — {} vertices, {} {}, {} tier {}\n",
1195 g.nodes.len(),
1196 cycles,
1197 if cycles == 1 { "cycle" } else { "cycles" },
1198 g.cuts.len(),
1199 if g.cuts.len() == 1 {
1200 "crossing"
1201 } else {
1202 "crossings"
1203 },
1204 );
1205
1206 let in_cycle: BTreeSet<SigId> = g
1207 .dep
1208 .cycles()
1209 .flat_map(|c| c.iter().map(|n| n.0 as usize))
1210 .collect();
1211 let page = g.by_name.get(&r.page_name).copied();
1212 let rows: Vec<(SigId, String, String)> = g
1213 .order()
1214 .into_iter()
1215 .map(|id| {
1216 let n = g.node(id);
1217 let inputs: Vec<&str> = n.inputs.iter().map(|&i| g.label(i)).collect();
1218 (
1219 id,
1220 n.label.to_string(),
1221 format!("{}({})", n.op.name(), inputs.join(", ")),
1222 )
1223 })
1224 .collect();
1225 let lw = rows.iter().map(|r| r.1.chars().count()).max().unwrap_or(0);
1226 let ew = rows.iter().map(|r| r.2.chars().count()).max().unwrap_or(0);
1227 for (id, label, expr) in &rows {
1228 let mut note = String::new();
1229 if in_cycle.contains(id) {
1230 note.push_str(" ↺");
1231 }
1232 if Some(*id) == page {
1233 note.push_str(if r.view_is_per_session {
1234 " ← the page, per session"
1235 } else {
1236 " ← the page, broadcast"
1237 });
1238 } else if g.sinks.contains(id) {
1239 note.push_str(" ← a sink nothing reads");
1240 }
1241 let _ = writeln!(
1242 out,
1243 " {label:<lw$} {expr:<ew$} {:<7}{note}",
1244 g.node(*id).tier.name(),
1245 );
1246 }
1247
1248 let _ = writeln!(out, "\naccumulator");
1249 if r.is_fused() {
1250 let _ = writeln!(
1251 out,
1252 " {} durable folds, fused into one record — §3.7 fixes one totally-ordered log per\n \
1253 application, so two folds are two projections of it rather than two logs.",
1254 r.states.len()
1255 );
1256 for s in &r.states {
1257 let _ = writeln!(out, " {FUSED_STATE}.{} : {}", s.name, s.ty);
1258 }
1259 } else {
1260 let _ = writeln!(
1261 out,
1262 " one durable fold — `{}` : {}",
1263 r.states[0].name, r.states[0].ty
1264 );
1265 }
1266
1267 let plan = slice_of(g, page.unwrap_or(0));
1268 let computed: Vec<&str> = plan
1269 .iter()
1270 .copied()
1271 .filter(|&i| {
1272 !matches!(g.node(i).op, Op::Durable | Op::Fold { .. })
1273 && !g.node(i).op.is_stream()
1274 && Some(i) != page
1275 })
1276 .map(|i| g.label(i))
1277 .collect();
1278 let _ = writeln!(out, "\nthe view recomputes, per event");
1279 let _ = writeln!(
1280 out,
1281 " {}",
1282 if computed.is_empty() {
1283 "nothing between the accumulator and the page".to_string()
1284 } else {
1285 computed.join(", ")
1286 }
1287 );
1288 let _ = writeln!(
1289 out,
1290 " shared: {}",
1291 if r.shared.is_empty() {
1292 "— (no signal is read by two consumers, so nothing is bound twice)".to_string()
1293 } else {
1294 format!(
1295 "{} (read by more than one consumer, so computed once)",
1296 r.shared
1297 .iter()
1298 .map(|s| s.to_string())
1299 .collect::<Vec<_>>()
1300 .join(", ")
1301 )
1302 }
1303 );
1304 let _ = writeln!(
1305 out,
1306 " (§5.3 makes these incremental; today every one is a full recompute)"
1307 );
1308
1309 if !g.cuts.is_empty() {
1310 let _ = writeln!(
1311 out,
1312 "\ntier crossings — each is one subscription, resumable by (id, seq) (§4.3)"
1313 );
1314 let edges: Vec<(String, String, String)> = g
1315 .cuts
1316 .iter()
1317 .map(|c| {
1318 (
1319 format!("{} → {}", g.label(c.from), g.label(c.to)),
1320 format!(
1321 "{} → {}",
1322 g.node(c.from).tier.name(),
1323 g.node(c.to).tier.name()
1324 ),
1325 format!("{}", c.carries),
1326 )
1327 })
1328 .collect();
1329 let nw = edges.iter().map(|e| e.0.chars().count()).max().unwrap_or(0);
1330 let tw = edges.iter().map(|e| e.1.chars().count()).max().unwrap_or(0);
1331 let cw = edges.iter().map(|e| e.2.chars().count()).max().unwrap_or(0);
1332 for (c, (names, tiers, carries)) in g.cuts.iter().zip(&edges) {
1333 let _ = writeln!(
1334 out,
1335 " {names:<nw$} {tiers:<tw$} carries {carries:<cw$} {}",
1336 c.id
1337 );
1338 }
1339 }
1340 out
1341}
1342
1343pub fn crossings(placed: &Placed) -> &[Cut] {
1345 &placed.graph.cuts
1346}
1347
1348pub fn slice_of(graph: &Graph, sink: SigId) -> Vec<SigId> {
1350 let mut seen = BTreeSet::new();
1351 let mut stack = vec![sink];
1352 while let Some(id) = stack.pop() {
1353 if !seen.insert(id) {
1354 continue;
1355 }
1356 for &i in &graph.node(id).inputs {
1357 stack.push(i);
1358 }
1359 }
1360 graph
1361 .order()
1362 .into_iter()
1363 .filter(|i| seen.contains(i))
1364 .collect()
1365}
1366
1367#[cfg(test)]
1368pub(crate) mod tests {
1369 use super::*;
1370 use crate::compile_str;
1371
1372 pub const TODO: &str = r#"
1374type Id = newtype[Str]
1375
1376model Todo:
1377 id: Id
1378 text: Str
1379 done: Bool
1380 owner: Str
1381
1382model State:
1383 todos: Map[Id, Todo]
1384
1385union Command:
1386 Add(id: Id, text: Str)
1387 Toggle(id: Id)
1388 Delete(id: Id)
1389
1390union Event:
1391 Added(id: Id, text: Str)
1392 Toggled(id: Id)
1393 Deleted(id: Id)
1394
1395union Rejection:
1396 BlankText
1397 IdTaken
1398 NoSuchTodo
1399 NotOwner
1400
1401def apply_event(s: State, env: Envelope[Event]) -> State:
1402 match env.body:
1403 case Added(id, text):
1404 return s.with(todos=map_insert(s.todos, id, Todo(id=id, text=text, done=False, owner=env.actor)))
1405 case Toggled(id):
1406 return toggle(s, id)
1407 case Deleted(id):
1408 return s.with(todos=map_remove(s.todos, id))
1409
1410def toggle(s: State, id: Id) -> State:
1411 match map_get(s.todos, id):
1412 case Some(value):
1413 return s.with(todos=map_insert(s.todos, id, value.with(done=not value.done)))
1414 case None:
1415 return s
1416
1417def validate(s: State, p: Proposal) -> Result[list[Event], Rejection]:
1418 match p.command:
1419 case Add(id, text):
1420 if str_is_empty(str_trim(text)):
1421 return Err(error=BlankText)
1422 if map_contains(s.todos, id):
1423 return Err(error=IdTaken)
1424 return Ok(value=[Added(id=id, text=text)])
1425 case Toggle(id):
1426 return owned(s, p, id, [Toggled(id=id)])
1427 case Delete(id):
1428 return owned(s, p, id, [Deleted(id=id)])
1429
1430def owned(s: State, p: Proposal, id: Id, evs: list[Event]) -> Result[list[Event], Rejection]:
1431 match map_get(s.todos, id):
1432 case Some(value):
1433 if value.owner != p.session.actor:
1434 return Err(error=NotOwner)
1435 return Ok(value=evs)
1436 case None:
1437 return Err(error=NoSuchTodo)
1438
1439def mine(s: State, session: Session) -> list[Todo]:
1440 return sort_by(filter_list(map_values(s.todos), lambda t: t.owner == session.actor), lambda t: t.text)
1441
1442def remaining_of(todos: list[Todo]) -> Int:
1443 return list_len(filter_list(todos, lambda t: not t.done))
1444
1445def view(s: State, session: Session) -> Html:
1446 todos = mine(s, session)
1447 return render(todos, remaining_of(todos))
1448
1449def render(todos: list[Todo], remaining: Int) -> Html:
1450 return ui:
1451 main:
1452 h1: "todos"
1453 ul:
1454 for t in todos:
1455 li(key=t.id, class=done_class(t)):
1456 span(on_click=Toggle(id=t.id)): t.text
1457 footer: (str(remaining) + " remaining")
1458
1459def done_class(t: Todo) -> Str:
1460 return "done" if t.done else ""
1461
1462@on(server)
1463proposals: Stream[Proposal] = merge_clients()
1464
1465@on(server)
1466events: Stream[Event] = decide(proposals, todos, validate)
1467
1468@on(data)
1469todos: Signal[State] = durable(fold(apply_event, State(todos={}), events))
1470
1471@on(client)
1472page: Signal[Html] = per_session(todos, view)
1473"#;
1474
1475 #[test]
1476 fn the_sketch_compiles_and_slices_into_roles() {
1477 let (placed, d, map) = compile_str("todo.beck", TODO);
1478 assert!(!d.has_errors(), "{}", d.render(&map));
1479 let placed = placed.expect("splitting succeeds");
1480 assert_eq!(placed.roles.state_name.as_ref(), "todos");
1481 assert_eq!(placed.roles.events_name.as_ref(), "events");
1482 assert_eq!(placed.roles.page_name.as_ref(), "page");
1483 assert!(placed.roles.view_is_per_session);
1484 assert_eq!(placed.roles.event_ty.con_name(), Some("Event"));
1485 assert_eq!(placed.roles.command_ty.con_name(), Some("Command"));
1486 assert_eq!(placed.wire_id.len(), 16);
1487 assert!(!placed.roles.is_fused());
1490 assert_eq!(placed.roles.states.len(), 1);
1491 assert_eq!(placed.roles.state_ty.con_name(), Some("State"));
1492 }
1493
1494 #[test]
1495 fn the_graph_holds_the_fold_as_its_own_vertex() {
1496 let (placed, _, _) = compile_str("todo.beck", TODO);
1500 let g = &placed.expect("placed").graph;
1501 assert_eq!(g.states().len(), 1);
1502 let durable = g.states()[0];
1503 let inner = g.node(durable).inputs[0];
1504 assert!(matches!(g.node(inner).op, Op::Fold { .. }));
1505 assert_eq!(g.label(inner), "todos·fold");
1506 assert_eq!(
1507 g.node(inner).name,
1508 None,
1509 "an inner vertex has no written name"
1510 );
1511 }
1512
1513 #[test]
1514 fn the_only_cycle_is_the_one_the_design_says_is_sound() {
1515 let (placed, _, _) = compile_str("todo.beck", TODO);
1518 let g = &placed.expect("placed").graph;
1519 let cycles: Vec<Vec<String>> = g
1520 .dep
1521 .cycles()
1522 .map(|c| {
1523 c.iter()
1524 .map(|n| g.label(n.0 as usize).to_string())
1525 .collect()
1526 })
1527 .collect();
1528 assert_eq!(cycles.len(), 1, "{cycles:?}");
1529 assert!(cycles[0].iter().any(|n| n == "events"));
1530 assert!(cycles[0].iter().any(|n| n == "todos"));
1531 assert!(cycles[0].iter().any(|n| n == "todos·fold"));
1532 }
1533
1534 #[test]
1535 fn the_wire_id_is_content_derived_and_stable_under_a_body_edit() {
1536 let (a, _, _) = compile_str("todo.beck", TODO);
1537 let edited = TODO.replace(
1539 r#""done" if t.done else """#,
1540 r#""done" if t.done else " ""#,
1541 );
1542 let (b, d, map) = compile_str("todo.beck", &edited);
1543 assert!(!d.has_errors(), "{}", d.render(&map));
1544 assert_eq!(
1545 a.expect("a").wire_id,
1546 b.expect("b").wire_id,
1547 "a body edit must not change the wire id"
1548 );
1549 }
1550
1551 #[test]
1552 fn the_wire_id_moves_when_the_wire_actually_changes() {
1553 let (a, _, _) = compile_str("todo.beck", TODO);
1557 let changed = TODO
1558 .replace(
1559 " Toggled(id: Id)\n Deleted(id: Id)",
1560 " Toggled(id: Id)\n Deleted(id: Id)\n Starred(id: Id)",
1561 )
1562 .replace(
1563 " case Deleted(id):\n return s.with(todos=map_remove(s.todos, id))",
1564 " case Deleted(id):\n return s.with(todos=map_remove(s.todos, id))\n case Starred(id):\n return toggle(s, id)",
1565 );
1566 let (b, d, map) = compile_str("todo.beck", &changed);
1567 assert!(!d.has_errors(), "{}", d.render(&map));
1568 assert_ne!(a.expect("a").wire_id, b.expect("b").wire_id);
1569
1570 let widened = TODO
1573 .replace(
1574 " Toggle(id: Id)\n Delete(id: Id)",
1575 " Toggle(id: Id, at: Int)\n Delete(id: Id)",
1576 )
1577 .replace("case Toggle(id):", "case Toggle(id, at):")
1578 .replace(
1579 "span(on_click=Toggle(id=t.id)): t.text",
1580 "span(on_click=Toggle(id=t.id, at=0)): t.text",
1581 );
1582 let (c, d, map) = compile_str("todo.beck", &widened);
1583 assert!(!d.has_errors(), "{}", d.render(&map));
1584 let (a, _, _) = compile_str("todo.beck", TODO);
1585 assert_ne!(a.expect("a").wire_id, c.expect("c").wire_id);
1586 }
1587
1588 #[test]
1589 fn a_program_with_no_merge_point_is_told_what_is_missing() {
1590 let (_, d, _) = compile_str("t.beck", "def f() -> Int:\n return 1\n");
1591 assert!(d.iter().any(|x| x.code == "B0500" && x.fix.is_some()));
1592 }
1593
1594 #[test]
1595 fn a_view_that_reads_a_stream_is_refused_by_name() {
1596 let src = TODO
1599 .replace(
1600 "@on(client)\npage: Signal[Html] = per_session(todos, view)",
1601 "@on(client)\npage: Signal[Html] = signal_map(events, render_ev)",
1602 )
1603 .replace(
1604 "@on(server)\nproposals",
1605 "def render_ev(e: Event) -> Html:\n return ui:\n main: \"x\"\n\n@on(server)\nproposals",
1606 );
1607 let (placed, d, _) = compile_str("t.beck", &src);
1608 assert!(placed.is_none());
1609 assert!(d.has_errors(), "a refusal must say why");
1610 }
1611
1612 #[test]
1613 fn a_cycle_with_no_fold_in_it_is_refused_rather_than_looped_on() {
1614 let src = TODO.replace(
1616 "@on(data)\ntodos: Signal[State] = durable(fold(apply_event, State(todos={}), events))",
1617 "@on(data)\ntodos: Signal[State] = durable(fold(apply_event, State(todos={}), events))\n\
1618 \nloop_a: Signal[State] = signal_map(loop_b, identity_state)\n\
1619 \nloop_b: Signal[State] = signal_map(loop_a, identity_state)",
1620 );
1621 let src = src.replace(
1622 "def done_class",
1623 "def identity_state(s: State) -> State:\n return s\n\ndef done_class",
1624 );
1625 let (placed, d, _) = compile_str("t.beck", &src);
1626 assert!(placed.is_none(), "a self-defined signal has no first value");
1627 assert!(
1628 d.iter().any(|x| x.code == "B0509"),
1629 "{:?}",
1630 d.iter().map(|x| x.code).collect::<Vec<_>>()
1631 );
1632 }
1633}