1use bynk_check::analysis::ContextSequenceInfo;
25use bynk_syntax::ast::*;
26use bynk_syntax::span::Span;
27
28#[derive(Debug, Clone, Copy)]
30pub enum HandlerOwner<'a> {
31 Service(&'a str),
32 Agent(&'a str),
33}
34
35const MAX_BLOCK_DEPTH: u32 = 2;
39
40#[derive(Debug, Clone, Default, PartialEq)]
41pub struct SequenceModel {
42 pub participants: Vec<Participant>,
43 pub messages: Vec<Message>,
44 pub blocks: Vec<AltBlock>,
45}
46
47#[derive(Debug, Clone, PartialEq)]
48pub struct Participant {
49 pub id: u32,
50 pub kind: ParticipantKind,
51 pub name: String,
52 pub span: Option<Span>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ParticipantKind {
59 Entry,
60 Capability,
61 Context,
62 Agent,
63 Actor,
69}
70
71#[derive(Debug, Clone, PartialEq)]
72pub struct Message {
73 pub from: u32,
74 pub to: u32,
75 pub kind: MessageKind,
76 pub label: String,
77 pub span: Span,
78 pub block: Option<u32>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum MessageKind {
83 Call,
84 Return,
85 Send,
87}
88
89#[derive(Debug, Clone, PartialEq)]
90pub struct AltBlock {
91 pub id: u32,
92 pub kind: AltKind,
93 pub branches: Vec<Branch>,
95 pub span: Span,
96 pub parent: Option<u32>,
97 pub parent_branch: Option<u32>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum AltKind {
108 If,
109 Match,
110 Collapsed,
111}
112
113#[derive(Debug, Clone, PartialEq)]
114pub struct Branch {
115 pub label: String,
116 pub message_ids: Vec<usize>,
117 pub reply: Option<String>,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137enum Arrow {
138 Awaited,
140 FireAndForget,
142}
143
144const ENTRY_ID: u32 = 0;
145
146#[derive(Debug, Clone, Copy)]
150struct BlockCtx {
151 id: u32,
152 branch: u32,
153}
154
155pub fn sequence_model(
169 handler: &Handler,
170 owner: HandlerOwner<'_>,
171 default_given: &[CapRef],
172 default_by: Option<&ByClause>,
173 info: Option<&ContextSequenceInfo>,
174) -> SequenceModel {
175 let given = if handler.given.is_empty() {
176 default_given
177 } else {
178 &handler.given
179 };
180 let by = handler.by_clause.as_ref().or(default_by);
181 let mut b = Builder::new(entry_label(handler, owner, by.is_some()), given, info);
182 if let Some(by) = by {
183 b.add_actor(actor_name(by), by.span);
188 b.emit_request(discriminator(handler), handler.span);
189 }
190 b.walk_block(&handler.body, None, 0, true);
193 b.finish()
194}
195
196fn discriminator(handler: &Handler) -> String {
200 match &handler.kind {
201 HandlerKind::Call => match handler.method_name.as_ref() {
202 Some(m) => m.name.clone(),
203 None => "call".to_string(),
204 },
205 HandlerKind::Http { method, path } => format!("{} {}", method.as_str(), path),
206 HandlerKind::Cron { expr } => format!("cron \"{expr}\""),
207 HandlerKind::Message => "message".to_string(),
208 HandlerKind::Open => "open".to_string(),
209 HandlerKind::Close => "close".to_string(),
210 HandlerKind::Event => "event".to_string(),
211 }
212}
213
214fn actor_name(by: &ByClause) -> String {
217 by.actors
218 .iter()
219 .map(|a| a.name.as_str())
220 .collect::<Vec<_>>()
221 .join(" | ")
222}
223
224fn entry_label(handler: &Handler, owner: HandlerOwner<'_>, has_actor: bool) -> String {
229 match owner {
230 HandlerOwner::Service(name) if has_actor => name.to_string(),
231 HandlerOwner::Agent(name) if has_actor => name.to_string(),
232 HandlerOwner::Service(name) => format!("{name} {}", discriminator(handler)),
233 HandlerOwner::Agent(name) => format!("{name}.{}", discriminator(handler)),
234 }
235}
236
237struct Builder<'a> {
238 given: &'a [CapRef],
239 info: Option<&'a ContextSequenceInfo>,
240 participants: Vec<Participant>,
241 messages: Vec<Message>,
242 blocks: Vec<AltBlock>,
243 actor: Option<u32>,
248}
249
250impl<'a> Builder<'a> {
251 fn new(
252 entry_label: String,
253 given: &'a [CapRef],
254 info: Option<&'a ContextSequenceInfo>,
255 ) -> Self {
256 Builder {
257 given,
258 info,
259 participants: vec![Participant {
260 id: ENTRY_ID,
261 kind: ParticipantKind::Entry,
262 name: entry_label,
263 span: None,
264 }],
265 messages: Vec::new(),
266 blocks: Vec::new(),
267 actor: None,
268 }
269 }
270
271 fn add_actor(&mut self, name: String, span: Span) {
276 let id = self.participants.len() as u32;
277 self.participants.insert(
278 0,
279 Participant {
280 id,
281 kind: ParticipantKind::Actor,
282 name,
283 span: Some(span),
284 },
285 );
286 self.actor = Some(id);
287 }
288
289 fn emit_request(&mut self, label: String, handler_span: Span) {
294 let Some(actor) = self.actor else { return };
295 self.messages.push(Message {
296 from: actor,
297 to: ENTRY_ID,
298 kind: MessageKind::Call,
299 label,
300 span: handler_span,
301 block: None,
302 });
303 }
304
305 fn emit_reply(&mut self, outcome: String, span: Span, current_block: Option<BlockCtx>) {
309 let Some(actor) = self.actor else { return };
310 self.messages.push(Message {
311 from: ENTRY_ID,
312 to: actor,
313 kind: MessageKind::Return,
314 label: outcome,
315 span,
316 block: current_block.map(|c| c.id),
317 });
318 }
319
320 fn finish(self) -> SequenceModel {
321 SequenceModel {
322 participants: self.participants,
323 messages: self.messages,
324 blocks: self.blocks,
325 }
326 }
327
328 fn participant_id(&mut self, kind: ParticipantKind, name: &str, span: Span) -> u32 {
329 if let Some(p) = self
330 .participants
331 .iter()
332 .find(|p| p.kind == kind && p.name == name)
333 {
334 return p.id;
335 }
336 let id = self.participants.len() as u32;
337 self.participants.push(Participant {
338 id,
339 kind,
340 name: name.to_string(),
341 span: Some(span),
342 });
343 id
344 }
345
346 fn walk_block(
353 &mut self,
354 block: &Block,
355 current_block: Option<BlockCtx>,
356 depth: u32,
357 ret: bool,
358 ) {
359 for stmt in &block.statements {
360 match stmt {
361 Statement::EffectLet(l) => {
362 self.walk_value(&l.value, current_block, depth, Some(Arrow::Awaited), false)
363 }
364 Statement::Do(d) => {
365 self.walk_value(&d.value, current_block, depth, Some(Arrow::Awaited), false)
366 }
367 Statement::Send(s) => self.walk_value(
368 &s.value,
369 current_block,
370 depth,
371 Some(Arrow::FireAndForget),
372 false,
373 ),
374 Statement::Let(l) => self.walk_value(&l.value, current_block, depth, None, false),
375 Statement::Expect(e) => {
376 self.walk_value(&e.value, current_block, depth, None, false)
377 }
378 Statement::Assign(a) => {
379 self.walk_value(&a.value, current_block, depth, None, false)
380 }
381 }
382 }
383 self.walk_value(&block.tail, current_block, depth, None, ret);
384 }
385
386 fn walk_value(
395 &mut self,
396 expr: &Expr,
397 current_block: Option<BlockCtx>,
398 depth: u32,
399 arrow: Option<Arrow>,
400 ret: bool,
401 ) {
402 let inner = peel_paren(expr);
403 match &inner.kind {
404 ExprKind::If { .. } => self.walk_if(inner, current_block, depth, ret),
407 ExprKind::Match { arms, .. } => {
408 self.walk_match(inner.span, arms, current_block, depth, ret)
409 }
410 ExprKind::Block(b) => self.walk_block(b, current_block, depth, ret),
411 ExprKind::Call { .. }
412 | ExprKind::ConstructorCall { .. }
413 | ExprKind::MethodCall { .. } => {
414 if let Some(arrow) = arrow {
415 self.classify_call(inner, arrow, current_block);
416 }
417 self.maybe_reply(inner, current_block, ret);
418 }
419 _ => self.maybe_reply(inner, current_block, ret),
420 }
421 }
422
423 fn maybe_reply(&mut self, expr: &Expr, current_block: Option<BlockCtx>, ret: bool) {
427 if !ret {
428 return;
429 }
430 if let Some(outcome) = branch_outcome(expr) {
431 self.emit_reply(outcome, expr.span, current_block);
432 }
433 }
434
435 fn walk_if(&mut self, if_expr: &Expr, current_block: Option<BlockCtx>, depth: u32, ret: bool) {
438 let ExprKind::If {
439 cond,
440 then_block,
441 else_block,
442 } = &if_expr.kind
443 else {
444 return;
445 };
446 let span = if_expr.span;
447 if depth >= MAX_BLOCK_DEPTH {
448 self.push_collapsed(span, current_block);
449 return;
450 }
451 let id = self.blocks.len() as u32;
452 self.blocks.push(AltBlock {
453 id,
454 kind: AltKind::If,
455 branches: Vec::new(),
456 span,
457 parent: current_block.map(|c| c.id),
458 parent_branch: current_block.map(|c| c.branch),
459 });
460 let then_start = self.messages.len();
461 self.walk_block(then_block, Some(BlockCtx { id, branch: 0 }), depth + 1, ret);
462 let then_ids = (then_start..self.messages.len()).collect();
463 let mut branches = vec![Branch {
466 label: bynk_fmt::expr_to_string(cond),
467 message_ids: then_ids,
468 reply: self.branch_reply(&then_block.tail),
469 }];
470 if !else_block.is_synth_unit() {
476 let else_start = self.messages.len();
477 self.walk_block(else_block, Some(BlockCtx { id, branch: 1 }), depth + 1, ret);
478 branches.push(Branch {
479 label: "otherwise".to_string(),
480 message_ids: (else_start..self.messages.len()).collect(),
481 reply: self.branch_reply(&else_block.tail),
482 });
483 }
484 self.blocks[id as usize].branches = branches;
485 }
486
487 fn branch_reply(&self, tail: &Expr) -> Option<String> {
492 if self.actor.is_some() {
493 None
494 } else {
495 branch_outcome(tail)
496 }
497 }
498
499 fn walk_match(
500 &mut self,
501 span: Span,
502 arms: &[MatchArm],
503 current_block: Option<BlockCtx>,
504 depth: u32,
505 ret: bool,
506 ) {
507 if depth >= MAX_BLOCK_DEPTH {
508 self.push_collapsed(span, current_block);
509 return;
510 }
511 let id = self.blocks.len() as u32;
512 self.blocks.push(AltBlock {
513 id,
514 kind: AltKind::Match,
515 branches: Vec::new(),
516 span,
517 parent: current_block.map(|c| c.id),
518 parent_branch: current_block.map(|c| c.branch),
519 });
520 let mut branches = Vec::with_capacity(arms.len());
521 for (arm_index, arm) in arms.iter().enumerate() {
522 let start = self.messages.len();
523 let branch_ctx = Some(BlockCtx {
524 id,
525 branch: arm_index as u32,
526 });
527 let reply = match &arm.body {
531 MatchBody::Expr(e) => {
532 self.walk_value(e, branch_ctx, depth + 1, None, ret);
533 self.branch_reply(e)
534 }
535 MatchBody::Block(b) => {
536 self.walk_block(b, branch_ctx, depth + 1, ret);
537 self.branch_reply(&b.tail)
538 }
539 };
540 branches.push(Branch {
541 label: pattern_summary(&arm.pattern),
542 message_ids: (start..self.messages.len()).collect(),
543 reply,
544 });
545 }
546 self.blocks[id as usize].branches = branches;
547 }
548
549 fn push_collapsed(&mut self, span: Span, current_block: Option<BlockCtx>) {
550 let id = self.blocks.len() as u32;
551 self.blocks.push(AltBlock {
552 id,
553 kind: AltKind::Collapsed,
554 branches: Vec::new(),
555 span,
556 parent: current_block.map(|c| c.id),
557 parent_branch: current_block.map(|c| c.branch),
558 });
559 }
560
561 fn classify_call(&mut self, expr: &Expr, arrow: Arrow, current_block: Option<BlockCtx>) {
562 let Some((target, label)) = self.classify_target(expr) else {
563 return;
566 };
567 let block = current_block.map(|c| c.id);
568 match arrow {
569 Arrow::FireAndForget => {
570 self.messages.push(Message {
571 from: ENTRY_ID,
572 to: target,
573 kind: MessageKind::Send,
574 label,
575 span: expr.span,
576 block,
577 });
578 }
579 Arrow::Awaited => {
580 self.messages.push(Message {
581 from: ENTRY_ID,
582 to: target,
583 kind: MessageKind::Call,
584 label,
585 span: expr.span,
586 block,
587 });
588 self.messages.push(Message {
589 from: target,
590 to: ENTRY_ID,
591 kind: MessageKind::Return,
592 label: String::new(),
593 span: expr.span,
594 block,
595 });
596 }
597 }
598 }
599
600 fn classify_target(&mut self, expr: &Expr) -> Option<(u32, String)> {
617 match &expr.kind {
618 ExprKind::MethodCall {
619 receiver,
620 method,
621 args,
622 ..
623 } => match &receiver.kind {
624 ExprKind::Call { name, .. }
625 if self.info.is_some_and(|i| i.agents.contains_key(&name.name)) =>
626 {
627 let id = self.participant_id(ParticipantKind::Agent, &name.name, name.span);
628 Some((id, call_label(&method.name, args)))
629 }
630 ExprKind::Ident(id) => self.classify_static(&id.name, &method.name, args, id.span),
631 _ => None,
632 },
633 ExprKind::ConstructorCall {
636 type_name,
637 method,
638 args,
639 } => self.classify_static(&type_name.name, &method.name, args, type_name.span),
640 _ => None,
641 }
642 }
643
644 fn classify_static(
650 &mut self,
651 name: &str,
652 method: &str,
653 args: &[Expr],
654 span: Span,
655 ) -> Option<(u32, String)> {
656 if self.given.iter().any(|c| c.key() == name) {
657 let id = self.participant_id(ParticipantKind::Capability, name, span);
658 return Some((id, call_label(method, args)));
659 }
660 self.classify_cross_context(name, method, args, span)
661 }
662
663 fn classify_cross_context(
664 &mut self,
665 prefix: &str,
666 method: &str,
667 args: &[Expr],
668 span: Span,
669 ) -> Option<(u32, String)> {
670 let ctx_name = self.info?.cross_context.resolve_prefix(prefix)?;
671 let id = self.participant_id(ParticipantKind::Context, &ctx_name, span);
672 Some((id, call_label(method, args)))
673 }
674}
675
676fn call_label(method: &str, args: &[Expr]) -> String {
677 let rendered: Vec<String> = args.iter().map(bynk_fmt::expr_to_string).collect();
678 format!("{method}({})", rendered.join(", "))
679}
680
681fn branch_outcome(tail: &Expr) -> Option<String> {
688 let inner = peel_paren(tail);
689 match &inner.kind {
690 ExprKind::UnitLit | ExprKind::If { .. } | ExprKind::Match { .. } | ExprKind::Block(_) => {
691 None
692 }
693 _ => Some(bynk_fmt::expr_to_string(inner)),
694 }
695}
696
697fn peel_paren(expr: &Expr) -> &Expr {
698 match &expr.kind {
699 ExprKind::Paren(inner) => peel_paren(inner),
700 _ => expr,
701 }
702}
703
704fn pattern_summary(pattern: &Pattern) -> String {
707 match pattern {
708 Pattern::Wildcard(_) => "_".to_string(),
709 Pattern::Binding(b) => b.name.clone(),
710 Pattern::Literal { value, .. } => value.describe(),
711 Pattern::Variant {
712 type_name, variant, ..
713 } => match type_name {
714 Some(t) => format!("{}.{}", t.name, variant.name),
715 None => variant.name.clone(),
716 },
717 Pattern::Refined { inner, .. } => pattern_summary(inner),
718 Pattern::Or(patterns, _) => patterns
719 .iter()
720 .map(pattern_summary)
721 .collect::<Vec<_>>()
722 .join(" | "),
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use std::fs;
730 use std::path::PathBuf;
731
732 fn setup_project(test_name: &str, files: &[(&str, &str)]) -> PathBuf {
738 let root = std::env::temp_dir().join(format!(
739 "bynk-ide-sequence-test-{test_name}-{}",
740 std::process::id()
741 ));
742 let _ = fs::remove_dir_all(&root);
743 fs::create_dir_all(&root).expect("create test root");
744 for (rel, contents) in files {
745 let p = root.join(rel);
746 if let Some(parent) = p.parent() {
747 fs::create_dir_all(parent).expect("create parent");
748 }
749 fs::write(&p, contents).expect("write file");
750 }
751 root
752 }
753
754 fn parse_context(text: &str) -> Context {
755 let tokens = bynk_syntax::lexer::tokenize(text).expect("tokenize");
756 let (unit, errs) = bynk_syntax::parser::parse_unit_with_recovery(&tokens, text);
757 assert!(errs.is_empty(), "parse errors: {errs:?}");
758 match unit.expect("parsed unit") {
759 SourceUnit::Context(c) => c,
760 _ => panic!("expected a context"),
761 }
762 }
763
764 fn find_service<'a>(ctx: &'a Context, name: &str) -> &'a ServiceDecl {
765 ctx.items
766 .iter()
767 .find_map(|i| match i {
768 CommonsItem::Service(s) if s.name.name == name => Some(s),
769 _ => None,
770 })
771 .unwrap_or_else(|| panic!("service {name} not found"))
772 }
773
774 const RATELIMIT_SRC: &str = r#"context ratelimit
781
782consumes bynk { Clock }
783
784type ClientId = String where NonEmpty
785
786type RateView = {
787 allowed: Bool,
788 remaining: Int,
789 resetAt: Int,
790}
791
792agent Limiter {
793 key client: ClientId
794
795 store count: Cell[Int]
796
797 on call hit(now: Int) -> Effect[RateView] {
798 let _ <- count.update((c) => c + 1)
799 RateView { allowed: count < 10, remaining: 10 - count, resetAt: now }
800 }
801}
802
803service api from http {
804 on GET("/check/:client") (client: ClientId) -> Effect[HttpResult[RateView]] by Visitor given Clock {
805 let now <- Clock.now()
806 let view <- Limiter(client).hit(now.toEpochMillis())
807 if view.allowed {
808 Ok(view)
809 } else {
810 TooManyRequests("rate limit exceeded")
811 }
812 }
813}
814"#;
815
816 #[test]
817 fn rate_limiter_get_check_client_classifies_capability_and_agent_and_gates_the_return() {
818 let root = setup_project("ratelimit", &[("ratelimit.bynk", RATELIMIT_SRC)]);
819 let diag = crate::testkit::diagnose_project(&root);
820 let info = diag
821 .sequence_info
822 .get("ratelimit")
823 .expect("sequence_info entry for ratelimit");
824
825 let ctx = parse_context(RATELIMIT_SRC);
826 let svc = find_service(&ctx, "api");
827 let handler = &svc.handlers[0];
828
829 let model = sequence_model(
830 handler,
831 HandlerOwner::Service("api"),
832 &svc.default_given,
833 svc.default_by.as_ref(),
834 Some(info),
835 );
836
837 let kinds: Vec<(ParticipantKind, &str)> = model
841 .participants
842 .iter()
843 .map(|p| (p.kind, p.name.as_str()))
844 .collect();
845 assert_eq!(
846 kinds,
847 vec![
848 (ParticipantKind::Actor, "Visitor"),
849 (ParticipantKind::Entry, "api"),
850 (ParticipantKind::Capability, "Clock"),
851 (ParticipantKind::Agent, "Limiter"),
852 ]
853 );
854 let actor_id = model.participants[0].id;
855
856 let req = &model.messages[0];
858 assert_eq!(req.kind, MessageKind::Call);
859 assert_eq!((req.from, req.to), (actor_id, ENTRY_ID));
860 assert_eq!(req.label, "GET /check/:client");
861
862 assert_eq!(
864 model.messages.len(),
865 7,
866 "request + Clock Call/Return + Limiter Call/Return + a reply-to-actor per branch"
867 );
868
869 assert_eq!(model.blocks.len(), 1);
870 assert_eq!(model.blocks[0].kind, AltKind::If);
871 assert_eq!(model.blocks[0].branches.len(), 2);
872 let labels: Vec<&str> = model.blocks[0]
876 .branches
877 .iter()
878 .map(|b| b.label.as_str())
879 .collect();
880 assert_eq!(labels, vec!["view.allowed", "otherwise"]);
881 assert!(
882 model.blocks[0].branches.iter().all(|b| b.reply.is_none()),
883 "with an actor the outcome is a message, not a note"
884 );
885 let branch_replies: Vec<(MessageKind, u32, u32, &str)> = model.blocks[0]
886 .branches
887 .iter()
888 .flat_map(|b| &b.message_ids)
889 .map(|&i| {
890 let m = &model.messages[i];
891 (m.kind, m.from, m.to, m.label.as_str())
892 })
893 .collect();
894 assert_eq!(
895 branch_replies,
896 vec![
897 (MessageKind::Return, ENTRY_ID, actor_id, "Ok(view)"),
898 (
899 MessageKind::Return,
900 ENTRY_ID,
901 actor_id,
902 "TooManyRequests(\"rate limit exceeded\")",
903 ),
904 ],
905 "each branch replies its outcome to the actor"
906 );
907 }
908
909 const PLATFORM_SRC: &str = r#"context platform
911
912service Pinger {
913 on call(n: Int) -> Effect[Int] {
914 n
915 }
916}
917"#;
918 const CONSUMER_SRC: &str = r#"context consumer
919
920consumes platform
921
922service api {
923 on call(n: Int) -> Effect[Int] {
924 let v <- platform.Pinger(n)
925 v
926 }
927}
928"#;
929
930 #[test]
931 fn cross_context_call_is_boundary_stop() {
932 let root = setup_project(
933 "crossctx",
934 &[
935 ("platform.bynk", PLATFORM_SRC),
936 ("consumer.bynk", CONSUMER_SRC),
937 ],
938 );
939 let diag = crate::testkit::diagnose_project(&root);
940 let info = diag
941 .sequence_info
942 .get("consumer")
943 .expect("sequence_info entry for consumer");
944
945 let ctx = parse_context(CONSUMER_SRC);
946 let svc = find_service(&ctx, "api");
947 let handler = &svc.handlers[0];
948 let model = sequence_model(
949 handler,
950 HandlerOwner::Service("api"),
951 &svc.default_given,
952 svc.default_by.as_ref(),
953 Some(info),
954 );
955
956 assert_eq!(model.participants.len(), 2, "Entry + the consumed context");
957 assert_eq!(model.participants[1].kind, ParticipantKind::Context);
958 assert_eq!(model.participants[1].name, "platform");
959 assert_eq!(
960 model.messages.len(),
961 2,
962 "one Call + one Return — the consumed service's own body is never walked"
963 );
964 assert_eq!(model.messages[0].kind, MessageKind::Call);
965 assert_eq!(model.messages[1].kind, MessageKind::Return);
966 }
967
968 const MISC_SRC: &str = r#"context misc
971
972consumes bynk { Clock, Logger }
973
974fn double(x: Int) -> Int {
975 x * 2
976}
977
978service fireService {
979 on call(n: Int) -> Effect[()] given Logger {
980 ~> Logger.info("hi")
981 Effect.pure(())
982 }
983}
984
985service localService {
986 on call(n: Int) -> Effect[Int] {
987 double(n)
988 }
989}
990
991service nestedService {
992 on call(n: Int) -> Effect[Int] given Clock {
993 let now <- Clock.now()
994 if n > 0 {
995 if n > 10 {
996 if n > 100 {
997 now.toEpochMillis()
998 } else {
999 1
1000 }
1001 } else {
1002 2
1003 }
1004 } else {
1005 3
1006 }
1007 }
1008}
1009"#;
1010
1011 fn misc_info(diag: &crate::ProjectDiagnostics) -> bynk_check::analysis::ContextSequenceInfo {
1012 diag.sequence_info
1013 .get("misc")
1014 .cloned()
1015 .expect("sequence_info entry for misc")
1016 }
1017
1018 #[test]
1019 fn fire_and_forget_send_has_no_paired_return() {
1020 let root = setup_project("misc-send", &[("misc.bynk", MISC_SRC)]);
1021 let diag = crate::testkit::diagnose_project(&root);
1022 let info = misc_info(&diag);
1023
1024 let ctx = parse_context(MISC_SRC);
1025 let svc = find_service(&ctx, "fireService");
1026 let handler = &svc.handlers[0];
1027 let model = sequence_model(
1028 handler,
1029 HandlerOwner::Service("fireService"),
1030 &svc.default_given,
1031 svc.default_by.as_ref(),
1032 Some(&info),
1033 );
1034
1035 assert_eq!(model.participants.len(), 2);
1036 assert_eq!(model.participants[1].kind, ParticipantKind::Capability);
1037 assert_eq!(model.participants[1].name, "Logger");
1038 assert_eq!(model.messages.len(), 1, "a Send has no paired Return");
1039 assert_eq!(model.messages[0].kind, MessageKind::Send);
1040 }
1041
1042 #[test]
1043 fn degenerate_handler_with_only_local_calls_has_no_lifelines() {
1044 let root = setup_project("misc-local", &[("misc.bynk", MISC_SRC)]);
1045 let diag = crate::testkit::diagnose_project(&root);
1046 let info = misc_info(&diag);
1047
1048 let ctx = parse_context(MISC_SRC);
1049 let svc = find_service(&ctx, "localService");
1050 let handler = &svc.handlers[0];
1051 let model = sequence_model(
1052 handler,
1053 HandlerOwner::Service("localService"),
1054 &svc.default_given,
1055 svc.default_by.as_ref(),
1056 Some(&info),
1057 );
1058
1059 assert_eq!(model.participants.len(), 1);
1060 assert_eq!(model.participants[0].kind, ParticipantKind::Entry);
1061 assert!(model.messages.is_empty());
1062 assert!(model.blocks.is_empty());
1063 }
1064
1065 #[test]
1066 fn nested_if_collapses_past_the_depth_budget() {
1067 let root = setup_project("misc-nested", &[("misc.bynk", MISC_SRC)]);
1068 let diag = crate::testkit::diagnose_project(&root);
1069 let info = misc_info(&diag);
1070
1071 let ctx = parse_context(MISC_SRC);
1072 let svc = find_service(&ctx, "nestedService");
1073 let handler = &svc.handlers[0];
1074 let model = sequence_model(
1075 handler,
1076 HandlerOwner::Service("nestedService"),
1077 &svc.default_given,
1078 svc.default_by.as_ref(),
1079 Some(&info),
1080 );
1081
1082 let kinds: Vec<AltKind> = model.blocks.iter().map(|b| b.kind).collect();
1088 assert_eq!(kinds, vec![AltKind::If, AltKind::If, AltKind::Collapsed]);
1089 assert!(
1090 model.blocks[2].branches.is_empty(),
1091 "a Collapsed block carries no branches"
1092 );
1093 assert_eq!(model.blocks[0].parent, None);
1099 assert_eq!(model.blocks[0].parent_branch, None);
1100 assert_eq!(model.blocks[1].parent, Some(0));
1101 assert_eq!(model.blocks[1].parent_branch, Some(0));
1102 assert_eq!(model.blocks[2].parent, Some(1));
1103 assert_eq!(model.blocks[2].parent_branch, Some(0));
1104 }
1105
1106 const SERVICE_GIVEN_SRC: &str = r#"context svcgiven
1112
1113consumes bynk { Clock }
1114
1115service api from http by Visitor given Clock {
1116 on GET("/now") () -> Effect[HttpResult[Int]] {
1117 let now <- Clock.now()
1118 Ok(now.toEpochMillis())
1119 }
1120}
1121"#;
1122
1123 #[test]
1124 fn service_level_given_default_is_inherited_by_a_handler_without_its_own() {
1125 let root = setup_project("svcgiven", &[("svcgiven.bynk", SERVICE_GIVEN_SRC)]);
1126 let diag = crate::testkit::diagnose_project(&root);
1127 let info = diag
1128 .sequence_info
1129 .get("svcgiven")
1130 .expect("sequence_info entry for svcgiven");
1131
1132 let ctx = parse_context(SERVICE_GIVEN_SRC);
1133 let svc = find_service(&ctx, "api");
1134 let handler = &svc.handlers[0];
1135 assert!(
1138 handler.given.is_empty(),
1139 "fixture handler must omit its own `given`"
1140 );
1141 assert_eq!(svc.default_given.len(), 1, "service-level `given Clock`");
1142 assert!(
1143 handler.by_clause.is_none() && svc.default_by.is_some(),
1144 "fixture handler must inherit the service-level `by Visitor`"
1145 );
1146
1147 let model = sequence_model(
1148 handler,
1149 HandlerOwner::Service("api"),
1150 &svc.default_given,
1151 svc.default_by.as_ref(),
1152 Some(info),
1153 );
1154
1155 let kinds: Vec<(ParticipantKind, &str)> = model
1156 .participants
1157 .iter()
1158 .map(|p| (p.kind, p.name.as_str()))
1159 .collect();
1160 assert_eq!(
1161 kinds,
1162 vec![
1163 (ParticipantKind::Actor, "Visitor"),
1166 (ParticipantKind::Entry, "api"),
1167 (ParticipantKind::Capability, "Clock"),
1168 ],
1169 "Clock must classify via the inherited `given`, and Visitor via the \
1170 inherited `by` — with only `handler.given`/`handler.by_clause` both \
1171 would be dropped"
1172 );
1173
1174 let actor_id = model.participants[0].id;
1177 let last = model.messages.last().expect("at least one message");
1178 assert_eq!(last.kind, MessageKind::Return);
1179 assert_eq!((last.from, last.to), (ENTRY_ID, actor_id));
1180 assert_eq!(last.label, "Ok(now.toEpochMillis())");
1181 }
1182
1183 const OUTCOME_SRC: &str = r#"context outcome
1188
1189consumes bynk { Logger }
1190
1191service guardSvc {
1192 on call(flag: Bool) -> Effect[()] given Logger {
1193 if flag {
1194 ~> Logger.info("hi")
1195 }
1196 }
1197}
1198
1199service routeSvc {
1200 on call(x: Int) -> Effect[Int] {
1201 match x {
1202 0 => 100
1203 _ => x
1204 }
1205 }
1206}
1207"#;
1208
1209 #[test]
1210 fn else_less_if_is_a_single_branch_opt_and_match_arms_capture_replies() {
1211 let ctx = parse_context(OUTCOME_SRC);
1216 let guard_svc = find_service(&ctx, "guardSvc");
1217 let route_svc = find_service(&ctx, "routeSvc");
1218
1219 let guard = &guard_svc.handlers[0];
1223 let gm = sequence_model(
1224 guard,
1225 HandlerOwner::Service("guardSvc"),
1226 &guard_svc.default_given,
1227 guard_svc.default_by.as_ref(),
1228 None,
1229 );
1230 assert_eq!(gm.blocks.len(), 1);
1231 assert_eq!(gm.blocks[0].kind, AltKind::If);
1232 assert_eq!(
1233 gm.blocks[0].branches.len(),
1234 1,
1235 "an else-less `if` renders as an `opt`, not an `alt` with an empty second branch"
1236 );
1237 assert_eq!(gm.blocks[0].branches[0].label, "flag");
1238 assert_eq!(
1239 gm.blocks[0].branches[0].reply, None,
1240 "a unit tail has no reply"
1241 );
1242 assert_eq!(
1243 gm.blocks[0].branches[0].message_ids.len(),
1244 1,
1245 "the `~>` Send"
1246 );
1247
1248 let route = &route_svc.handlers[0];
1251 let rm = sequence_model(
1252 route,
1253 HandlerOwner::Service("routeSvc"),
1254 &route_svc.default_given,
1255 route_svc.default_by.as_ref(),
1256 None,
1257 );
1258 assert_eq!(rm.blocks.len(), 1);
1259 assert_eq!(rm.blocks[0].kind, AltKind::Match);
1260 let replies: Vec<Option<&str>> = rm.blocks[0]
1261 .branches
1262 .iter()
1263 .map(|b| b.reply.as_deref())
1264 .collect();
1265 assert_eq!(replies, vec![Some("100"), Some("x")]);
1266 }
1267}