1use std::collections::{BTreeMap, HashMap, HashSet};
57use std::path::PathBuf;
58use std::sync::Arc;
59
60use crate::checker::{self, Types};
61use crate::context_checks::{build_capability_op_info, ts_type_ref_display};
62use crate::hints::HintSink;
63use crate::index::{RefSink, SymbolKind};
64use crate::locals::LocalsSink;
65use crate::requirements::RequirementSink;
66use crate::resolver::{self, MethodTable as ResolverMethodTable, ResolvedCommons};
67use crate::symbols::{UnitTable, build_cross_context_info};
68use bynk_project::ParsedFile;
69use bynk_project::UnitKind;
70use bynk_project::discovery::case_effective_tier;
71use bynk_syntax::ast::*;
72use bynk_syntax::error::CompileError;
73use bynk_syntax::span::Span;
74
75#[derive(Debug, Clone)]
81pub struct ResolvedStub {
82 pub cap: String,
84 pub cap_decl: CapabilityDecl,
86 pub clauses: Vec<StubClause>,
89 pub clause_cases: Vec<Option<String>>,
93 pub identity_path: PathBuf,
101}
102
103#[allow(clippy::too_many_arguments)]
118pub fn phase_test_bodies(
119 test_groups: &BTreeMap<String, Vec<usize>>,
120 parsed: &[ParsedFile],
121 kinds: &BTreeMap<String, UnitKind>,
122 unit_tables: &HashMap<String, UnitTable>,
123 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
124 unit_consumes: &HashMap<String, Vec<String>>,
125 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
126 unit_uses: &HashMap<String, Vec<String>>,
127 errors: &mut Vec<CompileError>,
128 refs: &mut RefSink,
129 tys: &Arc<Types>,
130) -> HashMap<String, HashMap<String, ResolvedStub>> {
131 let mut ready: HashMap<String, HashMap<String, ResolvedStub>> = HashMap::new();
132
133 let mut sorted_targets: Vec<&String> = test_groups.keys().collect();
134 sorted_targets.sort();
135
136 for target_name in sorted_targets {
137 let indices = test_groups.get(target_name).unwrap();
138 let target_kind = match kinds.get(target_name) {
140 Some(k) => *k,
141 None => {
142 let span = first_test_target_span(indices, parsed);
143 errors.push(
144 CompileError::new(
145 "bynk.suite.unknown_target",
146 span,
147 format!(
148 "test target `{target_name}` is not a declared commons or context in this project",
149 ),
150 )
151 .with_note(
152 "the target of a `test` declaration must be a commons or context declared elsewhere in the project",
153 ),
154 );
155 continue;
156 }
157 };
158
159 let mut seen_cases: HashMap<String, Span> = HashMap::new();
161 let mut had_dup = false;
162 for &i in indices {
163 if let Some(t) = parsed[i].test() {
164 for case in &t.cases {
165 if let Some(prev) = seen_cases.get(&case.name) {
166 had_dup = true;
167 errors.push(
168 CompileError::new(
169 "bynk.suite.duplicate_case_name",
170 case.name_span,
171 format!(
172 "test case `\"{}\"` is declared more than once in tests targeting `{target_name}`",
173 case.name
174 ),
175 )
176 .with_label(*prev, "previously declared here"),
177 );
178 } else {
179 seen_cases.insert(case.name.clone(), case.name_span);
180 }
181 }
182 }
183 }
184
185 let target_stubs = resolve_stubs(
194 target_name,
195 target_kind,
196 indices,
197 parsed,
198 unit_tables,
199 unit_consumes,
200 errors,
201 );
202
203 if had_dup {
204 continue;
206 }
207
208 let bodies_errs = check_test_bodies(
212 target_name,
213 target_kind,
214 indices,
215 parsed,
216 &target_stubs,
217 unit_tables,
218 exports_visibility,
219 unit_consumes,
220 unit_consumes_aliases,
221 unit_uses,
222 refs,
223 tys,
224 );
225 let bodies_failed = !bodies_errs.is_empty();
226 errors.extend(bodies_errs);
227
228 if bodies_failed {
229 continue;
230 }
231
232 ready.insert(target_name.clone(), target_stubs);
233 }
234
235 ready
236}
237
238fn resolve_stubs(
245 target_name: &str,
246 target_kind: UnitKind,
247 indices: &[usize],
248 parsed: &[ParsedFile],
249 unit_tables: &HashMap<String, UnitTable>,
250 unit_consumes: &HashMap<String, Vec<String>>,
251 errors: &mut Vec<CompileError>,
252) -> HashMap<String, ResolvedStub> {
253 let target_table = unit_tables.get(target_name);
254 let target_consumed = unit_consumes.get(target_name).cloned().unwrap_or_default();
255
256 let mut collected: Vec<(StubClause, Option<String>, PathBuf)> = Vec::new();
259 for &i in indices {
260 let Some(t) = parsed[i].test() else { continue };
261 for case in &t.cases {
262 for pc in &case.stubs {
263 collected.push((
264 pc.clone(),
265 Some(case.name.clone()),
266 parsed[i].identity_path(),
267 ));
268 }
269 }
270 }
271 for &i in indices {
272 let Some(t) = parsed[i].test() else { continue };
273 for pc in &t.stubs {
274 collected.push((pc.clone(), None, parsed[i].identity_path()));
275 }
276 }
277
278 let resolve_cap = |name: &str| -> Option<CapabilityDecl> {
282 target_table
283 .and_then(|t| t.capabilities.get(name).cloned())
284 .or_else(|| {
285 target_consumed.iter().find_map(|q| {
286 unit_tables
287 .get(q)
288 .and_then(|t| t.capabilities.get(name).cloned())
289 })
290 })
291 };
292
293 let mut out: HashMap<String, ResolvedStub> = HashMap::new();
294 for (pc, case, identity_path) in collected {
295 let cap_name = pc.capability.name.clone();
296 let Some(cap_decl) = resolve_cap(&cap_name) else {
297 let note = if target_kind == UnitKind::Commons {
300 "commons have no capability seams — `stub` overrides a capability the target context declares or consumes"
301 } else {
302 "a `stub` clause names a capability the target context declares or reaches through a consumed context"
303 };
304 errors.push(
305 CompileError::new(
306 "bynk.stub.not_a_seam",
307 pc.capability.span,
308 format!("`{cap_name}` is not a capability seam of `{target_name}`",),
309 )
310 .with_note(note),
311 );
312 continue;
313 };
314 let Some(op_decl) = cap_decl.ops.iter().find(|o| o.name.name == pc.method.name) else {
315 errors.push(CompileError::new(
316 "bynk.stub.unknown_op",
317 pc.method.span,
318 format!(
319 "`{}` is not an operation of capability `{cap_name}`",
320 pc.method.name
321 ),
322 ));
323 continue;
324 };
325 if !op_decl.type_params.is_empty() {
333 errors.push(
334 CompileError::new(
335 "bynk.stub.generic_op",
336 pc.method.span,
337 format!(
338 "`{cap_name}.{}` declares its own type parameter — a generic capability operation cannot be stubbed at v1",
339 pc.method.name
340 ),
341 )
342 .with_note(
343 "test through the capability's real (external) provider instead, or restructure the test to avoid stubbing this operation",
344 ),
345 );
346 continue;
347 }
348 if let StubRhs::ReturnsEach(outcomes, span) = &pc.rhs
349 && outcomes.is_empty()
350 {
351 errors.push(CompileError::new(
352 "bynk.stub.bad_sequence",
353 *span,
354 format!(
355 "`stub {cap_name}.{} returns each []` has no outcomes — a sequence needs at least one",
356 pc.method.name
357 ),
358 ));
359 continue;
360 }
361 let entry = out.entry(cap_name.clone()).or_insert_with(|| ResolvedStub {
362 cap: cap_name.clone(),
363 cap_decl: cap_decl.clone(),
364 clauses: Vec::new(),
365 clause_cases: Vec::new(),
366 identity_path: identity_path.clone(),
367 });
368 entry.clauses.push(pc);
369 entry.clause_cases.push(case);
370 }
371 out
372}
373
374pub fn infer_participants(
379 target: &str,
380 unit_consumes: &HashMap<String, Vec<String>>,
381) -> Vec<String> {
382 let mut seen: HashSet<String> = HashSet::new();
383 let mut order: Vec<String> = Vec::new();
384 let mut queue: Vec<String> = vec![target.to_string()];
385 seen.insert(target.to_string());
386 let mut head = 0;
387 while head < queue.len() {
388 let node = queue[head].clone();
389 head += 1;
390 order.push(node.clone());
391 if let Some(deps) = unit_consumes.get(&node) {
392 for d in deps {
393 if seen.insert(d.clone()) {
394 queue.push(d.clone());
395 }
396 }
397 }
398 }
399 order
400}
401
402#[allow(clippy::too_many_arguments)]
422pub fn phase_integration_bodies(
423 integration_groups: &BTreeMap<String, Vec<usize>>,
424 parsed: &[ParsedFile],
425 unit_tables: &HashMap<String, UnitTable>,
426 unit_consumes: &HashMap<String, Vec<String>>,
427 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
428 unit_uses: &HashMap<String, Vec<String>>,
429 errors: &mut Vec<CompileError>,
430 refs: &mut RefSink,
431 tys: &Arc<Types>,
432) -> HashMap<String, resolver::CrossContextInfo> {
433 let mut ready: HashMap<String, resolver::CrossContextInfo> = HashMap::new();
434
435 let mut sorted: Vec<&String> = integration_groups.keys().collect();
436 sorted.sort();
437
438 for group_name in sorted {
439 let indices = integration_groups.get(group_name).unwrap();
440 let first = indices[0];
441 let Some(decl) = parsed[first].integration() else {
442 continue;
443 };
444 let suite_target = decl.target.joined();
448 let participants = infer_participants(&suite_target, unit_consumes);
449
450 let mut bad = false;
451
452 let has_serialisation_edge = unit_tables.get(&suite_target).is_some_and(|t| {
467 t.services
468 .values()
469 .any(|s| matches!(s.protocol, bynk_syntax::ast::ServiceProtocol::Http))
470 });
471 if participants.len() < 2 && !has_serialisation_edge {
472 errors.push(
473 CompileError::new(
474 "bynk.tier.system_needs_wire",
475 decl.target.span,
476 format!(
477 "`system`-tier suite for `{suite_target}` has no serialisation edge — the target consumes no other context and exposes no `http` service",
478 ),
479 )
480 .with_note(
481 "a `system` case crosses a real serialise → JSON → deserialise boundary; this target has none to cross, so `unit` already covers it",
482 ),
483 );
484 bad = true;
485 }
486
487 let mut seen_cases: HashMap<String, Span> = HashMap::new();
489 for &i in indices {
490 let Some(d) = parsed[i].integration() else {
491 continue;
492 };
493 for case in &d.cases {
494 if let Some(prev) = seen_cases.get(&case.name) {
495 errors.push(
496 CompileError::new(
497 "bynk.suite.duplicate_case_name",
498 case.name_span,
499 format!(
500 "test case `\"{}\"` is declared more than once in tests targeting `{suite_target}`",
501 case.name
502 ),
503 )
504 .with_label(*prev, "previously declared here"),
505 );
506 bad = true;
507 } else {
508 seen_cases.insert(case.name.clone(), case.name_span);
509 }
510 }
511 }
512
513 for &i in indices {
523 let Some(d) = parsed[i].integration() else {
524 continue;
525 };
526 let suite_is_system = d.tier == Some(bynk_syntax::ast::TestTier::System);
527 let has_lower = d
528 .cases
529 .iter()
530 .any(|c| case_effective_tier(c, d) != bynk_syntax::ast::TestTier::System);
531 if !has_lower {
532 continue;
533 }
534 for case in &d.cases {
535 let tier = case_effective_tier(case, d);
536 let flagged = if suite_is_system {
539 tier != bynk_syntax::ast::TestTier::System
540 } else {
541 case.tier == Some(bynk_syntax::ast::TestTier::System)
542 };
543 if !flagged {
544 continue;
545 }
546 let (message, advice) = if suite_is_system {
547 (
548 format!(
549 "case `\"{}\"` is `{}`-tier, but its suite is `as system`",
550 case.name,
551 tier.as_str()
552 ),
553 format!(
554 "move this case into a separate `suite {suite_target}` without `as system`"
555 ),
556 )
557 } else {
558 (
559 format!(
560 "case `\"{}\"` is `as system`, but other cases in its suite run below `system`",
561 case.name
562 ),
563 format!(
564 "move this case into its own `suite {suite_target} as system {{ … }}`, addressing services by context path (`{suite_target}.<service>(…)`)"
565 ),
566 )
567 };
568 errors.push(
569 CompileError::new("bynk.tier.mixed_system_suite", case.name_span, message)
570 .with_note(format!(
571 "a `system` case runs against deployed Workers, so its suite holds `system` cases only; {advice}"
572 )),
573 );
574 }
575 }
576
577 if bad {
578 continue;
579 }
580
581 let harness_name = group_name.clone();
583 let mut uses_targets: Vec<String> = Vec::new();
584 for &i in indices {
585 if let Some(d) = parsed[i].integration() {
586 for u in &d.uses {
587 let q = u.target.joined();
588 if !uses_targets.contains(&q) {
589 uses_targets.push(q);
590 }
591 }
592 }
593 }
594 let mut harness_consumes = unit_consumes.clone();
595 harness_consumes.insert(harness_name.clone(), participants.clone());
596 let mut harness_uses = unit_uses.clone();
597 harness_uses.insert(harness_name.clone(), uses_targets.clone());
598 let cross_context = build_cross_context_info(
599 &harness_name,
600 &harness_consumes,
601 unit_consumes_aliases,
602 &harness_uses,
603 unit_tables,
604 );
605
606 let mut body_errs: Vec<CompileError> = Vec::new();
608 let mut harness_resolution = uses_targets.clone();
611 harness_resolution.extend(participants.iter().cloned());
612 refs.declare_namespace(&harness_name, harness_resolution);
613 for &i in indices {
614 let Some(d) = parsed[i].integration() else {
615 continue;
616 };
617 refs.enter_file(
618 &parsed[i].identity_path(),
619 &harness_name,
620 parsed[i].is_synthetic(),
621 );
622 for case in &d.cases {
623 check_integration_case_body(
624 &participants,
625 &uses_targets,
626 case,
627 &cross_context,
628 unit_tables,
629 &mut body_errs,
630 refs,
631 tys,
632 );
633 check_faults_tier(case, case_effective_tier(case, d), &mut body_errs);
634 if !matches!(
639 case_effective_tier(case, d),
640 bynk_syntax::ast::TestTier::System
641 ) && block_uses_wire(&case.body)
642 {
643 body_errs.push(CompileError::new(
644 "bynk.test.wire_needs_system",
645 case.name_span,
646 format!(
647 "case `\"{}\"` uses `Wire(...)` but is not a `system`-tier case",
648 case.name
649 ),
650 ).with_note(
651 "`Wire` hands raw, pre-validation input to the real boundary; promote the case with `as system`, or pass a typed argument",
652 ));
653 }
654 if !matches!(
658 case_effective_tier(case, d),
659 bynk_syntax::ast::TestTier::System
660 ) && block_uses_nobody(&case.body)
661 {
662 body_errs.push(CompileError::new(
663 "bynk.test.credential_needs_system",
664 case.name_span,
665 format!(
666 "case `\"{}\"` drives `by Nobody` but is not a `system`-tier case",
667 case.name
668 ),
669 ).with_note(
670 "`by Nobody` presents no credential to the real auth seam (the 401 path), which exists only at `system`; promote the case with `as system`, or supply `by <Actor>(<identity>)`",
671 ));
672 }
673 }
674 }
675 let bodies_failed = !body_errs.is_empty();
676 errors.extend(body_errs);
677 if bodies_failed {
678 continue;
679 }
680
681 ready.insert(group_name.clone(), cross_context);
682 }
683
684 ready
685}
686
687#[allow(clippy::too_many_arguments)]
693fn check_integration_case_body(
694 participants: &[String],
695 uses_targets: &[String],
696 case: &Case,
697 cross_context: &resolver::CrossContextInfo,
698 unit_tables: &HashMap<String, UnitTable>,
699 errors: &mut Vec<CompileError>,
700 refs: &mut RefSink,
701 tys: &Arc<Types>,
702) {
703 let mut types: HashMap<String, Arc<TypeDecl>> = HashMap::new();
707 let mut fns: HashMap<String, Arc<FnDecl>> = HashMap::new();
708 let mut methods: HashMap<String, ResolverMethodTable> = HashMap::new();
709 let mut merge = |src: Option<&UnitTable>, with_fns: bool| {
710 let Some(t) = src else { return };
711 for (n, d) in &t.types {
712 types.entry(n.clone()).or_insert_with(|| d.clone());
713 }
714 if with_fns {
715 for (n, f) in &t.fns {
716 fns.entry(n.clone()).or_insert_with(|| f.clone());
717 }
718 }
719 for (n, mt) in &t.methods {
720 let entry = methods.entry(n.clone()).or_default();
721 for (m, decl) in &mt.instance {
722 entry
723 .instance
724 .entry(m.clone())
725 .or_insert_with(|| decl.clone());
726 }
727 for (m, decl) in &mt.statics {
728 entry
729 .statics
730 .entry(m.clone())
731 .or_insert_with(|| decl.clone());
732 }
733 }
734 };
735 for u in uses_targets {
736 merge(unit_tables.get(u), true);
737 }
738 for p in participants {
739 merge(unit_tables.get(p), false);
740 }
741
742 let synthetic_commons = Commons {
743 name: QualifiedName {
744 parts: vec![Ident {
745 name: "integration".to_string(),
746 span: Span::default(),
747 }],
748 span: Span::default(),
749 },
750 items: Vec::new(),
751 uses: Vec::new(),
752 documentation: None,
753 form: CommonsForm::Brace,
754 span: Span::default(),
755 trivia: Trivia::default(),
756 trailing_comments: Vec::new(),
757 };
758 let no_local_types = HashMap::new();
763 let no_local_events = HashMap::new();
764 let resolved = ResolvedCommons::new(
765 synthetic_commons,
766 types,
767 &no_local_types,
768 fns,
769 methods,
770 HashMap::new(),
771 &no_local_events,
772 cross_context.clone(),
773 HashMap::new(),
774 false,
776 HashSet::new(),
777 );
778
779 let unit_span = case.span;
780 let synthetic_return = TypeRef::Effect(
781 Box::new(TypeRef::Result(
782 Box::new(TypeRef::Unit(unit_span)),
783 Box::new(TypeRef::ValidationError(unit_span)),
784 unit_span,
785 )),
786 unit_span,
787 );
788 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
789 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
790 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
791 let mut no_hints = HintSink::new();
793 let mut no_locals = LocalsSink::new();
794 let mut no_requirements = RequirementSink::new();
796 let _ = checker::check_body(
797 &resolved,
798 &case.body,
799 return_ty,
800 case.span,
801 HashMap::new(),
802 checker::CapabilityCtx::default(),
803 target_test_services(participants.first().and_then(|t| unit_tables.get(t))),
807 target_test_actors(participants.first().and_then(|t| unit_tables.get(t))),
808 None,
809 checker::CheckSinks {
810 tys,
811 expr_types: &mut expr_types,
812 errors,
813 refs,
814 hints: &mut no_hints,
815 locals: &mut no_locals,
816 requirements: &mut no_requirements,
817 callees: &mut callees,
818 },
819 );
820}
821
822fn first_test_target_span(indices: &[usize], parsed: &[ParsedFile]) -> Span {
823 indices
824 .first()
825 .and_then(|&i| parsed[i].test().map(|t| t.target.span))
826 .unwrap_or_default()
827}
828
829#[allow(clippy::too_many_arguments)]
834fn check_test_bodies(
835 target_name: &str,
836 target_kind: UnitKind,
837 indices: &[usize],
838 parsed: &[ParsedFile],
839 stubs: &HashMap<String, ResolvedStub>,
840 unit_tables: &HashMap<String, UnitTable>,
841 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
842 unit_consumes: &HashMap<String, Vec<String>>,
843 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
844 unit_uses: &HashMap<String, Vec<String>>,
845 refs: &mut RefSink,
846 tys: &Arc<Types>,
847) -> Vec<CompileError> {
848 let mut errors = Vec::new();
849 let _ = exports_visibility;
850
851 if !stubs.is_empty()
856 && let Some((resolved, _)) = build_privileged_resolved(
857 target_name,
858 unit_tables,
859 unit_uses,
860 unit_consumes,
861 unit_consumes_aliases,
862 )
863 {
864 for rp in stubs.values() {
865 refs.enter_file(&rp.identity_path, target_name, false);
866 for clause in &rp.clauses {
867 let Some(op) = rp
868 .cap_decl
869 .ops
870 .iter()
871 .find(|o| o.name.name == clause.method.name)
872 else {
873 continue;
874 };
875 let check_value = |e: &Expr, errors: &mut Vec<CompileError>| {
876 if !stub_value_typechecks(e, op, &resolved, tys) {
877 errors.push(CompileError::new(
878 "bynk.stub.rhs_type",
879 e.span,
880 format!(
881 "the value provided for `{}.{}` does not match the operation's declared return type `{}`",
882 rp.cap,
883 op.name.name,
884 ts_type_ref_display(&op.return_type),
885 ),
886 ));
887 }
888 };
889 match &clause.rhs {
890 StubRhs::Returns(e) => check_value(e, &mut errors),
891 StubRhs::ReturnsEach(outcomes, _) => {
892 for o in outcomes {
893 if let SeqOutcome::Value(e) = o {
894 check_value(e, &mut errors);
895 }
896 }
897 }
898 StubRhs::Fails(_) => {}
899 }
900 }
901 }
902 }
903
904 let crossing = CrossingServices::of(
907 target_name,
908 unit_tables,
909 unit_consumes,
910 unit_consumes_aliases,
911 );
912
913 for &i in indices {
916 let Some(test_decl) = parsed[i].test() else {
917 continue;
918 };
919 refs.enter_file(
922 &parsed[i].identity_path(),
923 target_name,
924 parsed[i].is_synthetic(),
925 );
926 for case in &test_decl.cases {
927 check_test_case_body(
928 target_name,
929 target_kind,
930 case,
931 unit_tables,
932 unit_uses,
933 unit_consumes,
934 unit_consumes_aliases,
935 &mut errors,
936 refs,
937 tys,
938 );
939 let tier = case_effective_tier(case, test_decl);
940 check_faults_tier(case, tier, &mut errors);
941 if tier != bynk_syntax::ast::TestTier::System {
942 crossing.check_case(target_name, case, tier, &mut errors);
943 }
944 }
945 for prop in &test_decl.properties {
952 check_property_body(
953 target_name,
954 target_kind,
955 prop,
956 unit_tables,
957 unit_uses,
958 unit_consumes,
959 unit_consumes_aliases,
960 &mut errors,
961 refs,
962 tys,
963 );
964 crossing.check_property(target_name, prop, &mut errors);
965 }
966 }
967
968 errors
969}
970
971pub fn value_block(e: &Expr) -> Block {
981 Block {
982 statements: Vec::new(),
983 tail: Box::new(e.clone()),
984 span: e.span,
985 tail_leading_comments: Vec::new(),
986 implicit_tail: false,
987 }
988}
989
990fn stub_value_typechecks(
995 e: &Expr,
996 op: &CapabilityOp,
997 resolved: &ResolvedCommons,
998 tys: &Arc<Types>,
999) -> bool {
1000 let block = value_block(e);
1001 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
1002 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
1003 let mut errs: Vec<CompileError> = Vec::new();
1004 checker::check_handler_body(
1005 resolved,
1006 checker::HandlerBodyCheck::new(&block, &op.return_type, &op.params, &[]),
1007 checker::CheckSinks {
1008 tys,
1009 expr_types: &mut expr_types,
1010 errors: &mut errs,
1011 refs: &mut RefSink::new(),
1012 hints: &mut HintSink::new(),
1013 locals: &mut LocalsSink::new(),
1014 requirements: &mut RequirementSink::new(),
1015 callees: &mut callees,
1016 },
1017 );
1018 errs.is_empty()
1019}
1020
1021fn block_uses_wire(block: &Block) -> bool {
1025 fn contains_wire(e: &Expr) -> bool {
1032 matches!(e.kind, ExprKind::Wire(_))
1033 || bynk_syntax::ast::expr_children(e)
1034 .into_iter()
1035 .any(contains_wire)
1036 }
1037 for s in &block.statements {
1038 let e = match s {
1039 Statement::Let(l) => &l.value,
1040 Statement::EffectLet(l) => &l.value,
1041 Statement::Expect(x) => &x.value,
1042 Statement::Send(x) => &x.value,
1043 Statement::Do(d) => &d.value,
1044 Statement::Assign(a) => &a.value,
1045 };
1046 if contains_wire(e) {
1047 return true;
1048 }
1049 }
1050 contains_wire(&block.tail)
1051}
1052
1053type CrossCall = (String, String);
1055
1056type AgentHandler = (String, String);
1058
1059struct CrossingServices<'a> {
1079 services: HashMap<String, CrossCall>,
1081 agents: HashMap<AgentHandler, CrossCall>,
1083 table: Option<&'a UnitTable>,
1085 consumed: &'a [String],
1087 aliases: Option<&'a HashMap<String, String>>,
1088 unit_tables: &'a HashMap<String, UnitTable>,
1089}
1090
1091impl<'a> CrossingServices<'a> {
1092 fn of(
1093 target_name: &str,
1094 unit_tables: &'a HashMap<String, UnitTable>,
1095 unit_consumes: &'a HashMap<String, Vec<String>>,
1096 unit_consumes_aliases: &'a HashMap<String, HashMap<String, String>>,
1097 ) -> Self {
1098 let mut this = CrossingServices {
1099 services: HashMap::new(),
1100 agents: HashMap::new(),
1101 table: unit_tables.get(target_name),
1102 consumed: unit_consumes
1103 .get(target_name)
1104 .map(Vec::as_slice)
1105 .unwrap_or(&[]),
1106 aliases: unit_consumes_aliases.get(target_name),
1107 unit_tables,
1108 };
1109 let Some(table) = this.table else {
1110 return this;
1111 };
1112 if this.consumed.is_empty() {
1113 return this;
1114 }
1115 let mut agent_edges: HashMap<AgentHandler, Vec<AgentHandler>> = HashMap::new();
1118 for (agent, decl) in &table.agents {
1119 for h in &decl.handlers {
1120 let Some(method) = &h.method_name else {
1121 continue;
1122 };
1123 let key = (agent.clone(), method.name.clone());
1124 let (hit, edges) = this.scan(&h.body);
1125 if let Some(hit) = hit {
1126 this.agents.insert(key.clone(), hit);
1127 }
1128 agent_edges.insert(key, edges);
1129 }
1130 }
1131 loop {
1132 let mut changed = false;
1133 for (key, edges) in &agent_edges {
1134 if this.agents.contains_key(key) {
1135 continue;
1136 }
1137 if let Some(hit) = edges.iter().find_map(|e| this.agents.get(e).cloned()) {
1138 this.agents.insert(key.clone(), hit);
1139 changed = true;
1140 }
1141 }
1142 if !changed {
1143 break;
1144 }
1145 }
1146 for (name, decl) in &table.services {
1147 for h in &decl.handlers {
1148 let (hit, edges) = this.scan(&h.body);
1149 let reached =
1150 hit.or_else(|| edges.iter().find_map(|e| this.agents.get(e).cloned()));
1151 if let Some(reached) = reached {
1152 this.services.entry(name.clone()).or_insert(reached);
1153 }
1154 }
1155 }
1156 this
1157 }
1158
1159 fn resolve(&self, chain: &str) -> Option<&String> {
1161 self.aliases
1162 .and_then(|a| a.get(chain))
1163 .or_else(|| self.consumed.iter().find(|c| *c == chain))
1164 }
1165
1166 fn cross_call(&self, e: &Expr) -> Option<CrossCall> {
1168 let ExprKind::MethodCall {
1169 receiver, method, ..
1170 } = &e.kind
1171 else {
1172 return None;
1173 };
1174 let ctx = self.resolve(&receiver_chain(receiver)?)?;
1175 let callee = self.unit_tables.get(ctx)?;
1176 callee
1177 .services
1178 .contains_key(&method.name)
1179 .then(|| (ctx.clone(), method.name.clone()))
1180 }
1181
1182 fn dispatch(&self, e: &Expr, bound: &HashMap<String, String>) -> Option<AgentHandler> {
1185 let table = self.table?;
1186 let ExprKind::MethodCall {
1187 receiver, method, ..
1188 } = &e.kind
1189 else {
1190 return None;
1191 };
1192 let agent = match &receiver.kind {
1193 ExprKind::Call { name, .. } if table.agents.contains_key(&name.name) => {
1194 name.name.clone()
1195 }
1196 ExprKind::Ident(x) => bound.get(&x.name)?.clone(),
1197 _ => return None,
1198 };
1199 Some((agent, method.name.clone()))
1200 }
1201
1202 fn scan(&self, block: &Block) -> (Option<CrossCall>, Vec<AgentHandler>) {
1205 let bound = self.agent_bindings(block);
1206 let mut hit = None;
1207 let mut edges = Vec::new();
1208 for e in exprs_in_order(block) {
1209 if hit.is_none() {
1210 hit = self.cross_call(e);
1211 }
1212 if let Some(edge) = self.dispatch(e, &bound) {
1213 edges.push(edge);
1214 }
1215 }
1216 (hit, edges)
1217 }
1218
1219 fn agent_bindings(&self, block: &Block) -> HashMap<String, String> {
1222 let mut bound = HashMap::new();
1223 let Some(table) = self.table else {
1224 return bound;
1225 };
1226 for b in blocks_deep(block) {
1227 for s in &b.statements {
1228 if let Statement::Let(l) | Statement::EffectLet(l) = s
1229 && let ExprKind::Call { name, .. } = &l.value.kind
1230 && table.agents.contains_key(&name.name)
1231 {
1232 bound.insert(l.name.name.clone(), name.name.clone());
1233 }
1234 }
1235 }
1236 bound
1237 }
1238
1239 fn check_block(
1245 &self,
1246 target_name: &str,
1247 subject: &str,
1248 below: &str,
1249 body: &Block,
1250 errors: &mut Vec<CompileError>,
1251 ) {
1252 if self.consumed.is_empty() {
1253 return;
1254 }
1255 let bound = self.agent_bindings(body);
1256 let shadowed: HashSet<String> = blocks_deep(body)
1258 .iter()
1259 .flat_map(|b| &b.statements)
1260 .filter_map(|s| match s {
1261 Statement::Let(l) | Statement::EffectLet(l) => Some(l.name.name.clone()),
1262 _ => None,
1263 })
1264 .collect();
1265 for e in exprs_in_order(body) {
1266 let ExprKind::MethodCall { receiver, .. } = &e.kind else {
1267 continue;
1268 };
1269 let through = match &receiver.kind {
1270 ExprKind::Ident(svc) if !shadowed.contains(&svc.name) => self
1272 .services
1273 .get(&svc.name)
1274 .map(|reached| (format!("`{}`", svc.name), Some(svc.name.clone()), reached)),
1275 _ => None,
1276 };
1277 let through = through.or_else(|| {
1278 let (agent, handler) = self.dispatch(e, &bound)?;
1279 let reached = self.agents.get(&(agent.clone(), handler.clone()))?;
1280 Some((format!("`{agent}.{handler}`"), None, reached))
1281 });
1282 let message = match through {
1283 Some((via, _, (ctx, svc))) => {
1284 format!(
1285 "{subject} calls {via}, which calls `{ctx}.{svc}` in another context, but {below}"
1286 )
1287 }
1288 None => match self.cross_call(e) {
1289 Some((ctx, svc)) => {
1290 format!("{subject} calls `{ctx}.{svc}` in another context, but {below}")
1291 }
1292 None => continue,
1293 },
1294 };
1295 let service = match &receiver.kind {
1296 ExprKind::Ident(svc) if self.services.contains_key(&svc.name) => {
1297 Some(svc.name.clone())
1298 }
1299 _ => None,
1300 };
1301 let promote = match service {
1302 Some(s) => format!(
1303 "move it into a `system` suite (`suite {target_name} as system {{ … }}`), where the service is addressed by its context path (`{target_name}.{s}(…)`)"
1304 ),
1305 None => format!(
1306 "move it into a `system` suite (`suite {target_name} as system {{ … }}`) and drive it through one of `{target_name}`'s services (`{target_name}.<service>(…)`), which reaches the other context over the real wire"
1307 ),
1308 };
1309 errors.push(
1310 CompileError::new("bynk.tier.cross_context_needs_system", e.span, message)
1311 .with_note(format!(
1312 "below `system` a test runs in-process, with no other context stood up to call (`stub` doubles capabilities, not a context's services); {promote}"
1313 )),
1314 );
1315 }
1316 }
1317
1318 fn check_case(
1320 &self,
1321 target_name: &str,
1322 case: &Case,
1323 tier: TestTier,
1324 errors: &mut Vec<CompileError>,
1325 ) {
1326 let tier = tier.as_str();
1327 let article = if tier == "integration" { "an" } else { "a" };
1329 self.check_block(
1330 target_name,
1331 &format!("case `\"{}\"`", case.name),
1332 &format!("it is {article} `{tier}`-tier case"),
1333 &case.body,
1334 errors,
1335 );
1336 }
1337
1338 fn check_property(
1340 &self,
1341 target_name: &str,
1342 prop: &PropertyDecl,
1343 errors: &mut Vec<CompileError>,
1344 ) {
1345 self.check_block(
1346 target_name,
1347 &format!("property `\"{}\"`", prop.name),
1348 "a property runs in-process (it generates, and has no `system` tier)",
1349 &prop.forall.body,
1350 errors,
1351 );
1352 }
1353}
1354
1355fn receiver_chain(e: &Expr) -> Option<String> {
1358 match &e.kind {
1359 ExprKind::Ident(id) => Some(id.name.clone()),
1360 ExprKind::FieldAccess { receiver, field } => {
1361 Some(format!("{}.{}", receiver_chain(receiver)?, field.name))
1362 }
1363 _ => None,
1364 }
1365}
1366
1367fn exprs_in_order(block: &Block) -> Vec<&Expr> {
1371 let mut roots: Vec<&Expr> = Vec::new();
1372 for s in &block.statements {
1373 bynk_syntax::ast::statement_exprs(s, &mut roots);
1374 }
1375 roots.push(&block.tail);
1376 let mut stack: Vec<&Expr> = roots.into_iter().rev().collect();
1378 let mut out = Vec::new();
1379 while let Some(e) = stack.pop() {
1380 out.push(e);
1381 stack.extend(bynk_syntax::ast::expr_children(e).into_iter().rev());
1382 }
1383 out
1384}
1385
1386fn blocks_deep(block: &Block) -> Vec<&Block> {
1389 let mut out = vec![block];
1390 for e in exprs_in_order(block) {
1391 match &e.kind {
1392 ExprKind::Block(b) => out.push(b),
1393 ExprKind::If {
1394 then_block,
1395 else_block,
1396 ..
1397 } => {
1398 out.push(then_block);
1399 out.push(else_block);
1400 }
1401 ExprKind::Match { arms, .. } => {
1402 for arm in arms {
1403 if let MatchBody::Block(b) = &arm.body {
1404 out.push(b);
1405 }
1406 }
1407 }
1408 _ => {}
1409 }
1410 }
1411 out
1412}
1413
1414fn block_uses_faults(block: &Block) -> bool {
1420 fn contains_faults(e: &Expr) -> bool {
1421 matches!(e.kind, ExprKind::Faults(_))
1422 || bynk_syntax::ast::expr_children(e)
1423 .into_iter()
1424 .any(contains_faults)
1425 }
1426 let mut exprs = Vec::new();
1427 for s in &block.statements {
1428 bynk_syntax::ast::statement_exprs(s, &mut exprs);
1429 }
1430 exprs.into_iter().any(contains_faults) || contains_faults(&block.tail)
1431}
1432
1433fn check_faults_tier(
1436 case: &Case,
1437 tier: bynk_syntax::ast::TestTier,
1438 errors: &mut Vec<CompileError>,
1439) {
1440 if tier == bynk_syntax::ast::TestTier::System && block_uses_faults(&case.body) {
1441 errors.push(
1442 CompileError::new(
1443 "bynk.test.faults_needs_in_process",
1444 case.name_span,
1445 format!(
1446 "case `\"{}\"` claims a fault with `faults`, but is a `system`-tier case",
1447 case.name
1448 ),
1449 )
1450 .with_note(
1451 "at `system` a handler's fault reaches the case as an error response from the deployed Worker, not a throw; claim the fault at `unit` or `integration`, or assert the response at `system`",
1452 ),
1453 );
1454 }
1455}
1456
1457fn block_uses_nobody(block: &Block) -> bool {
1462 block.statements.iter().any(|s| {
1463 matches!(s, Statement::EffectLet(l)
1464 if l.principal.as_ref().is_some_and(|p| p.actor.name == "Nobody"))
1465 })
1466}
1467
1468pub fn register_call_record_types(
1473 resolved: &mut ResolvedCommons,
1474 target_name: &str,
1475 unit_tables: &HashMap<String, UnitTable>,
1476) {
1477 let Some(table) = unit_tables.get(target_name) else {
1478 return;
1479 };
1480 let mut caps: Vec<(&String, &CapabilityDecl)> = table.capabilities.iter().collect();
1483 for (n, d) in flattened_platform_capabilities(unit_tables, resolved) {
1484 if !table.capabilities.contains_key(n) {
1485 caps.push((n, d));
1486 }
1487 }
1488 for (cap_name, decl) in caps {
1489 for op in &decl.ops {
1490 let fields: Vec<RecordField> = op
1491 .params
1492 .iter()
1493 .map(|p| RecordField {
1494 trivia: Default::default(),
1495 name: p.name.clone(),
1496 type_ref: p.type_ref.clone(),
1497 refinement: None,
1498 init: None,
1499 span: p.span,
1500 })
1501 .collect();
1502 let name = checker::call_record_type_name(cap_name, &op.name.name);
1503 resolved.types.insert(
1504 name.clone(),
1505 Arc::new(TypeDecl {
1506 type_params: Vec::new(),
1507 name: Ident {
1508 name,
1509 span: op.name.span,
1510 },
1511 body: TypeBody::Record(RecordBody {
1512 trailing_comments: Default::default(),
1513 fields,
1514 span: op.name.span,
1515 }),
1516 documentation: None,
1517 span: op.name.span,
1518 trivia: Trivia::default(),
1519 }),
1520 );
1521 }
1522 }
1523}
1524
1525fn target_test_actors(table: Option<&UnitTable>) -> HashMap<String, bynk_syntax::ast::ActorDecl> {
1526 table.map(|t| t.actors.clone()).unwrap_or_default()
1527}
1528
1529fn target_test_services(table: Option<&UnitTable>) -> HashMap<String, checker::TestServiceSig> {
1530 use bynk_syntax::ast::ServiceProtocol;
1531 let Some(t) = table else {
1532 return HashMap::new();
1533 };
1534 t.services
1535 .iter()
1536 .map(|(name, decl)| {
1537 let protocol = match &decl.protocol {
1538 ServiceProtocol::Call => None,
1539 ServiceProtocol::Http => Some("http".to_string()),
1540 ServiceProtocol::Cron => Some("cron".to_string()),
1541 ServiceProtocol::Queue { .. } => Some("queue".to_string()),
1542 ServiceProtocol::WebSocket { .. } => Some("websocket".to_string()),
1543 ServiceProtocol::Events { .. } => Some("events".to_string()),
1544 };
1545 let handlers = decl
1546 .handlers
1547 .iter()
1548 .map(|h| checker::TestHandler {
1549 kind: h.kind.clone(),
1550 params: h.params.clone(),
1551 by_clause: h.by_clause.clone(),
1552 span: h.span,
1553 })
1554 .collect();
1555 (name.clone(), checker::TestServiceSig { protocol, handlers })
1556 })
1557 .collect()
1558}
1559
1560fn flattened_platform_capabilities<'a>(
1564 unit_tables: &'a HashMap<String, UnitTable>,
1565 resolved: &ResolvedCommons,
1566) -> Vec<(&'a String, &'a CapabilityDecl)> {
1567 let mut out: Vec<_> = resolved
1568 .cross_context
1569 .flattened_caps
1570 .iter()
1571 .filter_map(|(cap, owner)| {
1572 let t = unit_tables.get(owner)?;
1573 if t.kind != Some(UnitKind::Adapter) {
1574 return None;
1575 }
1576 t.capabilities.get_key_value(cap)
1577 })
1578 .collect();
1579 out.sort_by_key(|(cap, _)| cap.as_str());
1580 out
1581}
1582
1583fn add_consumed_adapter_capabilities(
1589 map: &mut HashMap<String, checker::CapabilityInfo>,
1590 unit_tables: &HashMap<String, UnitTable>,
1591 resolved: &ResolvedCommons,
1592 tys: &Arc<Types>,
1593) {
1594 for (name, decl) in flattened_platform_capabilities(unit_tables, resolved) {
1595 if map.contains_key(name) {
1596 continue;
1597 }
1598 let ops = decl
1599 .ops
1600 .iter()
1601 .map(|op| build_capability_op_info(op, &resolved.types, tys))
1602 .collect();
1603 map.insert(
1604 name.clone(),
1605 checker::CapabilityInfo {
1606 name: name.clone(),
1607 ops,
1608 },
1609 );
1610 }
1611}
1612
1613#[allow(clippy::too_many_arguments)]
1626pub fn typecheck_case_body(
1627 target_name: &str,
1628 body: &Block,
1629 unit_span: Span,
1630 unit_tables: &HashMap<String, UnitTable>,
1631 resolved: &ResolvedCommons,
1632 errors: &mut Vec<CompileError>,
1633 refs: &mut RefSink,
1634 initial_scope: HashMap<String, checker::TyId>,
1637 tys: &Arc<Types>,
1638) -> (
1639 HashMap<ExprId, checker::TypedExpr>,
1640 HashMap<ExprId, checker::Callee>,
1641) {
1642 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
1643 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
1644 let synthetic_return = TypeRef::Effect(
1648 Box::new(TypeRef::Result(
1649 Box::new(TypeRef::Unit(unit_span)),
1650 Box::new(TypeRef::ValidationError(unit_span)),
1651 unit_span,
1652 )),
1653 unit_span,
1654 );
1655
1656 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
1659 if let Some(table) = unit_tables.get(target_name) {
1660 for (name, decl) in &table.capabilities {
1661 let ops = decl
1662 .ops
1663 .iter()
1664 .map(|op| build_capability_op_info(op, &resolved.types, tys))
1665 .collect();
1666 capability_info_map.insert(
1667 name.clone(),
1668 checker::CapabilityInfo {
1669 name: name.clone(),
1670 ops,
1671 },
1672 );
1673 }
1674 }
1675 add_consumed_adapter_capabilities(&mut capability_info_map, unit_tables, resolved, tys);
1676
1677 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
1681
1682 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
1683 let return_ty_span = unit_span;
1684 let mut no_hints = HintSink::new();
1686 let mut no_locals = LocalsSink::new();
1687 let mut no_requirements = RequirementSink::new();
1689 let _ = checker::check_body(
1690 resolved,
1691 body,
1692 return_ty,
1693 return_ty_span,
1694 initial_scope,
1695 checker::CapabilityCtx {
1696 capabilities: capability_info_map.clone(),
1697 declared_capabilities: capability_info_map,
1698 given_remaining: given_declared.iter().cloned().collect(),
1699 given_used: HashSet::new(),
1700 given_entries: Vec::new(),
1701 given_anchor: None,
1702 },
1703 target_test_services(unit_tables.get(target_name)),
1704 target_test_actors(unit_tables.get(target_name)),
1705 None,
1706 checker::CheckSinks {
1707 tys,
1708 expr_types: &mut expr_types,
1709 errors,
1710 refs,
1711 hints: &mut no_hints,
1712 locals: &mut no_locals,
1713 requirements: &mut no_requirements,
1714 callees: &mut callees,
1715 },
1716 );
1717 (expr_types, callees)
1718}
1719
1720#[allow(clippy::too_many_arguments)]
1721fn check_test_case_body(
1722 target_name: &str,
1723 target_kind: UnitKind,
1724 case: &Case,
1725 unit_tables: &HashMap<String, UnitTable>,
1726 unit_uses: &HashMap<String, Vec<String>>,
1727 unit_consumes: &HashMap<String, Vec<String>>,
1728 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1729 errors: &mut Vec<CompileError>,
1730 refs: &mut RefSink,
1731 tys: &Arc<Types>,
1732) {
1733 let Some((mut resolved, _)) = build_privileged_resolved(
1734 target_name,
1735 unit_tables,
1736 unit_uses,
1737 unit_consumes,
1738 unit_consumes_aliases,
1739 ) else {
1740 return;
1741 };
1742 register_call_record_types(&mut resolved, target_name, unit_tables);
1743 let _ = target_kind;
1744 let _ = typecheck_case_body(
1745 target_name,
1746 &case.body,
1747 case.span,
1748 unit_tables,
1749 &resolved,
1750 errors,
1751 refs,
1752 HashMap::new(),
1753 tys,
1754 );
1755 check_restated_contract(&case.body, &resolved, errors);
1766}
1767
1768fn check_restated_contract(
1775 body: &Block,
1776 resolved: &ResolvedCommons,
1777 errors: &mut Vec<CompileError>,
1778) {
1779 let mut bound: HashMap<String, (&FnDecl, &[Expr])> = HashMap::new();
1782 for stmt in &body.statements {
1783 let (name, value) = match stmt {
1784 Statement::Let(l) | Statement::EffectLet(l) => (&l.name.name, &l.value),
1785 _ => continue,
1786 };
1787 if let ExprKind::Call {
1788 name: callee, args, ..
1789 } = &value.kind
1790 && let Some(f) = resolved.fns.get(&callee.name)
1791 && matches!(&f.name, FnName::Free(_))
1792 && !f.ensures.is_empty()
1793 && f.params.len() == args.len()
1794 {
1795 bound.insert(name.clone(), (f, args.as_slice()));
1796 }
1797 }
1798 if bound.is_empty() {
1799 return;
1800 }
1801 for stmt in &body.statements {
1802 let Statement::Expect(e) = stmt else { continue };
1803 for (result_name, (f, args)) in &bound {
1804 let result_ident = Expr {
1806 id: ExprId::SYNTHETIC,
1807 kind: ExprKind::Ident(Ident {
1808 name: result_name.clone(),
1809 span: e.span,
1810 }),
1811 span: e.span,
1812 };
1813 let mut subst: HashMap<&str, &Expr> = HashMap::new();
1814 subst.insert("result", &result_ident);
1815 for (p, a) in f.params.iter().zip(args.iter()) {
1816 subst.insert(p.name.name.as_str(), a);
1817 }
1818 for c in &f.ensures {
1819 if expr_alpha_eq_subst(&c.predicate, &e.value, &subst) {
1820 let FnName::Free(fname) = &f.name else {
1821 continue;
1822 };
1823 errors.push(
1824 CompileError::new(
1825 "bynk.contract.restated_by_test",
1826 e.span,
1827 format!(
1828 "this `expect` restates the `ensures {}` contract of `{}`, which is already checked at every call and by the runner",
1829 c.name.name, fname.name
1830 ),
1831 )
1832 .with_note(
1833 "a contract is checked everywhere for free — delete the restating test, or keep a `case` only for a specific witnessed value",
1834 ),
1835 );
1836 break;
1837 }
1838 }
1839 }
1840 }
1841}
1842
1843fn expr_alpha_eq_subst(pattern: &Expr, actual: &Expr, subst: &HashMap<&str, &Expr>) -> bool {
1849 if let ExprKind::Ident(id) = &pattern.kind
1850 && let Some(replacement) = subst.get(id.name.as_str())
1851 {
1852 return expr_struct_eq(replacement, actual);
1853 }
1854 match (&pattern.kind, &actual.kind) {
1855 (ExprKind::Ident(a), ExprKind::Ident(b)) => a.name == b.name,
1856 (ExprKind::IntLit { value: a, .. }, ExprKind::IntLit { value: b, .. }) => a == b,
1857 (ExprKind::BoolLit(a), ExprKind::BoolLit(b)) => a == b,
1858 (ExprKind::StrLit(a), ExprKind::StrLit(b)) => a == b,
1859 (ExprKind::Paren(a), _) => expr_alpha_eq_subst(a, actual, subst),
1860 (_, ExprKind::Paren(b)) => expr_alpha_eq_subst(pattern, b, subst),
1861 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1862 oa == ob && expr_alpha_eq_subst(la, lb, subst) && expr_alpha_eq_subst(ra, rb, subst)
1863 }
1864 (ExprKind::UnaryOp(oa, a), ExprKind::UnaryOp(ob, b)) => {
1865 oa == ob && expr_alpha_eq_subst(a, b, subst)
1866 }
1867 (
1868 ExprKind::MethodCall {
1869 receiver: ra,
1870 method: ma,
1871 args: aa,
1872 ..
1873 },
1874 ExprKind::MethodCall {
1875 receiver: rb,
1876 method: mb,
1877 args: ab,
1878 ..
1879 },
1880 ) => {
1881 ma.name == mb.name
1882 && aa.len() == ab.len()
1883 && expr_alpha_eq_subst(ra, rb, subst)
1884 && aa
1885 .iter()
1886 .zip(ab.iter())
1887 .all(|(x, y)| expr_alpha_eq_subst(x, y, subst))
1888 }
1889 _ => false,
1890 }
1891}
1892
1893fn expr_struct_eq(a: &Expr, b: &Expr) -> bool {
1896 match (&a.kind, &b.kind) {
1897 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
1898 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
1899 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
1900 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
1901 (ExprKind::Paren(x), _) => expr_struct_eq(x, b),
1902 (_, ExprKind::Paren(y)) => expr_struct_eq(a, y),
1903 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1904 oa == ob && expr_struct_eq(la, lb) && expr_struct_eq(ra, rb)
1905 }
1906 (ExprKind::UnaryOp(oa, x), ExprKind::UnaryOp(ob, y)) => oa == ob && expr_struct_eq(x, y),
1907 (
1908 ExprKind::MethodCall {
1909 receiver: ra,
1910 method: ma,
1911 args: aa,
1912 ..
1913 },
1914 ExprKind::MethodCall {
1915 receiver: rb,
1916 method: mb,
1917 args: ab,
1918 ..
1919 },
1920 ) => {
1921 ma.name == mb.name
1922 && aa.len() == ab.len()
1923 && expr_struct_eq(ra, rb)
1924 && aa.iter().zip(ab.iter()).all(|(x, y)| expr_struct_eq(x, y))
1925 }
1926 _ => false,
1927 }
1928}
1929
1930pub const PROP_GEN_DEPTH: u32 = 12;
1933
1934pub fn prop_binding_generable(
1939 ty: checker::TyId,
1940 types: &HashMap<String, Arc<TypeDecl>>,
1941 depth: u32,
1942 tys: &Arc<Types>,
1943) -> bool {
1944 if depth == 0 {
1945 return false;
1946 }
1947 match &*tys.get(ty) {
1948 checker::Ty::Base(_) => true,
1949 checker::Ty::Named { name, .. } => {
1950 let Some(decl) = types.get(name) else {
1951 return false;
1952 };
1953 match &decl.body {
1954 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1955 !refinement.as_ref().is_some_and(|r| {
1956 r.predicates
1957 .iter()
1958 .any(|p| matches!(p.kind, PredKind::Matches(_)))
1959 })
1960 }
1961 TypeBody::Sum(s) => s.variants.first().is_some_and(|v| {
1962 v.payload.iter().all(|f| {
1963 checker::resolve_type_ref(&f.type_ref, types, tys)
1964 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1965 })
1966 }),
1967 TypeBody::Record(r) => r.fields.iter().all(|f| {
1968 checker::resolve_type_ref(&f.type_ref, types, tys)
1969 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1970 }),
1971 }
1972 }
1973 _ => false,
1974 }
1975}
1976
1977fn named_refinement<'a>(
1980 ty: checker::TyId,
1981 types: &'a HashMap<String, Arc<TypeDecl>>,
1982 tys: &Arc<Types>,
1983) -> Option<&'a Refinement> {
1984 let node = tys.get(ty);
1985 let checker::Ty::Named { name, .. } = &*node else {
1986 return None;
1987 };
1988 match &types.get(name)?.body {
1989 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1990 refinement.as_ref()
1991 }
1992 _ => None,
1993 }
1994}
1995
1996fn predicate_restates_refinement(pred: &Expr, bound_var: &str, refinement: &Refinement) -> bool {
2002 let ExprKind::BinOp(op, lhs, rhs) = &pred.kind else {
2003 return false;
2004 };
2005 let ExprKind::Ident(id) = &lhs.kind else {
2007 return false;
2008 };
2009 if id.name != bound_var {
2010 return false;
2011 }
2012 let ExprKind::IntLit { value: n, .. } = &rhs.kind else {
2013 return false;
2014 };
2015 let n = *n;
2016 let positive = refinement
2017 .predicates
2018 .iter()
2019 .any(|p| matches!(p.kind, PredKind::Positive));
2020 let non_negative = refinement
2021 .predicates
2022 .iter()
2023 .any(|p| matches!(p.kind, PredKind::NonNegative));
2024 match op {
2025 BinOp::Gt if n == 0 => positive,
2027 BinOp::GtEq if n == 1 => positive,
2028 BinOp::GtEq if n == 0 => non_negative,
2030 _ => false,
2031 }
2032}
2033
2034#[derive(Clone, Copy)]
2037enum HistoryRestate {
2038 Invariant,
2040 Transition,
2042}
2043
2044fn as_new_field<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
2047 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
2048 return None;
2049 };
2050 let ExprKind::FieldAccess {
2051 receiver: inner,
2052 field: which,
2053 } = &receiver.kind
2054 else {
2055 return None;
2056 };
2057 let ExprKind::Ident(id) = &inner.kind else {
2058 return None;
2059 };
2060 (id.name == s && which.name == "new").then_some(field.name.as_str())
2061}
2062
2063fn as_step_root<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
2066 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
2067 return None;
2068 };
2069 let ExprKind::Ident(id) = &receiver.kind else {
2070 return None;
2071 };
2072 (id.name == s && (field.name == "old" || field.name == "new")).then_some(field.name.as_str())
2073}
2074
2075fn history_pred_matches(body: &Expr, s: &str, decl: &Expr, mode: HistoryRestate) -> bool {
2080 match mode {
2082 HistoryRestate::Invariant => {
2083 if let (Some(f), ExprKind::Ident(id)) = (as_new_field(body, s), &decl.kind) {
2084 return f == id.name;
2085 }
2086 }
2087 HistoryRestate::Transition => {
2088 if let (Some(root), ExprKind::Ident(id)) = (as_step_root(body, s), &decl.kind) {
2089 return root == id.name;
2090 }
2091 }
2092 }
2093 match (&body.kind, &decl.kind) {
2094 (ExprKind::Paren(x), _) => history_pred_matches(x, s, decl, mode),
2095 (_, ExprKind::Paren(y)) => history_pred_matches(body, s, y, mode),
2096 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
2097 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
2098 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
2099 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
2100 (ExprKind::None, ExprKind::None) => true,
2101 (ExprKind::Some(x), ExprKind::Some(y)) => history_pred_matches(x, s, y, mode),
2102 (ExprKind::UnaryOp(o1, x), ExprKind::UnaryOp(o2, y)) => {
2103 o1 == o2 && history_pred_matches(x, s, y, mode)
2104 }
2105 (ExprKind::BinOp(o1, l1, r1), ExprKind::BinOp(o2, l2, r2)) => {
2106 o1 == o2
2107 && history_pred_matches(l1, s, l2, mode)
2108 && history_pred_matches(r1, s, r2, mode)
2109 }
2110 (
2111 ExprKind::FieldAccess {
2112 receiver: r1,
2113 field: f1,
2114 },
2115 ExprKind::FieldAccess {
2116 receiver: r2,
2117 field: f2,
2118 },
2119 ) => f1.name == f2.name && history_pred_matches(r1, s, r2, mode),
2120 (
2121 ExprKind::MethodCall {
2122 receiver: r1,
2123 method: m1,
2124 args: a1,
2125 ..
2126 },
2127 ExprKind::MethodCall {
2128 receiver: r2,
2129 method: m2,
2130 args: a2,
2131 ..
2132 },
2133 ) => {
2134 m1.name == m2.name
2135 && a1.len() == a2.len()
2136 && history_pred_matches(r1, s, r2, mode)
2137 && a1
2138 .iter()
2139 .zip(a2)
2140 .all(|(x, y)| history_pred_matches(x, s, y, mode))
2141 }
2142 (
2143 ExprKind::Call {
2144 name: n1, args: a1, ..
2145 },
2146 ExprKind::Call {
2147 name: n2, args: a2, ..
2148 },
2149 ) => {
2150 n1.name == n2.name
2151 && a1.len() == a2.len()
2152 && a1
2153 .iter()
2154 .zip(a2)
2155 .all(|(x, y)| history_pred_matches(x, s, y, mode))
2156 }
2157 _ => false,
2158 }
2159}
2160
2161fn history_restates_invariant(prop: &PropertyDecl, run_var: &str, agent: &AgentDecl) -> bool {
2168 let [stmt] = prop.forall.body.statements.as_slice() else {
2169 return false;
2170 };
2171 let Statement::Expect(e) = stmt else {
2172 return false;
2173 };
2174 let ExprKind::MethodCall {
2176 receiver,
2177 method,
2178 args,
2179 ..
2180 } = &e.value.kind
2181 else {
2182 return false;
2183 };
2184 if method.name != "all" && method.name != "any" {
2185 return false;
2186 }
2187 let ExprKind::Ident(recv) = &receiver.kind else {
2188 return false;
2189 };
2190 if recv.name != run_var {
2191 return false;
2192 }
2193 let [arg] = args.as_slice() else {
2194 return false;
2195 };
2196 let ExprKind::Lambda(lam) = &arg.kind else {
2197 return false;
2198 };
2199 let [param] = lam.params.as_slice() else {
2200 return false;
2201 };
2202 let s = ¶m.name.name;
2203 agent
2204 .invariants
2205 .iter()
2206 .any(|inv| history_pred_matches(&lam.body, s, &inv.predicate, HistoryRestate::Invariant))
2207 || agent
2208 .transitions
2209 .iter()
2210 .any(|tr| history_pred_matches(&lam.body, s, &tr.predicate, HistoryRestate::Transition))
2211}
2212
2213fn history_call_type_name(agent: &str) -> String {
2217 format!("__History_{agent}_Call")
2218}
2219fn history_step_type_name(agent: &str) -> String {
2220 format!("__History_{agent}_Step")
2221}
2222fn history_state_type_name(agent: &str) -> String {
2223 format!("__History_{agent}_State")
2224}
2225
2226pub fn history_variant_name(handler: &str) -> String {
2230 let mut chars = handler.chars();
2231 match chars.next() {
2232 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2233 None => handler.to_string(),
2234 }
2235}
2236
2237pub fn history_handlers(agent: &AgentDecl) -> Vec<&Handler> {
2241 agent
2242 .handlers
2243 .iter()
2244 .filter(|h| matches!(h.kind, HandlerKind::Call) && h.method_name.is_some())
2245 .collect()
2246}
2247
2248pub fn check_history_binding(
2256 inner: &TypeRef,
2257 span: Span,
2258 resolved: &mut ResolvedCommons,
2259 refs: &mut RefSink,
2260 tys: &Arc<Types>,
2261) -> Result<checker::Ty, CompileError> {
2262 let TypeRef::Named(agent_id) = inner else {
2265 return Err(CompileError::new(
2266 "bynk.history.not_an_agent",
2267 span,
2268 format!(
2269 "`for all` cannot generate `History[{}]` — only an agent has handlers to sequence",
2270 ts_type_ref_display(inner)
2271 ),
2272 )
2273 .with_note("generate a driven call-history over an agent: `for all run: History[Agent]`"));
2274 };
2275 let Some(agent) = resolved.agents.get(&agent_id.name).cloned() else {
2276 return Err(CompileError::new(
2277 "bynk.history.not_an_agent",
2278 span,
2279 format!(
2280 "`for all run: History[{}]` names `{}`, which is not an agent in scope",
2281 agent_id.name, agent_id.name
2282 ),
2283 )
2284 .with_note(
2285 "only an agent (with handlers and reachable state) can be driven as a history",
2286 ));
2287 };
2288 refs.record(agent_id.span, SymbolKind::Type, &agent_id.name);
2289
2290 let handlers = history_handlers(&agent);
2291 for h in &handlers {
2295 for p in &h.params {
2296 let generable = checker::resolve_type_ref(&p.type_ref, &resolved.types, tys)
2297 .is_some_and(|t| prop_binding_generable(t, &resolved.types, PROP_GEN_DEPTH, tys));
2298 if !generable {
2299 return Err(CompileError::new(
2300 "bynk.history.not_generable",
2301 span,
2302 format!(
2303 "`History[{}]` cannot be driven — handler `{}`'s parameter `{}: {}` is not generable (e.g. a `Matches` refinement)",
2304 agent_id.name,
2305 h.method_name.as_ref().map(|m| m.name.as_str()).unwrap_or(""),
2306 p.name.name,
2307 ts_type_ref_display(&p.type_ref),
2308 ),
2309 )
2310 .with_note(
2311 "every handler parameter must be refinement-generable for the run to be seeded",
2312 ));
2313 }
2314 }
2315 }
2316
2317 let state_name = history_state_type_name(&agent_id.name);
2322 let call_name = history_call_type_name(&agent_id.name);
2323 let step_name = history_step_type_name(&agent_id.name);
2324
2325 let state_fields: Vec<RecordField> = agent
2328 .store_fields
2329 .iter()
2330 .filter(|f| f.kind.head.name == "Cell" && f.kind.args.len() == 1)
2331 .map(|f| RecordField {
2332 trivia: Default::default(),
2333 name: f.name.clone(),
2334 type_ref: f.kind.args[0].clone(),
2335 refinement: None,
2336 init: None,
2337 span: f.span,
2338 })
2339 .collect();
2340 resolved.types.insert(
2341 state_name.clone(),
2342 Arc::new(TypeDecl {
2343 type_params: Vec::new(),
2344 name: Ident {
2345 name: state_name.clone(),
2346 span,
2347 },
2348 body: TypeBody::Record(RecordBody {
2349 trailing_comments: Default::default(),
2350 fields: state_fields,
2351 span,
2352 }),
2353 documentation: None,
2354 span,
2355 trivia: Trivia::default(),
2356 }),
2357 );
2358
2359 let variants: Vec<Variant> = handlers
2362 .iter()
2363 .map(|h| {
2364 let hname = h.method_name.as_ref().expect("call handler has a name");
2365 Variant {
2366 trivia: Default::default(),
2367 name: Ident {
2368 name: history_variant_name(&hname.name),
2369 span: hname.span,
2370 },
2371 payload: h
2372 .params
2373 .iter()
2374 .map(|p| VariantField {
2375 name: p.name.clone(),
2376 type_ref: p.type_ref.clone(),
2377 span: p.span,
2378 })
2379 .collect(),
2380 span: hname.span,
2381 }
2382 })
2383 .collect();
2384 resolved.types.insert(
2385 call_name.clone(),
2386 Arc::new(TypeDecl {
2387 type_params: Vec::new(),
2388 name: Ident {
2389 name: call_name.clone(),
2390 span,
2391 },
2392 body: TypeBody::Sum(SumBody {
2393 trailing_comments: Default::default(),
2394 variants,
2395 embeds: Vec::new(),
2396 span,
2397 }),
2398 documentation: None,
2399 span,
2400 trivia: Trivia::default(),
2401 }),
2402 );
2403
2404 let step_fields = vec![
2407 RecordField {
2408 trivia: Default::default(),
2409 name: Ident {
2410 name: "call".to_string(),
2411 span,
2412 },
2413 type_ref: TypeRef::Named(Ident {
2414 name: call_name.clone(),
2415 span,
2416 }),
2417 refinement: None,
2418 init: None,
2419 span,
2420 },
2421 RecordField {
2422 trivia: Default::default(),
2423 name: Ident {
2424 name: "accepted".to_string(),
2425 span,
2426 },
2427 type_ref: TypeRef::Base(BaseType::Bool, span),
2428 refinement: None,
2429 init: None,
2430 span,
2431 },
2432 RecordField {
2433 trivia: Default::default(),
2434 name: Ident {
2435 name: "old".to_string(),
2436 span,
2437 },
2438 type_ref: TypeRef::Named(Ident {
2439 name: state_name.clone(),
2440 span,
2441 }),
2442 refinement: None,
2443 init: None,
2444 span,
2445 },
2446 RecordField {
2447 trivia: Default::default(),
2448 name: Ident {
2449 name: "new".to_string(),
2450 span,
2451 },
2452 type_ref: TypeRef::Named(Ident {
2453 name: state_name.clone(),
2454 span,
2455 }),
2456 refinement: None,
2457 init: None,
2458 span,
2459 },
2460 ];
2461 resolved.types.insert(
2462 step_name.clone(),
2463 Arc::new(TypeDecl {
2464 type_params: Vec::new(),
2465 name: Ident {
2466 name: step_name.clone(),
2467 span,
2468 },
2469 body: TypeBody::Record(RecordBody {
2470 trailing_comments: Default::default(),
2471 fields: step_fields,
2472 span,
2473 }),
2474 documentation: None,
2475 span,
2476 trivia: Trivia::default(),
2477 }),
2478 );
2479
2480 Ok(checker::Ty::List(tys.intern(checker::Ty::Named {
2481 name: step_name,
2482 kind: checker::NamedKind::Record,
2483 args: Vec::new(),
2484 })))
2485}
2486
2487#[allow(clippy::too_many_arguments)]
2495fn check_property_body(
2496 target_name: &str,
2497 target_kind: UnitKind,
2498 prop: &PropertyDecl,
2499 unit_tables: &HashMap<String, UnitTable>,
2500 unit_uses: &HashMap<String, Vec<String>>,
2501 unit_consumes: &HashMap<String, Vec<String>>,
2502 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
2503 errors: &mut Vec<CompileError>,
2504 refs: &mut RefSink,
2505 tys: &Arc<Types>,
2506) {
2507 let Some((mut resolved, _)) = build_privileged_resolved(
2508 target_name,
2509 unit_tables,
2510 unit_uses,
2511 unit_consumes,
2512 unit_consumes_aliases,
2513 ) else {
2514 return;
2515 };
2516 register_call_record_types(&mut resolved, target_name, unit_tables);
2517 let _ = target_kind;
2518
2519 let mut binding_scope: HashMap<String, checker::TyId> = HashMap::new();
2521 let mut binding_types: Vec<(String, Option<checker::TyId>)> = Vec::new();
2522 let mut history_binding: Option<(String, AgentDecl)> = None;
2525 for b in &prop.forall.bindings {
2526 if let TypeRef::History(inner, hspan) = &b.type_ref {
2529 match check_history_binding(inner, *hspan, &mut resolved, refs, tys) {
2530 Ok(step_ty) => {
2531 if let TypeRef::Named(agent_id) = &**inner
2532 && let Some(agent) = resolved.agents.get(&agent_id.name)
2533 {
2534 history_binding = Some((b.name.name.clone(), agent.clone()));
2535 }
2536 binding_scope.insert(b.name.name.clone(), tys.intern(step_ty.clone()));
2537 binding_types.push((b.name.name.clone(), Some(tys.intern(step_ty))));
2538 }
2539 Err(err) => {
2540 errors.push(err);
2541 binding_types.push((b.name.name.clone(), None));
2542 binding_scope.insert(b.name.name.clone(), tys.intern(checker::Ty::Error));
2545 }
2546 }
2547 continue;
2548 }
2549 if let TypeRef::Named(id) = &b.type_ref
2552 && resolved.agents.contains_key(&id.name)
2553 {
2554 errors.push(
2555 CompileError::new(
2556 "bynk.val.agent_not_generable",
2557 b.type_ref.span(),
2558 format!(
2559 "`for all {}: {}` cannot generate an agent — a fabricated agent state need not be reachable",
2560 b.name.name, id.name
2561 ),
2562 )
2563 .with_note(
2564 "generate behaviour over an agent via handler sequences (the history rung), not fabricated states",
2565 ),
2566 );
2567 binding_types.push((b.name.name.clone(), None));
2568 binding_scope.insert(b.name.name.clone(), tys.intern(checker::Ty::Error));
2571 continue;
2572 }
2573 let ty = match checker::resolve_type_ref(&b.type_ref, &resolved.types, tys) {
2574 Some(t) => {
2575 record_type_refs_in_property(&b.type_ref, &resolved, refs);
2576 t
2577 }
2578 None => {
2579 errors.push(CompileError::new(
2580 "bynk.val.unknown_type",
2581 b.type_ref.span(),
2582 format!(
2583 "`for all {}: {}` names a type that does not resolve",
2584 b.name.name,
2585 ts_type_ref_display(&b.type_ref)
2586 ),
2587 ));
2588 binding_types.push((b.name.name.clone(), None));
2589 binding_scope.insert(b.name.name.clone(), tys.intern(checker::Ty::Error));
2592 continue;
2593 }
2594 };
2595 if !prop_binding_generable(ty, &resolved.types, PROP_GEN_DEPTH, tys) {
2596 errors.push(
2597 CompileError::new(
2598 "bynk.val.needs_pin",
2599 b.type_ref.span(),
2600 format!(
2601 "`for all {}: {}` cannot generate a value (e.g. a `Matches` refinement); a property cannot bind it",
2602 b.name.name,
2603 ts_type_ref_display(&b.type_ref)
2604 ),
2605 )
2606 .with_note("supply the witness in a `case` with a pinned `Val[T](...)` instead"),
2607 );
2608 }
2609 binding_scope.insert(b.name.name.clone(), ty);
2610 binding_types.push((b.name.name.clone(), Some(ty)));
2611 }
2612
2613 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
2616 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
2617 let unit_span = prop.span;
2618 let synthetic_return = TypeRef::Effect(
2619 Box::new(TypeRef::Result(
2620 Box::new(TypeRef::Unit(unit_span)),
2621 Box::new(TypeRef::ValidationError(unit_span)),
2622 unit_span,
2623 )),
2624 unit_span,
2625 );
2626 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
2627 if let Some(table) = unit_tables.get(target_name) {
2628 for (name, decl) in &table.capabilities {
2629 let ops = decl
2630 .ops
2631 .iter()
2632 .map(|op| build_capability_op_info(op, &resolved.types, tys))
2633 .collect();
2634 capability_info_map.insert(
2635 name.clone(),
2636 checker::CapabilityInfo {
2637 name: name.clone(),
2638 ops,
2639 },
2640 );
2641 }
2642 }
2643 add_consumed_adapter_capabilities(&mut capability_info_map, unit_tables, &resolved, tys);
2644 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
2645 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
2646 let return_ty_span = prop.span;
2647 let mut no_hints = HintSink::new();
2648 let mut no_locals = LocalsSink::new();
2649 let mut no_requirements = RequirementSink::new();
2650 let _ = checker::check_body(
2654 &resolved,
2655 &prop.forall.body,
2656 return_ty,
2657 return_ty_span,
2658 binding_scope,
2659 checker::CapabilityCtx {
2660 capabilities: capability_info_map.clone(),
2661 declared_capabilities: capability_info_map,
2662 given_remaining: given_declared.iter().cloned().collect(),
2663 given_used: HashSet::new(),
2664 given_entries: Vec::new(),
2665 given_anchor: None,
2666 },
2667 target_test_services(unit_tables.get(target_name)),
2668 target_test_actors(unit_tables.get(target_name)),
2669 prop.forall.where_pred.as_ref(),
2670 checker::CheckSinks {
2671 tys,
2672 expr_types: &mut expr_types,
2673 errors,
2674 refs,
2675 hints: &mut no_hints,
2676 locals: &mut no_locals,
2677 requirements: &mut no_requirements,
2678 callees: &mut callees,
2679 },
2680 );
2681
2682 if let [(var, Some(ty))] = binding_types.as_slice()
2685 && let Some(refinement) = named_refinement(*ty, &resolved.types, tys)
2686 && let [stmt] = prop.forall.body.statements.as_slice()
2687 && let Statement::Expect(e) = stmt
2688 && predicate_restates_refinement(&e.value, var, refinement)
2689 {
2690 errors.push(
2691 CompileError::new(
2692 "bynk.property.restates_refinement",
2693 prop.forall.body.span,
2694 format!(
2695 "property `{}` merely re-checks a refinement type `{}` already guarantees",
2696 prop.name,
2697 ty.display(tys)
2698 ),
2699 )
2700 .with_note(
2701 "a property earns its keep by asserting behaviour over valid inputs, not by restating the type's refinement",
2702 ),
2703 );
2704 }
2705
2706 if let Some((run_var, agent)) = &history_binding
2711 && history_restates_invariant(prop, run_var, agent)
2712 {
2713 errors.push(
2714 CompileError::new(
2715 "bynk.history.restates_invariant",
2716 prop.forall.body.span,
2717 format!(
2718 "history property `{}` merely re-checks a guarantee agent `{}`'s `invariant`/`transition` already enforces on every reached state",
2719 prop.name, agent.name.name
2720 ),
2721 )
2722 .with_note(
2723 "a history property earns its keep by asserting a cross-step protocol, not by restating a per-state invariant",
2724 ),
2725 );
2726 }
2727}
2728
2729fn record_type_refs_in_property(
2732 type_ref: &TypeRef,
2733 resolved: &ResolvedCommons,
2734 refs: &mut RefSink,
2735) {
2736 checker::record_type_refs(type_ref, &resolved.types, &HashSet::new(), refs);
2737}
2738
2739pub fn build_privileged_resolved(
2745 owning_unit: &str,
2746 unit_tables: &HashMap<String, UnitTable>,
2747 unit_uses: &HashMap<String, Vec<String>>,
2748 unit_consumes: &HashMap<String, Vec<String>>,
2749 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
2750) -> Option<(ResolvedCommons, ())> {
2751 let local = unit_tables.get(owning_unit)?;
2752 let mut types = local.types.clone();
2753 let mut fns = local.fns.clone();
2754 let mut methods = local.methods.clone();
2755 if let Some(targets) = unit_uses.get(owning_unit) {
2756 for t in targets {
2757 if let Some(used) = unit_tables.get(t) {
2758 for (n, d) in &used.types {
2759 types.entry(n.clone()).or_insert_with(|| d.clone());
2760 }
2761 for (n, d) in &used.fns {
2762 fns.entry(n.clone()).or_insert_with(|| d.clone());
2763 }
2764 for (n, mt) in &used.methods {
2765 let entry = methods.entry(n.clone()).or_default();
2766 for (m, decl) in &mt.instance {
2767 entry
2768 .instance
2769 .entry(m.clone())
2770 .or_insert_with(|| decl.clone());
2771 }
2772 for (m, decl) in &mt.statics {
2773 entry
2774 .statics
2775 .entry(m.clone())
2776 .or_insert_with(|| decl.clone());
2777 }
2778 }
2779 }
2780 }
2781 }
2782 if let Some(consumed) = unit_consumes.get(owning_unit) {
2784 for t in consumed {
2785 if let Some(used) = unit_tables.get(t) {
2786 for (n, d) in &used.types {
2787 types.entry(n.clone()).or_insert_with(|| d.clone());
2788 }
2789 for (n, mt) in &used.methods {
2790 let entry = methods.entry(n.clone()).or_default();
2791 for (m, decl) in &mt.instance {
2792 entry
2793 .instance
2794 .entry(m.clone())
2795 .or_insert_with(|| decl.clone());
2796 }
2797 }
2798 }
2799 }
2800 }
2801 let cross_context = build_cross_context_info(
2802 owning_unit,
2803 unit_consumes,
2804 unit_consumes_aliases,
2805 unit_uses,
2806 unit_tables,
2807 );
2808 let synthetic_commons = Commons {
2809 name: QualifiedName {
2810 parts: owning_unit
2811 .split('.')
2812 .map(|part| Ident {
2813 name: part.to_string(),
2814 span: Span::default(),
2815 })
2816 .collect(),
2817 span: Span::default(),
2818 },
2819 items: Vec::new(),
2820 uses: Vec::new(),
2821 documentation: None,
2822 form: CommonsForm::Brace,
2823 span: Span::default(),
2824 trivia: Trivia::default(),
2825 trailing_comments: Vec::new(),
2826 };
2827 let agents_for_resolved = unit_tables
2828 .get(owning_unit)
2829 .map(|t| t.agents.clone())
2830 .unwrap_or_default();
2831 let no_local_events = HashMap::new();
2832 let resolved = ResolvedCommons::new(
2833 synthetic_commons,
2834 types,
2835 &local.types,
2836 fns,
2837 methods,
2838 agents_for_resolved,
2839 &no_local_events,
2843 cross_context,
2844 HashMap::new(),
2845 false,
2846 HashSet::new(),
2847 );
2848 Some((resolved, ()))
2849}
2850
2851#[cfg(test)]
2852mod tests {
2853 use super::*;
2854
2855 fn handler_body(src: &str) -> Block {
2857 let tokens = bynk_syntax::lexer::tokenize(src).expect("lex");
2858 let unit = bynk_syntax::parser::parse_unit(&tokens, src).expect("parse");
2859 let SourceUnit::Context(ctx) = unit else {
2860 panic!("expected a context unit");
2861 };
2862 ctx.items
2863 .into_iter()
2864 .find_map(|item| match item {
2865 bynk_syntax::ast::CommonsItem::Service(s) => s.handlers.into_iter().next(),
2866 _ => None,
2867 })
2868 .expect("a service handler")
2869 .body
2870 }
2871
2872 const SRC: &str = "context demo\n\
2873 service svc {\n\
2874 on call(n: Int) -> Effect[Int] {\n\
2875 let a <- first(n)\n\
2876 let b = if n > 0 { let c = second(n)\n c } else { third(n) }\n\
2877 fourth(a + b)\n\
2878 }\n\
2879 }\n";
2880
2881 #[test]
2885 fn exprs_in_order_is_source_order() {
2886 let body = handler_body(SRC);
2887 let calls: Vec<&str> = exprs_in_order(&body)
2888 .into_iter()
2889 .filter_map(|e| match &e.kind {
2890 ExprKind::Call { name, .. } => Some(name.name.as_str()),
2891 _ => None,
2892 })
2893 .collect();
2894 assert_eq!(calls, ["first", "second", "third", "fourth"]);
2895 }
2896
2897 #[test]
2900 fn blocks_deep_reaches_nested_lets() {
2901 let body = handler_body(SRC);
2902 let lets: Vec<String> = blocks_deep(&body)
2903 .iter()
2904 .flat_map(|b| &b.statements)
2905 .filter_map(|s| match s {
2906 Statement::Let(l) | Statement::EffectLet(l) => Some(l.name.name.clone()),
2907 _ => None,
2908 })
2909 .collect();
2910 assert!(lets.contains(&"c".to_string()), "{lets:?}");
2911 assert!(lets.contains(&"a".to_string()) && lets.contains(&"b".to_string()));
2912 }
2913}