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(6, 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 awareness: None,
141 view_reads_freshness: false,
142 gestures: None,
143 };
144 let render = crate::render::Decision::of(&roles, &program.defs, false, None, span);
145 Placed {
146 program,
147 wire_id,
148 placement: crate::place::Solution {
149 tiers: Default::default(),
150 explanations: Vec::new(),
151 method: crate::place::Method::Exhaustive,
152 total: 0,
153 churn: Vec::new(),
154 ties: Vec::new(),
155 },
156 render,
157 roles,
158 graph,
159 kind: Kind::Library,
160 }
161 }
162}
163
164#[derive(Clone, Debug)]
166pub struct StateRole {
167 pub name: Arc<str>,
168 pub ty: Ty,
169 pub field: Option<Arc<str>>,
172 pub node: SigId,
173}
174
175#[derive(Clone, Debug)]
182pub struct Roles {
183 pub validate: Core,
185 pub fold: Core,
187 pub init: Core,
189 pub view: Core,
196 pub state_ty: Ty,
197 pub event_ty: Ty,
198 pub command_ty: Ty,
199 pub proposals_name: Arc<str>,
201 pub events_name: Arc<str>,
202 pub state_name: Arc<str>,
203 pub page_name: Arc<str>,
204 pub inlined: Vec<Arc<str>>,
206 pub shared: Vec<Arc<str>>,
209 pub states: Vec<StateRole>,
212 pub view_is_per_session: bool,
213 pub view_reads_presence: bool,
215 pub awareness: Option<Core>,
222 pub view_reads_freshness: bool,
225 pub gestures: Option<GestureRole>,
234}
235
236fn gesture_ty(step: &Core) -> Ty {
243 match &step.ty {
244 Ty::Fun(params, _, _) if params.len() == 2 => params[1].clone(),
245 _ => Ty::unit(),
246 }
247}
248
249#[derive(Clone, Debug)]
251pub struct GestureRole {
252 pub step: Core,
255 pub init: Core,
259 pub ty: Ty,
261 pub gesture_ty: Ty,
263}
264
265impl Roles {
266 pub fn is_fused(&self) -> bool {
268 self.states.len() > 1
269 }
270}
271
272pub fn split(mut program: Program, diags: &mut Diagnostics) -> Option<Placed> {
274 let graph = Graph::build(&program, diags)?;
275
276 let ingress = graph.ingress();
279 let Some(&proposals) = ingress.first() else {
280 diags.push(
281 Diagnostic::error("B0500", "this program has no merge point", Span::NONE)
282 .with_note(
283 "a Beck application is a fold over an event stream, and the stream starts at \
284 `merge_clients()` — the one place time enters",
285 )
286 .with_fix("add `@on(server)` and `proposals: Stream[Proposal] = merge_clients()`"),
287 );
288 return None;
289 };
290
291 let states = graph.states();
292 if states.is_empty() {
293 if let Some(fold) = graph
298 .find(|o| matches!(o, Op::Fold { .. }))
299 .into_iter()
300 .find(|&f| {
301 graph
302 .consumers(f)
303 .iter()
304 .all(|&c| !matches!(graph.node(c).op, Op::Durable))
305 })
306 {
307 diags.push(
308 Diagnostic::error(
309 "B0519",
310 format!(
311 "`{}` folds the log's own stream, so it has to be `durable`",
312 graph.label(fold)
313 ),
314 graph.node(fold).span,
315 )
316 .with_primary_label(
317 "a fold over the log's stream, with nowhere to keep what it folds",
318 )
319 .with_note(
320 "this stream is the log's, so its accumulator *is* a function of the log \
321 whatever it is called — every event on it was validated and recorded, and \
322 replay would reproduce this state whether or not the program asked for it. \
323 D30's rule is that ephemerality comes from the stream: a fold that should not \
324 survive a restart folds gestures, which are never recorded, rather than \
325 declining to persist events that were",
326 )
327 .with_fix(
328 "`durable(fold(…))` if this is state the log should reproduce; \
329 `gestures(step, init)` if it is interface state one client keeps to itself",
330 ),
331 );
332 return None;
333 }
334 diags.push(
335 Diagnostic::error("B0501", "this program has no durable state", Span::NONE)
336 .with_note("`durable(fold(f, init, s))` is what makes the log a database")
337 .with_fix("wrap the fold: `@on(data)` and `durable(fold(apply_event, …, events))`"),
338 );
339 return None;
340 }
341
342 let mut folds: Vec<(SigId, SigId)> = Vec::new(); for &s in &states {
345 let inner = follow_alias(&graph, graph.node(s).inputs[0]);
346 if !matches!(graph.node(inner).op, Op::Fold { .. }) {
347 diags.push(
348 Diagnostic::error("B0502", "`durable` must wrap a `fold`", graph.node(s).span)
349 .with_primary_label("only a fold has an accumulator to persist")
350 .with_label(
351 graph.node(inner).span,
352 format!("this is a `{}`", graph.node(inner).op.name()),
353 ),
354 );
355 return None;
356 }
357 folds.push((s, inner));
358 }
359
360 let decides = graph.decides();
363 let Some(&decide) = decides.first() else {
364 diags.push(
365 Diagnostic::error(
366 "B0504",
367 "events must come from `decide`",
368 graph.node(folds[0].1).span,
369 )
370 .with_primary_label("this fold has no chokepoint upstream of it")
371 .with_note(
372 "`decide` is the sole consumer of ingress and the one place a command becomes \
373 an event — §3.5's \"authority is one chokepoint\"",
374 ),
375 );
376 return None;
377 };
378 if decides.len() > 1 {
379 diags.push(
380 Diagnostic::error(
381 "B0511",
382 "a program has one authority chokepoint",
383 graph.node(decides[1]).span,
384 )
385 .with_primary_label("a second `decide`")
386 .with_label(graph.node(decide).span, "the first one is here")
387 .with_note(
388 "§3.5 rests on validation being one place: two of them are two answers to \"may \
389 this actor do this\", and the log would record whichever ran",
390 ),
391 );
392 return None;
393 }
394
395 if let Some(&here) = graph
399 .presences()
400 .iter()
401 .find(|&&p| reaches(&graph, decide, p))
402 {
403 diags.push(
404 Diagnostic::error(
405 "B0515",
406 "the chokepoint reads `presence`, which is not in the log",
407 graph.node(decide).span,
408 )
409 .with_primary_label(format!(
410 "`{}` decides from `{}`",
411 graph.label(decide),
412 graph.label(here)
413 ))
414 .with_label(graph.node(here).span, "who is connected is decided here")
415 .with_note(
416 "an event is what a replay reproduces, and who was connected when it was recorded \
417 is not written down anywhere. A `validate` that read the roster would decide one \
418 thing today and another on replay, and the log would no longer be the whole \
419 history",
420 )
421 .with_fix(
422 "record the fact instead: propose a command when a client arrives, and decide from \
423 the state that fold produces",
424 ),
425 );
426 return None;
427 }
428
429 if let Some(&aware) = graph
433 .awarenesses()
434 .iter()
435 .find(|&&a| reaches(&graph, decide, a))
436 {
437 diags.push(
438 Diagnostic::error(
439 "B0520",
440 "the chokepoint reads `awareness`, which is not in the log",
441 graph.node(decide).span,
442 )
443 .with_primary_label(format!(
444 "`{}` decides from `{}`",
445 graph.label(decide),
446 graph.label(aware)
447 ))
448 .with_label(
449 graph.node(aware).span,
450 "what everybody is doing is decided here",
451 )
452 .with_note(
453 "an event is what a replay reproduces, and what each connection was contributing \
454 when it was recorded is not written down anywhere. A `validate` that read the \
455 roster would decide one thing today and another on replay, and the log would no \
456 longer be the whole history",
457 )
458 .with_fix(
459 "record the fact instead: propose a command when the thing you are deciding from \
460 happens, and decide from the state that fold produces",
461 ),
462 );
463 return None;
464 }
465
466 if let Some(&g) = graph
472 .gestures()
473 .iter()
474 .find(|&&g| reaches(&graph, decide, g))
475 {
476 diags.push(
477 Diagnostic::error(
478 "B0523",
479 "the chokepoint reads a `gestures` fold, which is not in the log",
480 graph.node(decide).span,
481 )
482 .with_primary_label(format!(
483 "`{}` decides from `{}`",
484 graph.label(decide),
485 graph.label(g)
486 ))
487 .with_label(
488 graph.node(g).span,
489 "interface state one client keeps to itself",
490 )
491 .with_note(
492 "a gesture is not proposed, not validated and not recorded — it never leaves the \
493 client that made it. An event whose existence depended on one could not be \
494 replayed, because there is nothing to replay: the log holds no trace that the \
495 gesture happened",
496 )
497 .with_fix(
498 "if this interface state should decide an event, it is not interface state — propose \
499 a command when it changes and decide from the fold over the events that produces \
500 (`docs/10` D30's fifth home)",
501 ),
502 );
503 return None;
504 }
505
506 if let Some(&how) = graph
510 .freshnesses()
511 .iter()
512 .find(|&&f| reaches(&graph, decide, f))
513 {
514 diags.push(
515 Diagnostic::error(
516 "B0517",
517 "the chokepoint reads `freshness`, which is not in the log",
518 graph.node(decide).span,
519 )
520 .with_primary_label(format!(
521 "`{}` decides from `{}`",
522 graph.label(decide),
523 graph.label(how)
524 ))
525 .with_label(
526 graph.node(how).span,
527 "whether a guess is outstanding is decided here",
528 )
529 .with_note(
530 "how many of a client's commands were in flight when an event was recorded is \
531 written down nowhere, and on replay nothing is in flight at all. A `validate` \
532 that read it would accept a command today and refuse it on the way back",
533 )
534 .with_fix(
535 "decide from the accumulator: what has actually been recorded is the fold's job to \
536 say, and it is the same answer now and on replay",
537 ),
538 );
539 return None;
540 }
541
542 let mut fold_filters: Vec<Option<Core>> = Vec::new();
545 for &(_, f) in &folds {
546 let mut node = follow_alias(&graph, graph.node(f).inputs[0]);
547 let mut filter = None;
548 if let Op::FilterMap { f: pred } = &graph.node(node).op {
549 filter = Some(pred.clone());
550 node = follow_alias(&graph, graph.node(node).inputs[0]);
551 }
552 if node != decide {
553 diags.push(
554 Diagnostic::error(
555 "B0504",
556 "events must come from `decide`",
557 graph.node(f).span,
558 )
559 .with_primary_label(format!(
560 "this fold reads `{}`",
561 graph.label(graph.node(f).inputs[0])
562 ))
563 .with_note(
564 "the log holds what the chokepoint decided, so a fold reads `decide` — \
565 optionally through one `filter_map`, which is how two folds take \
566 different slices of one stream",
567 ),
568 );
569 return None;
570 }
571 fold_filters.push(filter);
572 }
573
574 let pages: Vec<SigId> = graph
577 .sinks
578 .iter()
579 .copied()
580 .filter(|&s| graph.node(s).tier == Tier::Client && is_html(&graph.node(s).ty))
581 .collect();
582 let Some(&page) = pages.first() else {
583 diags.push(
584 Diagnostic::error("B0505", "no signal is placed on the client", Span::NONE)
585 .with_note(
586 "`page` is the tier crossing: a `Signal[Html]` the browser subscribes to",
587 )
588 .with_fix("add `@on(client)` and `page: Signal[Html] = per_session(todos, view)`"),
589 );
590 return None;
591 };
592 if pages.len() > 1 {
593 diags.push(
594 Diagnostic::error(
595 "B0510",
596 "two signals are the page, and there is no router yet",
597 graph.node(pages[1]).span,
598 )
599 .with_primary_label(format!("`{}`", graph.label(pages[1])))
600 .with_label(graph.node(page).span, format!("`{}`", graph.label(page)))
601 .with_note(
602 "the slicer will slice both; the runtime serves one document per connection, and \
603 choosing between them is routing — a Phase 3 client bullet that is not built",
604 )
605 .with_fix("combine them in one view, or read one from the other"),
606 );
607 return None;
608 }
609
610 let fused = states.len() > 1;
613 let mut vars = Vars(max_var(&program));
614 let state_var = vars.fresh();
615 let session_var = vars.fresh();
616 let presence_var = vars.fresh();
617 let awareness_var = vars.fresh();
618 let freshness_var = vars.fresh();
619 let gestures_var = vars.fresh();
620
621 let state_roles: Vec<StateRole> = folds
622 .iter()
623 .map(|&(d, _)| {
624 let n = graph.node(d);
625 StateRole {
626 name: n.label.clone(),
627 ty: signal_elem(&n.ty),
628 field: fused.then(|| n.label.clone()),
629 node: d,
630 }
631 })
632 .collect();
633
634 let mut slicer = Slicer {
635 graph: &graph,
636 states: &state_roles,
637 state_var,
638 session_var,
639 presence_var,
640 awareness_var,
641 freshness_var,
642 gestures_var,
643 bound: BTreeMap::new(),
644 lets: Vec::new(),
645 inlined: Vec::new(),
646 shared: Vec::new(),
647 per_session: false,
648 reads_presence: false,
649 awareness: None,
650 reads_freshness: false,
651 gestures: None,
652 vars: &mut vars,
653 diags,
654 };
655 let view_body = slicer.lower_sink(page)?;
656 let view_body = slicer.wrap(view_body);
657 let inlined = slicer.inlined.clone();
658 let shared = slicer.shared.clone();
659 let per_session = slicer.per_session;
660 let reads_presence = slicer.reads_presence;
661 let awareness = slicer.awareness.clone();
662 let reads_freshness = slicer.reads_freshness;
663 let gestures = slicer.gestures.clone();
664
665 let state_ty = if fused {
666 Ty::con(FUSED_STATE)
667 } else {
668 state_roles[0].ty.clone()
669 };
670
671 let view = Core {
672 kind: CoreKind::Lam {
673 params: vec![
674 state_var,
675 session_var,
676 presence_var,
677 awareness_var,
678 freshness_var,
679 gestures_var,
680 ]
681 .into(),
682 body: Arc::new(view_body),
683 },
684 ty: Ty::fun(
685 vec![
686 state_ty.clone(),
687 Ty::con("Session"),
688 Ty::map(Ty::str_(), Ty::int()),
689 awareness
693 .as_ref()
694 .map(|(_, ty)| ty.clone())
695 .unwrap_or_else(|| Ty::map(Ty::str_(), Ty::unit())),
696 Ty::con("Freshness"),
697 gestures
700 .as_ref()
701 .map(|g| g.ty.clone())
702 .unwrap_or_else(Ty::unit),
703 ],
704 Ty::html(),
705 ),
706 tier: Tier::Client,
707 span: graph.node(page).span,
708 last_use: false,
709 order: crate::fields::UNORDERED,
710 locals: 0,
711 };
712
713 let (fold, init) = if fused {
716 program.types.insert(
717 Arc::from(FUSED_STATE),
718 crate::signal::fused_state_decl(
719 &state_roles
720 .iter()
721 .map(|s| (s.name.clone(), s.ty.clone()))
722 .collect::<Vec<_>>(),
723 ),
724 );
725 fuse(
726 &graph,
727 &folds,
728 &fold_filters,
729 &state_roles,
730 &state_ty,
731 &mut vars,
732 graph.node(page).span,
733 )
734 } else {
735 let Op::Fold { step, init } = &graph.node(folds[0].1).op else {
736 return None;
737 };
738 match &fold_filters[0] {
739 None => (step.clone(), init.clone()),
740 Some(pred) => (
741 filtered_step(
742 step,
743 pred,
744 &state_ty,
745 &mut vars,
746 graph.node(folds[0].1).span,
747 ),
748 init.clone(),
749 ),
750 }
751 };
752
753 let Op::Decide { validate } = &graph.node(decide).op else {
756 return None;
757 };
758 let validate = if fused {
759 let src = follow_alias(&graph, graph.node(decide).inputs[1]);
760 let Some(role) = state_roles.iter().find(|s| s.node == src) else {
761 diags.push(
762 Diagnostic::error(
763 "B0512",
764 "the chokepoint does not read a durable fold",
765 graph.node(decide).span,
766 )
767 .with_primary_label(format!("it reads `{}`", graph.label(src)))
768 .with_note(
769 "`decide` threads the accumulator through validation, so what it reads has to \
770 be one — that is what makes first-writer-wins and ownership decidable (§3.7)",
771 ),
772 );
773 return None;
774 };
775 let p = vars.fresh();
776 let s = vars.fresh();
777 let span = graph.node(decide).span;
778 Core {
779 kind: CoreKind::Lam {
780 params: vec![s, p].into(),
781 body: Arc::new(Core {
782 kind: CoreKind::App {
783 func: Box::new(validate.clone()),
784 args: vec![
785 field(var(s, state_ty.clone(), span), role, span),
786 var(p, Ty::con("Proposal"), span),
787 ],
788 },
789 ty: Ty::unit(),
790 tier: Tier::Server,
791 span,
792 last_use: false,
793 order: crate::fields::UNORDERED,
794 locals: 0,
795 }),
796 },
797 ty: Ty::unit(),
798 tier: Tier::Server,
799 span,
800 last_use: false,
801 order: crate::fields::UNORDERED,
802 locals: 0,
803 }
804 } else {
805 validate.clone()
806 };
807
808 let event_ty = signal_elem(&graph.node(decide).ty);
809 let command_ty = program
810 .types
811 .get("Command")
812 .map(|_| Ty::con("Command"))
813 .unwrap_or_else(Ty::unit);
814
815 if let Some(g) = graph.gestures().first() {
821 let gesture_ty = match &graph.node(*g).op {
822 Op::Gestures { step, .. } => gesture_ty(step),
823 _ => unreachable!("found by op"),
824 };
825 let names = |ty: &Ty| -> Vec<Arc<str>> {
826 match ty.con_name().and_then(|n| program.types.get(n)) {
827 Some(crate::ty::TyDecl::Union { variants, .. }) => {
828 variants.iter().map(|v| v.name.clone()).collect()
829 }
830 _ => Vec::new(),
831 }
832 };
833 let commands = names(&command_ty);
834 if let Some(clash) = names(&gesture_ty)
835 .into_iter()
836 .find(|n| commands.contains(n))
837 {
838 diags.push(
839 Diagnostic::error(
840 "B0524",
841 format!("`{clash}` is both a command and a gesture"),
842 graph.node(*g).span,
843 )
844 .with_primary_label(format!(
845 "`{}` and `{}` share this variant",
846 gesture_ty.con_name().unwrap_or("the gesture union"),
847 command_ty.con_name().unwrap_or("the command union"),
848 ))
849 .with_note(
850 "a handler in the page carries the constructor it builds, and the client routes \
851 on its name: a gesture is folded where it was made and a command is proposed to \
852 the server. A name that is both would make `on_click` mean whichever the client \
853 tried first",
854 )
855 .with_fix("rename one of them — they are different things happening"),
856 );
857 return None;
858 }
859 }
860
861 let mut hasher = blake3::Hasher::new();
868 hasher.update(program.name.as_bytes());
869 for t in [&command_ty, &event_ty, &state_ty] {
870 hasher.update(crate::iface::structural(t, &program.types).as_bytes());
871 hasher.update(b"\x00");
872 }
873 let wire_id = hasher.finalize().to_hex()[..16].to_string();
874
875 let roles = Roles {
876 validate,
877 fold,
878 init,
879 view,
880 state_ty,
881 event_ty,
882 command_ty,
883 proposals_name: graph.node(proposals).label.clone(),
884 events_name: graph.node(decide).label.clone(),
885 state_name: state_roles[0].name.clone(),
886 page_name: graph.node(page).label.clone(),
887 inlined,
888 shared,
889 states: state_roles,
890 view_is_per_session: per_session,
891 view_reads_presence: reads_presence,
892 awareness: awareness.map(|(f, _)| f),
893 view_reads_freshness: reads_freshness,
894 gestures,
895 };
896
897 let declared = program
902 .signals
903 .iter()
904 .find(|s| s.name == graph.node(page).label)
905 .and_then(|s| s.render);
906 let render =
907 crate::render::Decision::of(&roles, &program.defs, true, declared, graph.node(page).span);
908 render.refuse(diags);
909 if diags.has_errors() {
910 return None;
911 }
912
913 Some(Placed {
914 kind: Kind::Application,
915 placement: crate::place::Solution {
916 tiers: Default::default(),
917 explanations: Vec::new(),
918 method: crate::place::Method::Exhaustive,
919 total: 0,
920 churn: Vec::new(),
921 ties: Vec::new(),
922 },
923 render,
924 roles,
925 wire_id,
926 program,
927 graph,
928 })
929}
930
931fn is_html(t: &Ty) -> bool {
932 signal_elem(t).con_name() == Some(Ty::HTML)
933}
934
935fn reaches(graph: &Graph, from: SigId, target: SigId) -> bool {
940 let mut seen = BTreeSet::new();
941 let mut stack = vec![from];
942 while let Some(id) = stack.pop() {
943 if id == target {
944 return true;
945 }
946 if !seen.insert(id) {
947 continue;
948 }
949 stack.extend(graph.node(id).inputs.iter().copied());
950 }
951 false
952}
953
954fn follow_alias(graph: &Graph, mut id: SigId) -> SigId {
956 let mut guard = 0;
957 while matches!(graph.node(id).op, Op::Alias) && guard < graph.nodes.len() {
958 id = graph.node(id).inputs[0];
959 guard += 1;
960 }
961 id
962}
963
964fn var(v: VarId, ty: Ty, span: Span) -> Core {
965 Core {
966 kind: CoreKind::Var(v),
967 ty,
968 tier: Tier::Any,
969 span,
970 last_use: false,
971 order: crate::fields::UNORDERED,
972 locals: 0,
973 }
974}
975
976fn field(base: Core, role: &StateRole, span: Span) -> Core {
978 match &role.field {
979 None => base,
980 Some(f) => Core {
981 kind: CoreKind::Field {
982 base: Box::new(base),
983 name: f.clone(),
984 },
985 ty: role.ty.clone(),
986 tier: Tier::Any,
987 span,
988 last_use: false,
989 order: crate::fields::UNORDERED,
990 locals: 0,
991 },
992 }
993}
994
995fn call(func: Core, args: Vec<Core>, ty: Ty, span: Span) -> Core {
997 Core {
998 kind: CoreKind::App {
999 func: Box::new(func),
1000 args,
1001 },
1002 ty,
1003 tier: Tier::Any,
1004 span,
1005 last_use: false,
1006 order: crate::fields::UNORDERED,
1007 locals: 0,
1008 }
1009}
1010
1011fn fold_field(
1014 step: &Core,
1015 filter: &Option<Core>,
1016 acc: Core,
1017 env: Core,
1018 ty: &Ty,
1019 vars: &mut Vars,
1020 span: Span,
1021) -> Core {
1022 let applied = call(
1023 step.clone(),
1024 vec![acc.clone(), env.clone()],
1025 ty.clone(),
1026 span,
1027 );
1028 let Some(pred) = filter else {
1029 return applied;
1030 };
1031 let o = vars.fresh();
1040 let opt_ty = Ty::option(Ty::unit());
1041 let body = Core {
1042 kind: CoreKind::Field {
1043 base: Box::new(env.clone()),
1044 name: Arc::from("body"),
1045 },
1046 ty: Ty::unit(),
1047 tier: Tier::Any,
1048 span,
1049 last_use: false,
1050 order: crate::fields::UNORDERED,
1051 locals: 0,
1052 };
1053 let inner = Core {
1054 kind: CoreKind::Field {
1055 base: Box::new(var(o, opt_ty.clone(), span)),
1056 name: Arc::from("value"),
1057 },
1058 ty: Ty::unit(),
1059 tier: Tier::Any,
1060 span,
1061 last_use: false,
1062 order: crate::fields::UNORDERED,
1063 locals: 0,
1064 };
1065 let narrowed = Core {
1066 kind: CoreKind::With {
1067 base: Box::new(env),
1068 fields: vec![(Arc::from("body"), inner)],
1069 },
1070 ty: Ty::unit(),
1071 tier: Tier::Any,
1072 span,
1073 last_use: false,
1074 order: crate::fields::UNORDERED,
1075 locals: 0,
1076 };
1077 Core {
1078 kind: CoreKind::Let {
1079 var: o,
1080 value: Box::new(call(pred.clone(), vec![body], opt_ty.clone(), span)),
1081 body: Box::new(Core {
1082 kind: CoreKind::If {
1083 cond: Box::new(Core {
1084 kind: CoreKind::Prim {
1085 op: Prim::OptionIsSome,
1086 args: vec![var(o, opt_ty, span)],
1087 },
1088 ty: Ty::bool_(),
1089 tier: Tier::Any,
1090 span,
1091 last_use: false,
1092 order: crate::fields::UNORDERED,
1093 locals: 0,
1094 }),
1095 then: Box::new(call(
1096 step.clone(),
1097 vec![acc.clone(), narrowed],
1098 ty.clone(),
1099 span,
1100 )),
1101 alt: Box::new(acc),
1102 },
1103 ty: ty.clone(),
1104 tier: Tier::Any,
1105 span,
1106 last_use: false,
1107 order: crate::fields::UNORDERED,
1108 locals: 0,
1109 }),
1110 },
1111 ty: ty.clone(),
1112 tier: Tier::Any,
1113 span,
1114 last_use: false,
1115 order: crate::fields::UNORDERED,
1116 locals: 0,
1117 }
1118}
1119
1120fn filtered_step(step: &Core, pred: &Core, state_ty: &Ty, vars: &mut Vars, span: Span) -> Core {
1122 let s = vars.fresh();
1123 let e = vars.fresh();
1124 let body = fold_field(
1125 step,
1126 &Some(pred.clone()),
1127 var(s, state_ty.clone(), span),
1128 var(e, Ty::unit(), span),
1129 state_ty,
1130 vars,
1131 span,
1132 );
1133 Core {
1134 kind: CoreKind::Lam {
1135 params: vec![s, e].into(),
1136 body: Arc::new(body),
1137 },
1138 ty: Ty::fun(vec![state_ty.clone(), Ty::unit()], state_ty.clone()),
1139 tier: Tier::Data,
1140 span,
1141 last_use: false,
1142 order: crate::fields::UNORDERED,
1143 locals: 0,
1144 }
1145}
1146
1147fn fuse(
1154 graph: &Graph,
1155 folds: &[(SigId, SigId)],
1156 filters: &[Option<Core>],
1157 roles: &[StateRole],
1158 state_ty: &Ty,
1159 vars: &mut Vars,
1160 span: Span,
1161) -> (Core, Core) {
1162 let s = vars.fresh();
1163 let e = vars.fresh();
1164
1165 let mut step_fields = Vec::new();
1166 let mut init_fields = Vec::new();
1167 for (i, &(_, f)) in folds.iter().enumerate() {
1168 let Op::Fold { step, init } = &graph.node(f).op else {
1169 continue;
1170 };
1171 let role = &roles[i];
1172 let acc = field(var(s, state_ty.clone(), span), role, span);
1173 step_fields.push((
1174 role.name.clone(),
1175 fold_field(
1176 step,
1177 &filters[i],
1178 acc,
1179 var(e, Ty::unit(), span),
1180 &role.ty,
1181 vars,
1182 span,
1183 ),
1184 ));
1185 init_fields.push((role.name.clone(), init.clone()));
1186 }
1187
1188 let make = |fields: Vec<(Arc<str>, Core)>| {
1191 let mut c = Core {
1192 kind: CoreKind::Make {
1193 ty: Arc::from(FUSED_STATE),
1194 variant: None,
1195 fields,
1196 },
1197 ty: state_ty.clone(),
1198 tier: Tier::Data,
1199 span,
1200 last_use: false,
1201 order: crate::fields::UNORDERED,
1202 locals: 0,
1203 };
1204 crate::fields::order_here(&mut c);
1205 c
1206 };
1207
1208 (
1209 Core {
1210 kind: CoreKind::Lam {
1211 params: vec![s, e].into(),
1212 body: Arc::new(make(step_fields)),
1213 },
1214 ty: Ty::fun(vec![state_ty.clone(), Ty::unit()], state_ty.clone()),
1215 tier: Tier::Data,
1216 span,
1217 last_use: false,
1218 order: crate::fields::UNORDERED,
1219 locals: 0,
1220 },
1221 make(init_fields),
1222 )
1223}
1224
1225struct Vars(VarId);
1232
1233impl Vars {
1234 fn fresh(&mut self) -> VarId {
1235 self.0 += 1;
1236 self.0
1237 }
1238}
1239
1240fn max_var(program: &Program) -> VarId {
1242 fn go(c: &Core, max: &mut VarId) {
1243 match &c.kind {
1244 CoreKind::Const(_) | CoreKind::Global(_) => {}
1245 CoreKind::Var(v) => *max = (*max).max(*v),
1246 CoreKind::Lam { params, body } => {
1247 for p in params.iter() {
1248 *max = (*max).max(*p);
1249 }
1250 go(body, max);
1251 }
1252 CoreKind::App { func, args } => {
1253 go(func, max);
1254 args.iter().for_each(|a| go(a, max));
1255 }
1256 CoreKind::Prim { args, .. } => args.iter().for_each(|a| go(a, max)),
1257 CoreKind::Let { var, value, body } => {
1258 *max = (*max).max(*var);
1259 go(value, max);
1260 go(body, max);
1261 }
1262 CoreKind::If { cond, then, alt } => {
1263 go(cond, max);
1264 go(then, max);
1265 go(alt, max);
1266 }
1267 CoreKind::Match { scrutinee, arms } => {
1268 go(scrutinee, max);
1269 for a in arms {
1270 for v in a.pattern.binders() {
1271 *max = (*max).max(v);
1272 }
1273 for e in a.exprs() {
1274 go(e, max);
1275 }
1276 }
1277 }
1278 CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, f)| go(f, max)),
1279 CoreKind::Field { base, .. } => go(base, max),
1280 CoreKind::With { base, fields } => {
1281 go(base, max);
1282 fields.iter().for_each(|(_, f)| go(f, max));
1283 }
1284 CoreKind::ListLit(items) => items.iter().for_each(|i| go(i, max)),
1285 CoreKind::MapLit(pairs) => pairs.iter().for_each(|(k, v)| {
1286 go(k, max);
1287 go(v, max);
1288 }),
1289 }
1290 }
1291 let mut max = 0;
1292 for d in program.defs.values() {
1293 go(&d.body, &mut max);
1294 }
1295 for s in &program.signals {
1296 go(&s.expr, &mut max);
1297 }
1298 for t in &program.tests {
1299 max = max
1300 .max(t.bindings.state)
1301 .max(t.bindings.events)
1302 .max(t.bindings.result);
1303 }
1304 max
1305}
1306
1307struct Slicer<'a, 'd> {
1320 graph: &'a Graph,
1321 states: &'a [StateRole],
1322 state_var: VarId,
1323 session_var: VarId,
1324 presence_var: VarId,
1325 awareness_var: VarId,
1326 freshness_var: VarId,
1327 gestures_var: VarId,
1328 vars: &'d mut Vars,
1329 bound: BTreeMap<SigId, VarId>,
1331 lets: Vec<(VarId, Core)>,
1333 inlined: Vec<Arc<str>>,
1334 shared: Vec<Arc<str>>,
1335 per_session: bool,
1336 reads_presence: bool,
1337 awareness: Option<(Core, Ty)>,
1339 reads_freshness: bool,
1340 gestures: Option<GestureRole>,
1341 diags: &'d mut Diagnostics,
1342}
1343
1344impl Slicer<'_, '_> {
1345 fn lower_sink(&mut self, id: SigId) -> Option<Core> {
1348 let body = self.lower(id)?;
1349 if let Some(name) = &self.graph.node(follow_alias(self.graph, id)).name {
1350 self.inlined.retain(|n| n != name);
1351 }
1352 Some(body)
1353 }
1354
1355 fn lower(&mut self, id: SigId) -> Option<Core> {
1356 let id = follow_alias(self.graph, id);
1357 let node = self.graph.node(id);
1358
1359 if let Some(role) = self.states.iter().find(|s| s.node == id) {
1362 return Some(field(
1363 var(self.state_var, Ty::unit(), node.span),
1364 role,
1365 node.span,
1366 ));
1367 }
1368 if matches!(node.op, Op::Presence) {
1371 self.reads_presence = true;
1372 return Some(var(self.presence_var, signal_elem(&node.ty), node.span));
1373 }
1374 if let Op::Awareness { f } = &node.op {
1378 let elem = signal_elem(&node.ty);
1379 self.awareness = Some((f.clone(), elem.clone()));
1380 return Some(var(self.awareness_var, elem, node.span));
1381 }
1382 if matches!(node.op, Op::Freshness) {
1385 self.reads_freshness = true;
1386 return Some(var(self.freshness_var, signal_elem(&node.ty), node.span));
1387 }
1388 if let Op::Gestures { step, init } = &node.op {
1394 let ty = signal_elem(&node.ty);
1395 self.gestures = Some(GestureRole {
1396 step: step.clone(),
1397 init: init.clone(),
1398 ty: ty.clone(),
1399 gesture_ty: gesture_ty(step),
1400 });
1401 return Some(var(self.gestures_var, ty, node.span));
1402 }
1403 if let Some(&v) = self.bound.get(&id) {
1404 return Some(var(v, signal_elem(&node.ty), node.span));
1405 }
1406
1407 let span = node.span;
1408 let ty = signal_elem(&node.ty);
1409 let body = match &node.op {
1410 Op::Map { f } => {
1411 let input = self.lower(node.inputs[0])?;
1412 call(f.clone(), vec![input], ty.clone(), span)
1413 }
1414 Op::Map2 { f } => {
1415 let a = self.lower(node.inputs[0])?;
1416 let b = self.lower(node.inputs[1])?;
1417 call(f.clone(), vec![a, b], ty.clone(), span)
1418 }
1419 Op::PerSession { f } => {
1420 self.per_session = true;
1421 let input = self.lower(node.inputs[0])?;
1422 let session = var(self.session_var, Ty::con("Session"), span);
1423 call(f.clone(), vec![input, session], ty.clone(), span)
1424 }
1425 Op::Fold { .. } => {
1426 self.diags.push(
1430 Diagnostic::error(
1431 "B0513",
1432 format!("`{}` is a fold that is not durable", self.graph.label(id)),
1433 span,
1434 )
1435 .with_primary_label("its accumulator has nowhere to live across a restart")
1436 .with_note(
1437 "the log is what survives, and `durable` is what says an accumulator is \
1438 folded from it — a fold outside one would be rebuilt from nothing on \
1439 every deploy",
1440 )
1441 .with_fix(
1442 "wrap it — `durable(fold(…))` — or, if this is interface state that should \
1443 not survive a restart, fold gestures instead: `gestures(step, init)` \
1444 (`docs/10` D30)",
1445 ),
1446 );
1447 return None;
1448 }
1449 Op::Ingress | Op::Decide { .. } | Op::FilterMap { .. } => {
1450 self.diags.push(
1451 Diagnostic::error(
1452 "B0507",
1453 format!(
1454 "a view cannot read `{}`, which is a stream",
1455 self.graph.label(id)
1456 ),
1457 span,
1458 )
1459 .with_primary_label(format!("`{}` produces occurrences", node.op.name()))
1460 .with_note(
1461 "§3.7: a `Stream` is discrete occurrences and a `Signal` is a value \
1462 defined at all times. A view renders a value, so it reads what a stream \
1463 was folded into",
1464 ),
1465 );
1466 return None;
1467 }
1468 Op::Durable
1469 | Op::Alias
1470 | Op::Presence
1471 | Op::Awareness { .. }
1472 | Op::Freshness
1473 | Op::Gestures { .. } => {
1474 unreachable!("handled above")
1475 }
1476 };
1477
1478 if let Some(name) = &node.name {
1479 if !self.inlined.contains(name) {
1480 self.inlined.push(name.clone());
1481 }
1482 }
1483
1484 if self.graph.consumers(id).len() > 1 {
1487 let v = self.vars.fresh();
1488 self.bound.insert(id, v);
1489 self.lets.push((v, body));
1490 if let Some(name) = &node.name {
1491 self.shared.push(name.clone());
1492 }
1493 return Some(var(v, ty, span));
1494 }
1495 Some(body)
1496 }
1497
1498 fn wrap(&self, body: Core) -> Core {
1500 self.lets.iter().rev().fold(body, |acc, (v, value)| Core {
1501 kind: CoreKind::Let {
1502 var: *v,
1503 value: Box::new(value.clone()),
1504 body: Box::new(acc.clone()),
1505 },
1506 ty: acc.ty.clone(),
1507 tier: Tier::Client,
1508 span: acc.span,
1509 last_use: false,
1510 order: crate::fields::UNORDERED,
1511 locals: 0,
1512 })
1513 }
1514}
1515
1516pub fn wire_report(placed: &Placed) -> String {
1521 use std::fmt::Write;
1522 let mut out = String::new();
1523 let _ = writeln!(out, "operation id {}", placed.wire_id);
1524 let _ = writeln!(out, "command {}", placed.roles.command_ty);
1525 let _ = writeln!(out, "event {}", placed.roles.event_ty);
1526 let _ = writeln!(out, "state {}", placed.roles.state_ty);
1527 let _ = writeln!(
1528 out,
1529 "\nthe id is content-derived from the module and those three types, so a body \
1530 edit does not move it and a signature change does."
1531 );
1532 out
1533}
1534
1535pub fn flow_report(placed: &Placed) -> String {
1543 use std::fmt::Write;
1544 let g = &placed.graph;
1545 let r = &placed.roles;
1546 let mut out = String::new();
1547 let cycles = g.dep.cycles().count();
1548 let _ = writeln!(
1549 out,
1550 "signal graph — {} vertices, {} {}, {} tier {}\n",
1551 g.nodes.len(),
1552 cycles,
1553 if cycles == 1 { "cycle" } else { "cycles" },
1554 g.cuts.len(),
1555 if g.cuts.len() == 1 {
1556 "crossing"
1557 } else {
1558 "crossings"
1559 },
1560 );
1561
1562 let in_cycle: BTreeSet<SigId> = g
1563 .dep
1564 .cycles()
1565 .flat_map(|c| c.iter().map(|n| n.0 as usize))
1566 .collect();
1567 let page = g.by_name.get(&r.page_name).copied();
1568 let rows: Vec<(SigId, String, String)> = g
1569 .order()
1570 .into_iter()
1571 .map(|id| {
1572 let n = g.node(id);
1573 let inputs: Vec<&str> = n.inputs.iter().map(|&i| g.label(i)).collect();
1574 (
1575 id,
1576 n.label.to_string(),
1577 format!("{}({})", n.op.name(), inputs.join(", ")),
1578 )
1579 })
1580 .collect();
1581 let lw = rows.iter().map(|r| r.1.chars().count()).max().unwrap_or(0);
1582 let ew = rows.iter().map(|r| r.2.chars().count()).max().unwrap_or(0);
1583 for (id, label, expr) in &rows {
1584 let mut note = String::new();
1585 if in_cycle.contains(id) {
1586 note.push_str(" ↺");
1587 }
1588 if Some(*id) == page {
1589 note.push_str(if r.view_is_per_session {
1590 " ← the page, per session"
1591 } else {
1592 " ← the page, broadcast"
1593 });
1594 } else if g.sinks.contains(id) {
1595 note.push_str(" ← a sink nothing reads");
1596 }
1597 let _ = writeln!(
1598 out,
1599 " {label:<lw$} {expr:<ew$} {:<7}{note}",
1600 g.node(*id).tier.name(),
1601 );
1602 }
1603
1604 let _ = writeln!(out, "\naccumulator");
1605 if r.is_fused() {
1606 let _ = writeln!(
1607 out,
1608 " {} durable folds, fused into one record — §3.7 fixes one totally-ordered log per\n \
1609 application, so two folds are two projections of it rather than two logs.",
1610 r.states.len()
1611 );
1612 for s in &r.states {
1613 let _ = writeln!(out, " {FUSED_STATE}.{} : {}", s.name, s.ty);
1614 }
1615 } else {
1616 let _ = writeln!(
1617 out,
1618 " one durable fold — `{}` : {}",
1619 r.states[0].name, r.states[0].ty
1620 );
1621 }
1622
1623 let plan = slice_of(g, page.unwrap_or(0));
1624 let computed: Vec<&str> = plan
1625 .iter()
1626 .copied()
1627 .filter(|&i| {
1628 !matches!(g.node(i).op, Op::Durable | Op::Fold { .. })
1629 && !g.node(i).op.is_stream()
1630 && Some(i) != page
1631 })
1632 .map(|i| g.label(i))
1633 .collect();
1634 let _ = writeln!(out, "\nthe view recomputes, per event");
1635 let _ = writeln!(
1636 out,
1637 " {}",
1638 if computed.is_empty() {
1639 "nothing between the accumulator and the page".to_string()
1640 } else {
1641 computed.join(", ")
1642 }
1643 );
1644 let _ = writeln!(
1645 out,
1646 " shared: {}",
1647 if r.shared.is_empty() {
1648 "— (no signal is read by two consumers, so nothing is bound twice)".to_string()
1649 } else {
1650 format!(
1651 "{} (read by more than one consumer, so computed once)",
1652 r.shared
1653 .iter()
1654 .map(|s| s.to_string())
1655 .collect::<Vec<_>>()
1656 .join(", ")
1657 )
1658 }
1659 );
1660 let _ = writeln!(
1661 out,
1662 " (§5.3 makes these incremental; today every one is a full recompute)"
1663 );
1664
1665 if !g.cuts.is_empty() {
1666 let _ = writeln!(
1667 out,
1668 "\ntier crossings — each is one subscription, resumable by (id, seq) (§4.3)"
1669 );
1670 let edges: Vec<(String, String, String)> = g
1671 .cuts
1672 .iter()
1673 .map(|c| {
1674 (
1675 format!("{} → {}", g.label(c.from), g.label(c.to)),
1676 format!(
1677 "{} → {}",
1678 g.node(c.from).tier.name(),
1679 g.node(c.to).tier.name()
1680 ),
1681 format!("{}", c.carries),
1682 )
1683 })
1684 .collect();
1685 let nw = edges.iter().map(|e| e.0.chars().count()).max().unwrap_or(0);
1686 let tw = edges.iter().map(|e| e.1.chars().count()).max().unwrap_or(0);
1687 let cw = edges.iter().map(|e| e.2.chars().count()).max().unwrap_or(0);
1688 for (c, (names, tiers, carries)) in g.cuts.iter().zip(&edges) {
1689 let _ = writeln!(
1690 out,
1691 " {names:<nw$} {tiers:<tw$} carries {carries:<cw$} {}",
1692 c.id
1693 );
1694 }
1695 }
1696 out
1697}
1698
1699pub fn crossings(placed: &Placed) -> &[Cut] {
1701 &placed.graph.cuts
1702}
1703
1704pub fn slice_of(graph: &Graph, sink: SigId) -> Vec<SigId> {
1706 let mut seen = BTreeSet::new();
1707 let mut stack = vec![sink];
1708 while let Some(id) = stack.pop() {
1709 if !seen.insert(id) {
1710 continue;
1711 }
1712 for &i in &graph.node(id).inputs {
1713 stack.push(i);
1714 }
1715 }
1716 graph
1717 .order()
1718 .into_iter()
1719 .filter(|i| seen.contains(i))
1720 .collect()
1721}
1722
1723#[cfg(test)]
1724pub(crate) mod tests {
1725 use super::*;
1726 use crate::compile_str;
1727
1728 pub const TODO: &str = r#"
1730type Id = newtype[Str]
1731
1732model Todo:
1733 id: Id
1734 text: Str
1735 done: Bool
1736 owner: Str
1737
1738model State:
1739 todos: Map[Id, Todo]
1740
1741union Command:
1742 Add(id: Id, text: Str)
1743 Toggle(id: Id)
1744 Delete(id: Id)
1745
1746union Event:
1747 Added(id: Id, text: Str)
1748 Toggled(id: Id)
1749 Deleted(id: Id)
1750
1751union Rejection:
1752 BlankText
1753 IdTaken
1754 NoSuchTodo
1755 NotOwner
1756
1757def apply_event(s: State, env: Envelope[Event]) -> State:
1758 match env.body:
1759 case Added(id, text):
1760 return s.with(todos=map_insert(s.todos, id, Todo(id=id, text=text, done=False, owner=env.actor)))
1761 case Toggled(id):
1762 return toggle(s, id)
1763 case Deleted(id):
1764 return s.with(todos=map_remove(s.todos, id))
1765
1766def toggle(s: State, id: Id) -> State:
1767 match map_get(s.todos, id):
1768 case Some(value):
1769 return s.with(todos=map_insert(s.todos, id, value.with(done=not value.done)))
1770 case None:
1771 return s
1772
1773def validate(s: State, p: Proposal) -> Result[list[Event], Rejection]:
1774 match p.command:
1775 case Add(id, text):
1776 if str_is_empty(str_trim(text)):
1777 return Err(error=BlankText)
1778 if map_contains(s.todos, id):
1779 return Err(error=IdTaken)
1780 return Ok(value=[Added(id=id, text=text)])
1781 case Toggle(id):
1782 return owned(s, p, id, [Toggled(id=id)])
1783 case Delete(id):
1784 return owned(s, p, id, [Deleted(id=id)])
1785
1786def owned(s: State, p: Proposal, id: Id, evs: list[Event]) -> Result[list[Event], Rejection]:
1787 match map_get(s.todos, id):
1788 case Some(value):
1789 if value.owner != p.session.actor:
1790 return Err(error=NotOwner)
1791 return Ok(value=evs)
1792 case None:
1793 return Err(error=NoSuchTodo)
1794
1795def mine(s: State, session: Session) -> list[Todo]:
1796 return sort_by(filter_list(map_values(s.todos), lambda t: t.owner == session.actor), lambda t: t.text)
1797
1798def remaining_of(todos: list[Todo]) -> Int:
1799 return list_len(filter_list(todos, lambda t: not t.done))
1800
1801def view(s: State, session: Session) -> Html:
1802 todos = mine(s, session)
1803 return render(todos, remaining_of(todos))
1804
1805def render(todos: list[Todo], remaining: Int) -> Html:
1806 return ui:
1807 main:
1808 h1: "todos"
1809 ul:
1810 for t in todos:
1811 li(key=t.id, class=done_class(t)):
1812 span(on_click=Toggle(id=t.id)): t.text
1813 footer: (str(remaining) + " remaining")
1814
1815def done_class(t: Todo) -> Str:
1816 return "done" if t.done else ""
1817
1818@on(server)
1819proposals: Stream[Proposal] = merge_clients()
1820
1821@on(server)
1822events: Stream[Event] = decide(proposals, todos, validate)
1823
1824@on(data)
1825todos: Signal[State] = durable(fold(apply_event, State(todos={}), events))
1826
1827@on(client)
1828page: Signal[Html] = per_session(todos, view)
1829"#;
1830
1831 #[test]
1832 fn the_sketch_compiles_and_slices_into_roles() {
1833 let (placed, d, map) = compile_str("todo.beck", TODO);
1834 assert!(!d.has_errors(), "{}", d.render(&map));
1835 let placed = placed.expect("splitting succeeds");
1836 assert_eq!(placed.roles.state_name.as_ref(), "todos");
1837 assert_eq!(placed.roles.events_name.as_ref(), "events");
1838 assert_eq!(placed.roles.page_name.as_ref(), "page");
1839 assert!(placed.roles.view_is_per_session);
1840 assert_eq!(placed.roles.event_ty.con_name(), Some("Event"));
1841 assert_eq!(placed.roles.command_ty.con_name(), Some("Command"));
1842 assert_eq!(placed.wire_id.len(), 16);
1843 assert!(!placed.roles.is_fused());
1846 assert_eq!(placed.roles.states.len(), 1);
1847 assert_eq!(placed.roles.state_ty.con_name(), Some("State"));
1848 }
1849
1850 #[test]
1851 fn the_graph_holds_the_fold_as_its_own_vertex() {
1852 let (placed, _, _) = compile_str("todo.beck", TODO);
1856 let g = &placed.expect("placed").graph;
1857 assert_eq!(g.states().len(), 1);
1858 let durable = g.states()[0];
1859 let inner = g.node(durable).inputs[0];
1860 assert!(matches!(g.node(inner).op, Op::Fold { .. }));
1861 assert_eq!(g.label(inner), "todos·fold");
1862 assert_eq!(
1863 g.node(inner).name,
1864 None,
1865 "an inner vertex has no written name"
1866 );
1867 }
1868
1869 #[test]
1870 fn the_only_cycle_is_the_one_the_design_says_is_sound() {
1871 let (placed, _, _) = compile_str("todo.beck", TODO);
1874 let g = &placed.expect("placed").graph;
1875 let cycles: Vec<Vec<String>> = g
1876 .dep
1877 .cycles()
1878 .map(|c| {
1879 c.iter()
1880 .map(|n| g.label(n.0 as usize).to_string())
1881 .collect()
1882 })
1883 .collect();
1884 assert_eq!(cycles.len(), 1, "{cycles:?}");
1885 assert!(cycles[0].iter().any(|n| n == "events"));
1886 assert!(cycles[0].iter().any(|n| n == "todos"));
1887 assert!(cycles[0].iter().any(|n| n == "todos·fold"));
1888 }
1889
1890 #[test]
1891 fn the_wire_id_is_content_derived_and_stable_under_a_body_edit() {
1892 let (a, _, _) = compile_str("todo.beck", TODO);
1893 let edited = TODO.replace(
1895 r#""done" if t.done else """#,
1896 r#""done" if t.done else " ""#,
1897 );
1898 let (b, d, map) = compile_str("todo.beck", &edited);
1899 assert!(!d.has_errors(), "{}", d.render(&map));
1900 assert_eq!(
1901 a.expect("a").wire_id,
1902 b.expect("b").wire_id,
1903 "a body edit must not change the wire id"
1904 );
1905 }
1906
1907 #[test]
1908 fn the_wire_id_moves_when_the_wire_actually_changes() {
1909 let (a, _, _) = compile_str("todo.beck", TODO);
1913 let changed = TODO
1914 .replace(
1915 " Toggled(id: Id)\n Deleted(id: Id)",
1916 " Toggled(id: Id)\n Deleted(id: Id)\n Starred(id: Id)",
1917 )
1918 .replace(
1919 " case Deleted(id):\n return s.with(todos=map_remove(s.todos, id))",
1920 " case Deleted(id):\n return s.with(todos=map_remove(s.todos, id))\n case Starred(id):\n return toggle(s, id)",
1921 );
1922 let (b, d, map) = compile_str("todo.beck", &changed);
1923 assert!(!d.has_errors(), "{}", d.render(&map));
1924 assert_ne!(a.expect("a").wire_id, b.expect("b").wire_id);
1925
1926 let widened = TODO
1929 .replace(
1930 " Toggle(id: Id)\n Delete(id: Id)",
1931 " Toggle(id: Id, at: Int)\n Delete(id: Id)",
1932 )
1933 .replace("case Toggle(id):", "case Toggle(id, at):")
1934 .replace(
1935 "span(on_click=Toggle(id=t.id)): t.text",
1936 "span(on_click=Toggle(id=t.id, at=0)): t.text",
1937 );
1938 let (c, d, map) = compile_str("todo.beck", &widened);
1939 assert!(!d.has_errors(), "{}", d.render(&map));
1940 let (a, _, _) = compile_str("todo.beck", TODO);
1941 assert_ne!(a.expect("a").wire_id, c.expect("c").wire_id);
1942 }
1943
1944 #[test]
1945 fn a_program_with_no_merge_point_is_told_what_is_missing() {
1946 let (_, d, _) = compile_str("t.beck", "def f() -> Int:\n return 1\n");
1947 assert!(d.iter().any(|x| x.code == "B0500" && x.fix.is_some()));
1948 }
1949
1950 #[test]
1951 fn a_view_that_reads_a_stream_is_refused_by_name() {
1952 let src = TODO
1955 .replace(
1956 "@on(client)\npage: Signal[Html] = per_session(todos, view)",
1957 "@on(client)\npage: Signal[Html] = signal_map(events, render_ev)",
1958 )
1959 .replace(
1960 "@on(server)\nproposals",
1961 "def render_ev(e: Event) -> Html:\n return ui:\n main: \"x\"\n\n@on(server)\nproposals",
1962 );
1963 let (placed, d, _) = compile_str("t.beck", &src);
1964 assert!(placed.is_none());
1965 assert!(d.has_errors(), "a refusal must say why");
1966 }
1967
1968 #[test]
1969 fn a_cycle_with_no_fold_in_it_is_refused_rather_than_looped_on() {
1970 let src = TODO.replace(
1972 "@on(data)\ntodos: Signal[State] = durable(fold(apply_event, State(todos={}), events))",
1973 "@on(data)\ntodos: Signal[State] = durable(fold(apply_event, State(todos={}), events))\n\
1974 \nloop_a: Signal[State] = signal_map(loop_b, identity_state)\n\
1975 \nloop_b: Signal[State] = signal_map(loop_a, identity_state)",
1976 );
1977 let src = src.replace(
1978 "def done_class",
1979 "def identity_state(s: State) -> State:\n return s\n\ndef done_class",
1980 );
1981 let (placed, d, _) = compile_str("t.beck", &src);
1982 assert!(placed.is_none(), "a self-defined signal has no first value");
1983 assert!(
1984 d.iter().any(|x| x.code == "B0509"),
1985 "{:?}",
1986 d.iter().map(|x| x.code).collect::<Vec<_>>()
1987 );
1988 }
1989}