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 identity_path: PathBuf,
97}
98
99#[allow(clippy::too_many_arguments)]
114pub fn phase_test_bodies(
115 test_groups: &BTreeMap<String, Vec<usize>>,
116 parsed: &[ParsedFile],
117 kinds: &BTreeMap<String, UnitKind>,
118 unit_tables: &HashMap<String, UnitTable>,
119 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
120 unit_consumes: &HashMap<String, Vec<String>>,
121 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
122 unit_uses: &HashMap<String, Vec<String>>,
123 errors: &mut Vec<CompileError>,
124 refs: &mut RefSink,
125 tys: &Arc<Types>,
126) -> HashMap<String, HashMap<String, ResolvedStub>> {
127 let mut ready: HashMap<String, HashMap<String, ResolvedStub>> = HashMap::new();
128
129 let mut sorted_targets: Vec<&String> = test_groups.keys().collect();
130 sorted_targets.sort();
131
132 for target_name in sorted_targets {
133 let indices = test_groups.get(target_name).unwrap();
134 let target_kind = match kinds.get(target_name) {
136 Some(k) => *k,
137 None => {
138 let span = first_test_target_span(indices, parsed);
139 errors.push(
140 CompileError::new(
141 "bynk.suite.unknown_target",
142 span,
143 format!(
144 "test target `{target_name}` is not a declared commons or context in this project",
145 ),
146 )
147 .with_note(
148 "the target of a `test` declaration must be a commons or context declared elsewhere in the project",
149 ),
150 );
151 continue;
152 }
153 };
154
155 let mut seen_cases: HashMap<String, Span> = HashMap::new();
157 let mut had_dup = false;
158 for &i in indices {
159 if let Some(t) = parsed[i].test() {
160 for case in &t.cases {
161 if let Some(prev) = seen_cases.get(&case.name) {
162 had_dup = true;
163 errors.push(
164 CompileError::new(
165 "bynk.suite.duplicate_case_name",
166 case.name_span,
167 format!(
168 "test case `\"{}\"` is declared more than once in tests targeting `{target_name}`",
169 case.name
170 ),
171 )
172 .with_label(*prev, "previously declared here"),
173 );
174 } else {
175 seen_cases.insert(case.name.clone(), case.name_span);
176 }
177 }
178 }
179 }
180
181 let target_stubs = resolve_stubs(
190 target_name,
191 target_kind,
192 indices,
193 parsed,
194 unit_tables,
195 unit_consumes,
196 errors,
197 );
198
199 if had_dup {
200 continue;
202 }
203
204 let bodies_errs = check_test_bodies(
208 target_name,
209 target_kind,
210 indices,
211 parsed,
212 &target_stubs,
213 unit_tables,
214 exports_visibility,
215 unit_consumes,
216 unit_consumes_aliases,
217 unit_uses,
218 refs,
219 tys,
220 );
221 let bodies_failed = !bodies_errs.is_empty();
222 errors.extend(bodies_errs);
223
224 if bodies_failed {
225 continue;
226 }
227
228 ready.insert(target_name.clone(), target_stubs);
229 }
230
231 ready
232}
233
234fn resolve_stubs(
241 target_name: &str,
242 target_kind: UnitKind,
243 indices: &[usize],
244 parsed: &[ParsedFile],
245 unit_tables: &HashMap<String, UnitTable>,
246 unit_consumes: &HashMap<String, Vec<String>>,
247 errors: &mut Vec<CompileError>,
248) -> HashMap<String, ResolvedStub> {
249 let target_table = unit_tables.get(target_name);
250 let target_consumed = unit_consumes.get(target_name).cloned().unwrap_or_default();
251
252 let mut collected: Vec<(StubClause, PathBuf)> = Vec::new();
255 for &i in indices {
256 let Some(t) = parsed[i].test() else { continue };
257 for case in &t.cases {
258 for pc in &case.stubs {
259 collected.push((pc.clone(), parsed[i].identity_path()));
260 }
261 }
262 }
263 for &i in indices {
264 let Some(t) = parsed[i].test() else { continue };
265 for pc in &t.stubs {
266 collected.push((pc.clone(), parsed[i].identity_path()));
267 }
268 }
269
270 let resolve_cap = |name: &str| -> Option<CapabilityDecl> {
274 target_table
275 .and_then(|t| t.capabilities.get(name).cloned())
276 .or_else(|| {
277 target_consumed.iter().find_map(|q| {
278 unit_tables
279 .get(q)
280 .and_then(|t| t.capabilities.get(name).cloned())
281 })
282 })
283 };
284
285 let mut out: HashMap<String, ResolvedStub> = HashMap::new();
286 for (pc, identity_path) in collected {
287 let cap_name = pc.capability.name.clone();
288 let Some(cap_decl) = resolve_cap(&cap_name) else {
289 let note = if target_kind == UnitKind::Commons {
292 "commons have no capability seams — `stub` overrides a capability the target context declares or consumes"
293 } else {
294 "a `stub` clause names a capability the target context declares or reaches through a consumed context"
295 };
296 errors.push(
297 CompileError::new(
298 "bynk.stub.not_a_seam",
299 pc.capability.span,
300 format!("`{cap_name}` is not a capability seam of `{target_name}`",),
301 )
302 .with_note(note),
303 );
304 continue;
305 };
306 let Some(op_decl) = cap_decl.ops.iter().find(|o| o.name.name == pc.method.name) else {
307 errors.push(CompileError::new(
308 "bynk.stub.unknown_op",
309 pc.method.span,
310 format!(
311 "`{}` is not an operation of capability `{cap_name}`",
312 pc.method.name
313 ),
314 ));
315 continue;
316 };
317 if !op_decl.type_params.is_empty() {
325 errors.push(
326 CompileError::new(
327 "bynk.stub.generic_op",
328 pc.method.span,
329 format!(
330 "`{cap_name}.{}` declares its own type parameter — a generic capability operation cannot be stubbed at v1",
331 pc.method.name
332 ),
333 )
334 .with_note(
335 "test through the capability's real (external) provider instead, or restructure the test to avoid stubbing this operation",
336 ),
337 );
338 continue;
339 }
340 if let StubRhs::ReturnsEach(outcomes, span) = &pc.rhs
341 && outcomes.is_empty()
342 {
343 errors.push(CompileError::new(
344 "bynk.stub.bad_sequence",
345 *span,
346 format!(
347 "`stub {cap_name}.{} returns each []` has no outcomes — a sequence needs at least one",
348 pc.method.name
349 ),
350 ));
351 continue;
352 }
353 let entry = out.entry(cap_name.clone()).or_insert_with(|| ResolvedStub {
354 cap: cap_name.clone(),
355 cap_decl: cap_decl.clone(),
356 clauses: Vec::new(),
357 identity_path: identity_path.clone(),
358 });
359 entry.clauses.push(pc);
360 }
361 out
362}
363
364pub fn infer_participants(
369 target: &str,
370 unit_consumes: &HashMap<String, Vec<String>>,
371) -> Vec<String> {
372 let mut seen: HashSet<String> = HashSet::new();
373 let mut order: Vec<String> = Vec::new();
374 let mut queue: Vec<String> = vec![target.to_string()];
375 seen.insert(target.to_string());
376 let mut head = 0;
377 while head < queue.len() {
378 let node = queue[head].clone();
379 head += 1;
380 order.push(node.clone());
381 if let Some(deps) = unit_consumes.get(&node) {
382 for d in deps {
383 if seen.insert(d.clone()) {
384 queue.push(d.clone());
385 }
386 }
387 }
388 }
389 order
390}
391
392#[allow(clippy::too_many_arguments)]
412pub fn phase_integration_bodies(
413 integration_groups: &BTreeMap<String, Vec<usize>>,
414 parsed: &[ParsedFile],
415 unit_tables: &HashMap<String, UnitTable>,
416 unit_consumes: &HashMap<String, Vec<String>>,
417 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
418 unit_uses: &HashMap<String, Vec<String>>,
419 errors: &mut Vec<CompileError>,
420 refs: &mut RefSink,
421 tys: &Arc<Types>,
422) -> HashMap<String, resolver::CrossContextInfo> {
423 let mut ready: HashMap<String, resolver::CrossContextInfo> = HashMap::new();
424
425 let mut sorted: Vec<&String> = integration_groups.keys().collect();
426 sorted.sort();
427
428 for group_name in sorted {
429 let indices = integration_groups.get(group_name).unwrap();
430 let first = indices[0];
431 let Some(decl) = parsed[first].integration() else {
432 continue;
433 };
434 let suite_target = decl.target.joined();
438 let participants = infer_participants(&suite_target, unit_consumes);
439
440 let mut bad = false;
441
442 let has_serialisation_edge = unit_tables.get(&suite_target).is_some_and(|t| {
457 t.services
458 .values()
459 .any(|s| matches!(s.protocol, bynk_syntax::ast::ServiceProtocol::Http))
460 });
461 if participants.len() < 2 && !has_serialisation_edge {
462 errors.push(
463 CompileError::new(
464 "bynk.tier.system_needs_wire",
465 decl.target.span,
466 format!(
467 "`system`-tier suite for `{suite_target}` has no serialisation edge — the target consumes no other context and exposes no `http` service",
468 ),
469 )
470 .with_note(
471 "a `system` case crosses a real serialise → JSON → deserialise boundary; this target has none to cross, so `unit` already covers it",
472 ),
473 );
474 bad = true;
475 }
476
477 let mut seen_cases: HashMap<String, Span> = HashMap::new();
479 for &i in indices {
480 let Some(d) = parsed[i].integration() else {
481 continue;
482 };
483 for case in &d.cases {
484 if let Some(prev) = seen_cases.get(&case.name) {
485 errors.push(
486 CompileError::new(
487 "bynk.suite.duplicate_case_name",
488 case.name_span,
489 format!(
490 "test case `\"{}\"` is declared more than once in tests targeting `{suite_target}`",
491 case.name
492 ),
493 )
494 .with_label(*prev, "previously declared here"),
495 );
496 bad = true;
497 } else {
498 seen_cases.insert(case.name.clone(), case.name_span);
499 }
500 }
501 }
502
503 if bad {
504 continue;
505 }
506
507 let harness_name = group_name.clone();
509 let mut uses_targets: Vec<String> = Vec::new();
510 for &i in indices {
511 if let Some(d) = parsed[i].integration() {
512 for u in &d.uses {
513 let q = u.target.joined();
514 if !uses_targets.contains(&q) {
515 uses_targets.push(q);
516 }
517 }
518 }
519 }
520 let mut harness_consumes = unit_consumes.clone();
521 harness_consumes.insert(harness_name.clone(), participants.clone());
522 let mut harness_uses = unit_uses.clone();
523 harness_uses.insert(harness_name.clone(), uses_targets.clone());
524 let cross_context = build_cross_context_info(
525 &harness_name,
526 &harness_consumes,
527 unit_consumes_aliases,
528 &harness_uses,
529 unit_tables,
530 );
531
532 let mut body_errs: Vec<CompileError> = Vec::new();
534 let mut harness_resolution = uses_targets.clone();
537 harness_resolution.extend(participants.iter().cloned());
538 refs.declare_namespace(&harness_name, harness_resolution);
539 for &i in indices {
540 let Some(d) = parsed[i].integration() else {
541 continue;
542 };
543 refs.enter_file(
544 &parsed[i].identity_path(),
545 &harness_name,
546 parsed[i].is_synthetic(),
547 );
548 for case in &d.cases {
549 check_integration_case_body(
550 &participants,
551 &uses_targets,
552 case,
553 &cross_context,
554 unit_tables,
555 &mut body_errs,
556 refs,
557 tys,
558 );
559 if !matches!(
564 case_effective_tier(case, d),
565 bynk_syntax::ast::TestTier::System
566 ) && block_uses_wire(&case.body)
567 {
568 body_errs.push(CompileError::new(
569 "bynk.test.wire_needs_system",
570 case.name_span,
571 format!(
572 "case `\"{}\"` uses `Wire(...)` but is not a `system`-tier case",
573 case.name
574 ),
575 ).with_note(
576 "`Wire` hands raw, pre-validation input to the real boundary; promote the case with `as system`, or pass a typed argument",
577 ));
578 }
579 if !matches!(
583 case_effective_tier(case, d),
584 bynk_syntax::ast::TestTier::System
585 ) && block_uses_nobody(&case.body)
586 {
587 body_errs.push(CompileError::new(
588 "bynk.test.credential_needs_system",
589 case.name_span,
590 format!(
591 "case `\"{}\"` drives `by Nobody` but is not a `system`-tier case",
592 case.name
593 ),
594 ).with_note(
595 "`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>)`",
596 ));
597 }
598 }
599 }
600 let bodies_failed = !body_errs.is_empty();
601 errors.extend(body_errs);
602 if bodies_failed {
603 continue;
604 }
605
606 ready.insert(group_name.clone(), cross_context);
607 }
608
609 ready
610}
611
612#[allow(clippy::too_many_arguments)]
618fn check_integration_case_body(
619 participants: &[String],
620 uses_targets: &[String],
621 case: &Case,
622 cross_context: &resolver::CrossContextInfo,
623 unit_tables: &HashMap<String, UnitTable>,
624 errors: &mut Vec<CompileError>,
625 refs: &mut RefSink,
626 tys: &Arc<Types>,
627) {
628 let mut types: HashMap<String, Arc<TypeDecl>> = HashMap::new();
632 let mut fns: HashMap<String, Arc<FnDecl>> = HashMap::new();
633 let mut methods: HashMap<String, ResolverMethodTable> = HashMap::new();
634 let mut merge = |src: Option<&UnitTable>, with_fns: bool| {
635 let Some(t) = src else { return };
636 for (n, d) in &t.types {
637 types.entry(n.clone()).or_insert_with(|| d.clone());
638 }
639 if with_fns {
640 for (n, f) in &t.fns {
641 fns.entry(n.clone()).or_insert_with(|| f.clone());
642 }
643 }
644 for (n, mt) in &t.methods {
645 let entry = methods.entry(n.clone()).or_default();
646 for (m, decl) in &mt.instance {
647 entry
648 .instance
649 .entry(m.clone())
650 .or_insert_with(|| decl.clone());
651 }
652 for (m, decl) in &mt.statics {
653 entry
654 .statics
655 .entry(m.clone())
656 .or_insert_with(|| decl.clone());
657 }
658 }
659 };
660 for u in uses_targets {
661 merge(unit_tables.get(u), true);
662 }
663 for p in participants {
664 merge(unit_tables.get(p), false);
665 }
666
667 let synthetic_commons = Commons {
668 name: QualifiedName {
669 parts: vec![Ident {
670 name: "integration".to_string(),
671 span: Span::default(),
672 }],
673 span: Span::default(),
674 },
675 items: Vec::new(),
676 uses: Vec::new(),
677 documentation: None,
678 form: CommonsForm::Brace,
679 span: Span::default(),
680 trivia: Trivia::default(),
681 trailing_comments: Vec::new(),
682 };
683 let no_local_types = HashMap::new();
688 let no_local_events = HashMap::new();
689 let resolved = ResolvedCommons::new(
690 synthetic_commons,
691 types,
692 &no_local_types,
693 fns,
694 methods,
695 HashMap::new(),
696 &no_local_events,
697 cross_context.clone(),
698 HashMap::new(),
699 false,
701 HashSet::new(),
702 );
703
704 let unit_span = case.span;
705 let synthetic_return = TypeRef::Effect(
706 Box::new(TypeRef::Result(
707 Box::new(TypeRef::Unit(unit_span)),
708 Box::new(TypeRef::ValidationError(unit_span)),
709 unit_span,
710 )),
711 unit_span,
712 );
713 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
714 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
715 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
716 let mut no_hints = HintSink::new();
718 let mut no_locals = LocalsSink::new();
719 let mut no_requirements = RequirementSink::new();
721 let _ = checker::check_body(
722 &resolved,
723 &case.body,
724 return_ty,
725 case.span,
726 HashMap::new(),
727 checker::CapabilityCtx::default(),
728 target_test_services(participants.first().and_then(|t| unit_tables.get(t))),
732 target_test_actors(participants.first().and_then(|t| unit_tables.get(t))),
733 None,
734 checker::CheckSinks {
735 tys,
736 expr_types: &mut expr_types,
737 errors,
738 refs,
739 hints: &mut no_hints,
740 locals: &mut no_locals,
741 requirements: &mut no_requirements,
742 callees: &mut callees,
743 },
744 );
745}
746
747fn first_test_target_span(indices: &[usize], parsed: &[ParsedFile]) -> Span {
748 indices
749 .first()
750 .and_then(|&i| parsed[i].test().map(|t| t.target.span))
751 .unwrap_or_default()
752}
753
754#[allow(clippy::too_many_arguments)]
759fn check_test_bodies(
760 target_name: &str,
761 target_kind: UnitKind,
762 indices: &[usize],
763 parsed: &[ParsedFile],
764 stubs: &HashMap<String, ResolvedStub>,
765 unit_tables: &HashMap<String, UnitTable>,
766 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
767 unit_consumes: &HashMap<String, Vec<String>>,
768 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
769 unit_uses: &HashMap<String, Vec<String>>,
770 refs: &mut RefSink,
771 tys: &Arc<Types>,
772) -> Vec<CompileError> {
773 let mut errors = Vec::new();
774 let _ = exports_visibility;
775
776 if !stubs.is_empty()
781 && let Some((resolved, _)) = build_privileged_resolved(
782 target_name,
783 unit_tables,
784 unit_uses,
785 unit_consumes,
786 unit_consumes_aliases,
787 )
788 {
789 for rp in stubs.values() {
790 refs.enter_file(&rp.identity_path, target_name, false);
791 for clause in &rp.clauses {
792 let Some(op) = rp
793 .cap_decl
794 .ops
795 .iter()
796 .find(|o| o.name.name == clause.method.name)
797 else {
798 continue;
799 };
800 let check_value = |e: &Expr, errors: &mut Vec<CompileError>| {
801 if !stub_value_typechecks(e, op, &resolved, tys) {
802 errors.push(CompileError::new(
803 "bynk.stub.rhs_type",
804 e.span,
805 format!(
806 "the value provided for `{}.{}` does not match the operation's declared return type `{}`",
807 rp.cap,
808 op.name.name,
809 ts_type_ref_display(&op.return_type),
810 ),
811 ));
812 }
813 };
814 match &clause.rhs {
815 StubRhs::Returns(e) => check_value(e, &mut errors),
816 StubRhs::ReturnsEach(outcomes, _) => {
817 for o in outcomes {
818 if let SeqOutcome::Value(e) = o {
819 check_value(e, &mut errors);
820 }
821 }
822 }
823 StubRhs::Fails(_) => {}
824 }
825 }
826 }
827 }
828
829 for &i in indices {
832 let Some(test_decl) = parsed[i].test() else {
833 continue;
834 };
835 refs.enter_file(
838 &parsed[i].identity_path(),
839 target_name,
840 parsed[i].is_synthetic(),
841 );
842 for case in &test_decl.cases {
843 check_test_case_body(
844 target_name,
845 target_kind,
846 case,
847 unit_tables,
848 unit_uses,
849 unit_consumes,
850 unit_consumes_aliases,
851 &mut errors,
852 refs,
853 tys,
854 );
855 }
856 for prop in &test_decl.properties {
859 if property_tier(prop).is_some() {
864 errors.push(CompileError::new(
865 "bynk.tier.property_has_tier",
866 prop.name_span,
867 format!(
868 "property `\"{}\"` cannot declare a tier — tiers are a `case`-only affordance",
869 prop.name
870 ),
871 ));
872 }
873 check_property_body(
874 target_name,
875 target_kind,
876 prop,
877 unit_tables,
878 unit_uses,
879 unit_consumes,
880 unit_consumes_aliases,
881 &mut errors,
882 refs,
883 tys,
884 );
885 }
886 }
887
888 errors
889}
890
891fn property_tier(_prop: &PropertyDecl) -> Option<bynk_syntax::ast::TestTier> {
896 None
897}
898
899pub fn value_block(e: &Expr) -> Block {
909 Block {
910 statements: Vec::new(),
911 tail: Box::new(e.clone()),
912 span: e.span,
913 tail_leading_comments: Vec::new(),
914 implicit_tail: false,
915 }
916}
917
918fn stub_value_typechecks(
923 e: &Expr,
924 op: &CapabilityOp,
925 resolved: &ResolvedCommons,
926 tys: &Arc<Types>,
927) -> bool {
928 let block = value_block(e);
929 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
930 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
931 let mut errs: Vec<CompileError> = Vec::new();
932 checker::check_handler_body(
933 resolved,
934 checker::HandlerBodyCheck::new(&block, &op.return_type, &op.params, &[]),
935 checker::CheckSinks {
936 tys,
937 expr_types: &mut expr_types,
938 errors: &mut errs,
939 refs: &mut RefSink::new(),
940 hints: &mut HintSink::new(),
941 locals: &mut LocalsSink::new(),
942 requirements: &mut RequirementSink::new(),
943 callees: &mut callees,
944 },
945 );
946 errs.is_empty()
947}
948
949fn block_uses_wire(block: &Block) -> bool {
953 fn contains_wire(e: &Expr) -> bool {
960 matches!(e.kind, ExprKind::Wire(_))
961 || bynk_syntax::ast::expr_children(e)
962 .into_iter()
963 .any(contains_wire)
964 }
965 for s in &block.statements {
966 let e = match s {
967 Statement::Let(l) => &l.value,
968 Statement::EffectLet(l) => &l.value,
969 Statement::Expect(x) => &x.value,
970 Statement::Send(x) => &x.value,
971 Statement::Do(d) => &d.value,
972 Statement::Assign(a) => &a.value,
973 };
974 if contains_wire(e) {
975 return true;
976 }
977 }
978 contains_wire(&block.tail)
979}
980
981fn block_uses_nobody(block: &Block) -> bool {
986 block.statements.iter().any(|s| {
987 matches!(s, Statement::EffectLet(l)
988 if l.principal.as_ref().is_some_and(|p| p.actor.name == "Nobody"))
989 })
990}
991
992pub fn register_call_record_types(
997 resolved: &mut ResolvedCommons,
998 target_name: &str,
999 unit_tables: &HashMap<String, UnitTable>,
1000) {
1001 let Some(table) = unit_tables.get(target_name) else {
1002 return;
1003 };
1004 for (cap_name, decl) in &table.capabilities {
1005 for op in &decl.ops {
1006 let fields: Vec<RecordField> = op
1007 .params
1008 .iter()
1009 .map(|p| RecordField {
1010 name: p.name.clone(),
1011 type_ref: p.type_ref.clone(),
1012 refinement: None,
1013 init: None,
1014 span: p.span,
1015 })
1016 .collect();
1017 let name = checker::call_record_type_name(cap_name, &op.name.name);
1018 resolved.types.insert(
1019 name.clone(),
1020 Arc::new(TypeDecl {
1021 type_params: Vec::new(),
1022 name: Ident {
1023 name,
1024 span: op.name.span,
1025 },
1026 body: TypeBody::Record(RecordBody {
1027 fields,
1028 span: op.name.span,
1029 }),
1030 documentation: None,
1031 span: op.name.span,
1032 trivia: Trivia::default(),
1033 }),
1034 );
1035 }
1036 }
1037}
1038
1039fn target_test_actors(table: Option<&UnitTable>) -> HashMap<String, bynk_syntax::ast::ActorDecl> {
1040 table.map(|t| t.actors.clone()).unwrap_or_default()
1041}
1042
1043fn target_test_services(table: Option<&UnitTable>) -> HashMap<String, checker::TestServiceSig> {
1044 use bynk_syntax::ast::ServiceProtocol;
1045 let Some(t) = table else {
1046 return HashMap::new();
1047 };
1048 t.services
1049 .iter()
1050 .map(|(name, decl)| {
1051 let protocol = match &decl.protocol {
1052 ServiceProtocol::Call => None,
1053 ServiceProtocol::Http => Some("http".to_string()),
1054 ServiceProtocol::Cron => Some("cron".to_string()),
1055 ServiceProtocol::Queue { .. } => Some("queue".to_string()),
1056 ServiceProtocol::WebSocket { .. } => Some("websocket".to_string()),
1057 ServiceProtocol::Events { .. } => Some("events".to_string()),
1058 };
1059 let handlers = decl
1060 .handlers
1061 .iter()
1062 .map(|h| checker::TestHandler {
1063 kind: h.kind.clone(),
1064 params: h.params.clone(),
1065 by_clause: h.by_clause.clone(),
1066 span: h.span,
1067 })
1068 .collect();
1069 (name.clone(), checker::TestServiceSig { protocol, handlers })
1070 })
1071 .collect()
1072}
1073
1074#[allow(clippy::too_many_arguments)]
1087pub fn typecheck_case_body(
1088 target_name: &str,
1089 body: &Block,
1090 unit_span: Span,
1091 unit_tables: &HashMap<String, UnitTable>,
1092 resolved: &ResolvedCommons,
1093 errors: &mut Vec<CompileError>,
1094 refs: &mut RefSink,
1095 initial_scope: HashMap<String, checker::TyId>,
1098 tys: &Arc<Types>,
1099) -> (
1100 HashMap<ExprId, checker::TypedExpr>,
1101 HashMap<ExprId, checker::Callee>,
1102) {
1103 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
1104 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
1105 let synthetic_return = TypeRef::Effect(
1109 Box::new(TypeRef::Result(
1110 Box::new(TypeRef::Unit(unit_span)),
1111 Box::new(TypeRef::ValidationError(unit_span)),
1112 unit_span,
1113 )),
1114 unit_span,
1115 );
1116
1117 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
1120 if let Some(table) = unit_tables.get(target_name) {
1121 for (name, decl) in &table.capabilities {
1122 let ops = decl
1123 .ops
1124 .iter()
1125 .map(|op| build_capability_op_info(op, &resolved.types, tys))
1126 .collect();
1127 capability_info_map.insert(
1128 name.clone(),
1129 checker::CapabilityInfo {
1130 name: name.clone(),
1131 ops,
1132 },
1133 );
1134 }
1135 }
1136
1137 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
1141
1142 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
1143 let return_ty_span = unit_span;
1144 let mut no_hints = HintSink::new();
1146 let mut no_locals = LocalsSink::new();
1147 let mut no_requirements = RequirementSink::new();
1149 let _ = checker::check_body(
1150 resolved,
1151 body,
1152 return_ty,
1153 return_ty_span,
1154 initial_scope,
1155 checker::CapabilityCtx {
1156 capabilities: capability_info_map.clone(),
1157 declared_capabilities: capability_info_map,
1158 given_remaining: given_declared.iter().cloned().collect(),
1159 given_used: HashSet::new(),
1160 given_entries: Vec::new(),
1161 given_anchor: None,
1162 },
1163 target_test_services(unit_tables.get(target_name)),
1164 target_test_actors(unit_tables.get(target_name)),
1165 None,
1166 checker::CheckSinks {
1167 tys,
1168 expr_types: &mut expr_types,
1169 errors,
1170 refs,
1171 hints: &mut no_hints,
1172 locals: &mut no_locals,
1173 requirements: &mut no_requirements,
1174 callees: &mut callees,
1175 },
1176 );
1177 (expr_types, callees)
1178}
1179
1180#[allow(clippy::too_many_arguments)]
1181fn check_test_case_body(
1182 target_name: &str,
1183 target_kind: UnitKind,
1184 case: &Case,
1185 unit_tables: &HashMap<String, UnitTable>,
1186 unit_uses: &HashMap<String, Vec<String>>,
1187 unit_consumes: &HashMap<String, Vec<String>>,
1188 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1189 errors: &mut Vec<CompileError>,
1190 refs: &mut RefSink,
1191 tys: &Arc<Types>,
1192) {
1193 let Some((mut resolved, _)) = build_privileged_resolved(
1194 target_name,
1195 unit_tables,
1196 unit_uses,
1197 unit_consumes,
1198 unit_consumes_aliases,
1199 ) else {
1200 return;
1201 };
1202 register_call_record_types(&mut resolved, target_name, unit_tables);
1203 let _ = target_kind;
1204 let _ = typecheck_case_body(
1205 target_name,
1206 &case.body,
1207 case.span,
1208 unit_tables,
1209 &resolved,
1210 errors,
1211 refs,
1212 HashMap::new(),
1213 tys,
1214 );
1215 check_restated_contract(&case.body, &resolved, errors);
1226}
1227
1228fn check_restated_contract(
1235 body: &Block,
1236 resolved: &ResolvedCommons,
1237 errors: &mut Vec<CompileError>,
1238) {
1239 let mut bound: HashMap<String, (&FnDecl, &[Expr])> = HashMap::new();
1242 for stmt in &body.statements {
1243 let (name, value) = match stmt {
1244 Statement::Let(l) | Statement::EffectLet(l) => (&l.name.name, &l.value),
1245 _ => continue,
1246 };
1247 if let ExprKind::Call {
1248 name: callee, args, ..
1249 } = &value.kind
1250 && let Some(f) = resolved.fns.get(&callee.name)
1251 && matches!(&f.name, FnName::Free(_))
1252 && !f.ensures.is_empty()
1253 && f.params.len() == args.len()
1254 {
1255 bound.insert(name.clone(), (f, args.as_slice()));
1256 }
1257 }
1258 if bound.is_empty() {
1259 return;
1260 }
1261 for stmt in &body.statements {
1262 let Statement::Expect(e) = stmt else { continue };
1263 for (result_name, (f, args)) in &bound {
1264 let result_ident = Expr {
1266 id: ExprId::SYNTHETIC,
1267 kind: ExprKind::Ident(Ident {
1268 name: result_name.clone(),
1269 span: e.span,
1270 }),
1271 span: e.span,
1272 };
1273 let mut subst: HashMap<&str, &Expr> = HashMap::new();
1274 subst.insert("result", &result_ident);
1275 for (p, a) in f.params.iter().zip(args.iter()) {
1276 subst.insert(p.name.name.as_str(), a);
1277 }
1278 for c in &f.ensures {
1279 if expr_alpha_eq_subst(&c.predicate, &e.value, &subst) {
1280 let FnName::Free(fname) = &f.name else {
1281 continue;
1282 };
1283 errors.push(
1284 CompileError::new(
1285 "bynk.contract.restated_by_test",
1286 e.span,
1287 format!(
1288 "this `expect` restates the `ensures {}` contract of `{}`, which is already checked at every call and by the runner",
1289 c.name.name, fname.name
1290 ),
1291 )
1292 .with_note(
1293 "a contract is checked everywhere for free — delete the restating test, or keep a `case` only for a specific witnessed value",
1294 ),
1295 );
1296 break;
1297 }
1298 }
1299 }
1300 }
1301}
1302
1303fn expr_alpha_eq_subst(pattern: &Expr, actual: &Expr, subst: &HashMap<&str, &Expr>) -> bool {
1309 if let ExprKind::Ident(id) = &pattern.kind
1310 && let Some(replacement) = subst.get(id.name.as_str())
1311 {
1312 return expr_struct_eq(replacement, actual);
1313 }
1314 match (&pattern.kind, &actual.kind) {
1315 (ExprKind::Ident(a), ExprKind::Ident(b)) => a.name == b.name,
1316 (ExprKind::IntLit { value: a, .. }, ExprKind::IntLit { value: b, .. }) => a == b,
1317 (ExprKind::BoolLit(a), ExprKind::BoolLit(b)) => a == b,
1318 (ExprKind::StrLit(a), ExprKind::StrLit(b)) => a == b,
1319 (ExprKind::Paren(a), _) => expr_alpha_eq_subst(a, actual, subst),
1320 (_, ExprKind::Paren(b)) => expr_alpha_eq_subst(pattern, b, subst),
1321 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1322 oa == ob && expr_alpha_eq_subst(la, lb, subst) && expr_alpha_eq_subst(ra, rb, subst)
1323 }
1324 (ExprKind::UnaryOp(oa, a), ExprKind::UnaryOp(ob, b)) => {
1325 oa == ob && expr_alpha_eq_subst(a, b, subst)
1326 }
1327 (
1328 ExprKind::MethodCall {
1329 receiver: ra,
1330 method: ma,
1331 args: aa,
1332 ..
1333 },
1334 ExprKind::MethodCall {
1335 receiver: rb,
1336 method: mb,
1337 args: ab,
1338 ..
1339 },
1340 ) => {
1341 ma.name == mb.name
1342 && aa.len() == ab.len()
1343 && expr_alpha_eq_subst(ra, rb, subst)
1344 && aa
1345 .iter()
1346 .zip(ab.iter())
1347 .all(|(x, y)| expr_alpha_eq_subst(x, y, subst))
1348 }
1349 _ => false,
1350 }
1351}
1352
1353fn expr_struct_eq(a: &Expr, b: &Expr) -> bool {
1356 match (&a.kind, &b.kind) {
1357 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
1358 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
1359 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
1360 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
1361 (ExprKind::Paren(x), _) => expr_struct_eq(x, b),
1362 (_, ExprKind::Paren(y)) => expr_struct_eq(a, y),
1363 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1364 oa == ob && expr_struct_eq(la, lb) && expr_struct_eq(ra, rb)
1365 }
1366 (ExprKind::UnaryOp(oa, x), ExprKind::UnaryOp(ob, y)) => oa == ob && expr_struct_eq(x, y),
1367 (
1368 ExprKind::MethodCall {
1369 receiver: ra,
1370 method: ma,
1371 args: aa,
1372 ..
1373 },
1374 ExprKind::MethodCall {
1375 receiver: rb,
1376 method: mb,
1377 args: ab,
1378 ..
1379 },
1380 ) => {
1381 ma.name == mb.name
1382 && aa.len() == ab.len()
1383 && expr_struct_eq(ra, rb)
1384 && aa.iter().zip(ab.iter()).all(|(x, y)| expr_struct_eq(x, y))
1385 }
1386 _ => false,
1387 }
1388}
1389
1390pub const PROP_GEN_DEPTH: u32 = 12;
1393
1394pub fn prop_binding_generable(
1399 ty: checker::TyId,
1400 types: &HashMap<String, Arc<TypeDecl>>,
1401 depth: u32,
1402 tys: &Arc<Types>,
1403) -> bool {
1404 if depth == 0 {
1405 return false;
1406 }
1407 match &*tys.get(ty) {
1408 checker::Ty::Base(_) => true,
1409 checker::Ty::Named { name, .. } => {
1410 let Some(decl) = types.get(name) else {
1411 return false;
1412 };
1413 match &decl.body {
1414 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1415 !refinement.as_ref().is_some_and(|r| {
1416 r.predicates
1417 .iter()
1418 .any(|p| matches!(p.kind, PredKind::Matches(_)))
1419 })
1420 }
1421 TypeBody::Sum(s) => s.variants.first().is_some_and(|v| {
1422 v.payload.iter().all(|f| {
1423 checker::resolve_type_ref(&f.type_ref, types, tys)
1424 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1425 })
1426 }),
1427 TypeBody::Record(r) => r.fields.iter().all(|f| {
1428 checker::resolve_type_ref(&f.type_ref, types, tys)
1429 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1430 }),
1431 }
1432 }
1433 _ => false,
1434 }
1435}
1436
1437fn named_refinement<'a>(
1440 ty: checker::TyId,
1441 types: &'a HashMap<String, Arc<TypeDecl>>,
1442 tys: &Arc<Types>,
1443) -> Option<&'a Refinement> {
1444 let node = tys.get(ty);
1445 let checker::Ty::Named { name, .. } = &*node else {
1446 return None;
1447 };
1448 match &types.get(name)?.body {
1449 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1450 refinement.as_ref()
1451 }
1452 _ => None,
1453 }
1454}
1455
1456fn predicate_restates_refinement(pred: &Expr, bound_var: &str, refinement: &Refinement) -> bool {
1462 let ExprKind::BinOp(op, lhs, rhs) = &pred.kind else {
1463 return false;
1464 };
1465 let ExprKind::Ident(id) = &lhs.kind else {
1467 return false;
1468 };
1469 if id.name != bound_var {
1470 return false;
1471 }
1472 let ExprKind::IntLit { value: n, .. } = &rhs.kind else {
1473 return false;
1474 };
1475 let n = *n;
1476 let positive = refinement
1477 .predicates
1478 .iter()
1479 .any(|p| matches!(p.kind, PredKind::Positive));
1480 let non_negative = refinement
1481 .predicates
1482 .iter()
1483 .any(|p| matches!(p.kind, PredKind::NonNegative));
1484 match op {
1485 BinOp::Gt if n == 0 => positive,
1487 BinOp::GtEq if n == 1 => positive,
1488 BinOp::GtEq if n == 0 => non_negative,
1490 _ => false,
1491 }
1492}
1493
1494#[derive(Clone, Copy)]
1497enum HistoryRestate {
1498 Invariant,
1500 Transition,
1502}
1503
1504fn as_new_field<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
1507 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
1508 return None;
1509 };
1510 let ExprKind::FieldAccess {
1511 receiver: inner,
1512 field: which,
1513 } = &receiver.kind
1514 else {
1515 return None;
1516 };
1517 let ExprKind::Ident(id) = &inner.kind else {
1518 return None;
1519 };
1520 (id.name == s && which.name == "new").then_some(field.name.as_str())
1521}
1522
1523fn as_step_root<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
1526 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
1527 return None;
1528 };
1529 let ExprKind::Ident(id) = &receiver.kind else {
1530 return None;
1531 };
1532 (id.name == s && (field.name == "old" || field.name == "new")).then_some(field.name.as_str())
1533}
1534
1535fn history_pred_matches(body: &Expr, s: &str, decl: &Expr, mode: HistoryRestate) -> bool {
1540 match mode {
1542 HistoryRestate::Invariant => {
1543 if let (Some(f), ExprKind::Ident(id)) = (as_new_field(body, s), &decl.kind) {
1544 return f == id.name;
1545 }
1546 }
1547 HistoryRestate::Transition => {
1548 if let (Some(root), ExprKind::Ident(id)) = (as_step_root(body, s), &decl.kind) {
1549 return root == id.name;
1550 }
1551 }
1552 }
1553 match (&body.kind, &decl.kind) {
1554 (ExprKind::Paren(x), _) => history_pred_matches(x, s, decl, mode),
1555 (_, ExprKind::Paren(y)) => history_pred_matches(body, s, y, mode),
1556 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
1557 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
1558 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
1559 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
1560 (ExprKind::None, ExprKind::None) => true,
1561 (ExprKind::Some(x), ExprKind::Some(y)) => history_pred_matches(x, s, y, mode),
1562 (ExprKind::UnaryOp(o1, x), ExprKind::UnaryOp(o2, y)) => {
1563 o1 == o2 && history_pred_matches(x, s, y, mode)
1564 }
1565 (ExprKind::BinOp(o1, l1, r1), ExprKind::BinOp(o2, l2, r2)) => {
1566 o1 == o2
1567 && history_pred_matches(l1, s, l2, mode)
1568 && history_pred_matches(r1, s, r2, mode)
1569 }
1570 (
1571 ExprKind::FieldAccess {
1572 receiver: r1,
1573 field: f1,
1574 },
1575 ExprKind::FieldAccess {
1576 receiver: r2,
1577 field: f2,
1578 },
1579 ) => f1.name == f2.name && history_pred_matches(r1, s, r2, mode),
1580 (
1581 ExprKind::MethodCall {
1582 receiver: r1,
1583 method: m1,
1584 args: a1,
1585 ..
1586 },
1587 ExprKind::MethodCall {
1588 receiver: r2,
1589 method: m2,
1590 args: a2,
1591 ..
1592 },
1593 ) => {
1594 m1.name == m2.name
1595 && a1.len() == a2.len()
1596 && history_pred_matches(r1, s, r2, mode)
1597 && a1
1598 .iter()
1599 .zip(a2)
1600 .all(|(x, y)| history_pred_matches(x, s, y, mode))
1601 }
1602 (
1603 ExprKind::Call {
1604 name: n1, args: a1, ..
1605 },
1606 ExprKind::Call {
1607 name: n2, args: a2, ..
1608 },
1609 ) => {
1610 n1.name == n2.name
1611 && a1.len() == a2.len()
1612 && a1
1613 .iter()
1614 .zip(a2)
1615 .all(|(x, y)| history_pred_matches(x, s, y, mode))
1616 }
1617 _ => false,
1618 }
1619}
1620
1621fn history_restates_invariant(prop: &PropertyDecl, run_var: &str, agent: &AgentDecl) -> bool {
1628 let [stmt] = prop.forall.body.statements.as_slice() else {
1629 return false;
1630 };
1631 let Statement::Expect(e) = stmt else {
1632 return false;
1633 };
1634 let ExprKind::MethodCall {
1636 receiver,
1637 method,
1638 args,
1639 ..
1640 } = &e.value.kind
1641 else {
1642 return false;
1643 };
1644 if method.name != "all" && method.name != "any" {
1645 return false;
1646 }
1647 let ExprKind::Ident(recv) = &receiver.kind else {
1648 return false;
1649 };
1650 if recv.name != run_var {
1651 return false;
1652 }
1653 let [arg] = args.as_slice() else {
1654 return false;
1655 };
1656 let ExprKind::Lambda(lam) = &arg.kind else {
1657 return false;
1658 };
1659 let [param] = lam.params.as_slice() else {
1660 return false;
1661 };
1662 let s = ¶m.name.name;
1663 agent
1664 .invariants
1665 .iter()
1666 .any(|inv| history_pred_matches(&lam.body, s, &inv.predicate, HistoryRestate::Invariant))
1667 || agent
1668 .transitions
1669 .iter()
1670 .any(|tr| history_pred_matches(&lam.body, s, &tr.predicate, HistoryRestate::Transition))
1671}
1672
1673fn history_call_type_name(agent: &str) -> String {
1677 format!("__History_{agent}_Call")
1678}
1679fn history_step_type_name(agent: &str) -> String {
1680 format!("__History_{agent}_Step")
1681}
1682fn history_state_type_name(agent: &str) -> String {
1683 format!("__History_{agent}_State")
1684}
1685
1686pub fn history_variant_name(handler: &str) -> String {
1690 let mut chars = handler.chars();
1691 match chars.next() {
1692 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1693 None => handler.to_string(),
1694 }
1695}
1696
1697pub fn history_handlers(agent: &AgentDecl) -> Vec<&Handler> {
1701 agent
1702 .handlers
1703 .iter()
1704 .filter(|h| matches!(h.kind, HandlerKind::Call) && h.method_name.is_some())
1705 .collect()
1706}
1707
1708pub fn check_history_binding(
1716 inner: &TypeRef,
1717 span: Span,
1718 resolved: &mut ResolvedCommons,
1719 refs: &mut RefSink,
1720 tys: &Arc<Types>,
1721) -> Result<checker::Ty, CompileError> {
1722 let TypeRef::Named(agent_id) = inner else {
1725 return Err(CompileError::new(
1726 "bynk.history.not_an_agent",
1727 span,
1728 format!(
1729 "`for all` cannot generate `History[{}]` — only an agent has handlers to sequence",
1730 ts_type_ref_display(inner)
1731 ),
1732 )
1733 .with_note("generate a driven call-history over an agent: `for all run: History[Agent]`"));
1734 };
1735 let Some(agent) = resolved.agents.get(&agent_id.name).cloned() else {
1736 return Err(CompileError::new(
1737 "bynk.history.not_an_agent",
1738 span,
1739 format!(
1740 "`for all run: History[{}]` names `{}`, which is not an agent in scope",
1741 agent_id.name, agent_id.name
1742 ),
1743 )
1744 .with_note(
1745 "only an agent (with handlers and reachable state) can be driven as a history",
1746 ));
1747 };
1748 refs.record(agent_id.span, SymbolKind::Type, &agent_id.name);
1749
1750 let handlers = history_handlers(&agent);
1751 for h in &handlers {
1755 for p in &h.params {
1756 let generable = checker::resolve_type_ref(&p.type_ref, &resolved.types, tys)
1757 .is_some_and(|t| prop_binding_generable(t, &resolved.types, PROP_GEN_DEPTH, tys));
1758 if !generable {
1759 return Err(CompileError::new(
1760 "bynk.history.not_generable",
1761 span,
1762 format!(
1763 "`History[{}]` cannot be driven — handler `{}`'s parameter `{}: {}` is not generable (e.g. a `Matches` refinement)",
1764 agent_id.name,
1765 h.method_name.as_ref().map(|m| m.name.as_str()).unwrap_or(""),
1766 p.name.name,
1767 ts_type_ref_display(&p.type_ref),
1768 ),
1769 )
1770 .with_note(
1771 "every handler parameter must be refinement-generable for the run to be seeded",
1772 ));
1773 }
1774 }
1775 }
1776
1777 let state_name = history_state_type_name(&agent_id.name);
1782 let call_name = history_call_type_name(&agent_id.name);
1783 let step_name = history_step_type_name(&agent_id.name);
1784
1785 let state_fields: Vec<RecordField> = agent
1788 .store_fields
1789 .iter()
1790 .filter(|f| f.kind.head.name == "Cell" && f.kind.args.len() == 1)
1791 .map(|f| RecordField {
1792 name: f.name.clone(),
1793 type_ref: f.kind.args[0].clone(),
1794 refinement: None,
1795 init: None,
1796 span: f.span,
1797 })
1798 .collect();
1799 resolved.types.insert(
1800 state_name.clone(),
1801 Arc::new(TypeDecl {
1802 type_params: Vec::new(),
1803 name: Ident {
1804 name: state_name.clone(),
1805 span,
1806 },
1807 body: TypeBody::Record(RecordBody {
1808 fields: state_fields,
1809 span,
1810 }),
1811 documentation: None,
1812 span,
1813 trivia: Trivia::default(),
1814 }),
1815 );
1816
1817 let variants: Vec<Variant> = handlers
1820 .iter()
1821 .map(|h| {
1822 let hname = h.method_name.as_ref().expect("call handler has a name");
1823 Variant {
1824 name: Ident {
1825 name: history_variant_name(&hname.name),
1826 span: hname.span,
1827 },
1828 payload: h
1829 .params
1830 .iter()
1831 .map(|p| VariantField {
1832 name: p.name.clone(),
1833 type_ref: p.type_ref.clone(),
1834 span: p.span,
1835 })
1836 .collect(),
1837 span: hname.span,
1838 }
1839 })
1840 .collect();
1841 resolved.types.insert(
1842 call_name.clone(),
1843 Arc::new(TypeDecl {
1844 type_params: Vec::new(),
1845 name: Ident {
1846 name: call_name.clone(),
1847 span,
1848 },
1849 body: TypeBody::Sum(SumBody {
1850 variants,
1851 embeds: Vec::new(),
1852 span,
1853 }),
1854 documentation: None,
1855 span,
1856 trivia: Trivia::default(),
1857 }),
1858 );
1859
1860 let step_fields = vec![
1863 RecordField {
1864 name: Ident {
1865 name: "call".to_string(),
1866 span,
1867 },
1868 type_ref: TypeRef::Named(Ident {
1869 name: call_name.clone(),
1870 span,
1871 }),
1872 refinement: None,
1873 init: None,
1874 span,
1875 },
1876 RecordField {
1877 name: Ident {
1878 name: "accepted".to_string(),
1879 span,
1880 },
1881 type_ref: TypeRef::Base(BaseType::Bool, span),
1882 refinement: None,
1883 init: None,
1884 span,
1885 },
1886 RecordField {
1887 name: Ident {
1888 name: "old".to_string(),
1889 span,
1890 },
1891 type_ref: TypeRef::Named(Ident {
1892 name: state_name.clone(),
1893 span,
1894 }),
1895 refinement: None,
1896 init: None,
1897 span,
1898 },
1899 RecordField {
1900 name: Ident {
1901 name: "new".to_string(),
1902 span,
1903 },
1904 type_ref: TypeRef::Named(Ident {
1905 name: state_name.clone(),
1906 span,
1907 }),
1908 refinement: None,
1909 init: None,
1910 span,
1911 },
1912 ];
1913 resolved.types.insert(
1914 step_name.clone(),
1915 Arc::new(TypeDecl {
1916 type_params: Vec::new(),
1917 name: Ident {
1918 name: step_name.clone(),
1919 span,
1920 },
1921 body: TypeBody::Record(RecordBody {
1922 fields: step_fields,
1923 span,
1924 }),
1925 documentation: None,
1926 span,
1927 trivia: Trivia::default(),
1928 }),
1929 );
1930
1931 Ok(checker::Ty::List(tys.intern(checker::Ty::Named {
1932 name: step_name,
1933 kind: checker::NamedKind::Record,
1934 args: Vec::new(),
1935 })))
1936}
1937
1938#[allow(clippy::too_many_arguments)]
1946fn check_property_body(
1947 target_name: &str,
1948 target_kind: UnitKind,
1949 prop: &PropertyDecl,
1950 unit_tables: &HashMap<String, UnitTable>,
1951 unit_uses: &HashMap<String, Vec<String>>,
1952 unit_consumes: &HashMap<String, Vec<String>>,
1953 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1954 errors: &mut Vec<CompileError>,
1955 refs: &mut RefSink,
1956 tys: &Arc<Types>,
1957) {
1958 let Some((mut resolved, _)) = build_privileged_resolved(
1959 target_name,
1960 unit_tables,
1961 unit_uses,
1962 unit_consumes,
1963 unit_consumes_aliases,
1964 ) else {
1965 return;
1966 };
1967 register_call_record_types(&mut resolved, target_name, unit_tables);
1968 let _ = target_kind;
1969
1970 let mut binding_scope: HashMap<String, checker::TyId> = HashMap::new();
1972 let mut binding_types: Vec<(String, Option<checker::TyId>)> = Vec::new();
1973 let mut history_binding: Option<(String, AgentDecl)> = None;
1976 for b in &prop.forall.bindings {
1977 if let TypeRef::History(inner, hspan) = &b.type_ref {
1980 match check_history_binding(inner, *hspan, &mut resolved, refs, tys) {
1981 Ok(step_ty) => {
1982 if let TypeRef::Named(agent_id) = &**inner
1983 && let Some(agent) = resolved.agents.get(&agent_id.name)
1984 {
1985 history_binding = Some((b.name.name.clone(), agent.clone()));
1986 }
1987 binding_scope.insert(b.name.name.clone(), tys.intern(step_ty.clone()));
1988 binding_types.push((b.name.name.clone(), Some(tys.intern(step_ty))));
1989 }
1990 Err(err) => {
1991 errors.push(err);
1992 binding_types.push((b.name.name.clone(), None));
1993 }
1994 }
1995 continue;
1996 }
1997 if let TypeRef::Named(id) = &b.type_ref
2000 && resolved.agents.contains_key(&id.name)
2001 {
2002 errors.push(
2003 CompileError::new(
2004 "bynk.val.agent_not_generable",
2005 b.type_ref.span(),
2006 format!(
2007 "`for all {}: {}` cannot generate an agent — a fabricated agent state need not be reachable",
2008 b.name.name, id.name
2009 ),
2010 )
2011 .with_note(
2012 "generate behaviour over an agent via handler sequences (the history rung), not fabricated states",
2013 ),
2014 );
2015 binding_types.push((b.name.name.clone(), None));
2016 continue;
2017 }
2018 let ty = match checker::resolve_type_ref(&b.type_ref, &resolved.types, tys) {
2019 Some(t) => {
2020 record_type_refs_in_property(&b.type_ref, &resolved, refs);
2021 t
2022 }
2023 None => {
2024 errors.push(CompileError::new(
2025 "bynk.val.unknown_type",
2026 b.type_ref.span(),
2027 format!(
2028 "`for all {}: {}` names a type that does not resolve",
2029 b.name.name,
2030 ts_type_ref_display(&b.type_ref)
2031 ),
2032 ));
2033 binding_types.push((b.name.name.clone(), None));
2034 continue;
2035 }
2036 };
2037 if !prop_binding_generable(ty, &resolved.types, PROP_GEN_DEPTH, tys) {
2038 errors.push(
2039 CompileError::new(
2040 "bynk.val.needs_pin",
2041 b.type_ref.span(),
2042 format!(
2043 "`for all {}: {}` cannot generate a value (e.g. a `Matches` refinement); a property cannot bind it",
2044 b.name.name,
2045 ts_type_ref_display(&b.type_ref)
2046 ),
2047 )
2048 .with_note("supply the witness in a `case` with a pinned `Val[T](...)` instead"),
2049 );
2050 }
2051 binding_scope.insert(b.name.name.clone(), ty);
2052 binding_types.push((b.name.name.clone(), Some(ty)));
2053 }
2054
2055 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
2058 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
2059 let unit_span = prop.span;
2060 let synthetic_return = TypeRef::Effect(
2061 Box::new(TypeRef::Result(
2062 Box::new(TypeRef::Unit(unit_span)),
2063 Box::new(TypeRef::ValidationError(unit_span)),
2064 unit_span,
2065 )),
2066 unit_span,
2067 );
2068 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
2069 if let Some(table) = unit_tables.get(target_name) {
2070 for (name, decl) in &table.capabilities {
2071 let ops = decl
2072 .ops
2073 .iter()
2074 .map(|op| build_capability_op_info(op, &resolved.types, tys))
2075 .collect();
2076 capability_info_map.insert(
2077 name.clone(),
2078 checker::CapabilityInfo {
2079 name: name.clone(),
2080 ops,
2081 },
2082 );
2083 }
2084 }
2085 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
2086 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
2087 let return_ty_span = prop.span;
2088 let mut no_hints = HintSink::new();
2089 let mut no_locals = LocalsSink::new();
2090 let mut no_requirements = RequirementSink::new();
2091 let _ = checker::check_body(
2095 &resolved,
2096 &prop.forall.body,
2097 return_ty,
2098 return_ty_span,
2099 binding_scope,
2100 checker::CapabilityCtx {
2101 capabilities: capability_info_map.clone(),
2102 declared_capabilities: capability_info_map,
2103 given_remaining: given_declared.iter().cloned().collect(),
2104 given_used: HashSet::new(),
2105 given_entries: Vec::new(),
2106 given_anchor: None,
2107 },
2108 target_test_services(unit_tables.get(target_name)),
2109 target_test_actors(unit_tables.get(target_name)),
2110 prop.forall.where_pred.as_ref(),
2111 checker::CheckSinks {
2112 tys,
2113 expr_types: &mut expr_types,
2114 errors,
2115 refs,
2116 hints: &mut no_hints,
2117 locals: &mut no_locals,
2118 requirements: &mut no_requirements,
2119 callees: &mut callees,
2120 },
2121 );
2122
2123 if let [(var, Some(ty))] = binding_types.as_slice()
2126 && let Some(refinement) = named_refinement(*ty, &resolved.types, tys)
2127 && let [stmt] = prop.forall.body.statements.as_slice()
2128 && let Statement::Expect(e) = stmt
2129 && predicate_restates_refinement(&e.value, var, refinement)
2130 {
2131 errors.push(
2132 CompileError::new(
2133 "bynk.property.restates_refinement",
2134 prop.forall.body.span,
2135 format!(
2136 "property `{}` merely re-checks a refinement type `{}` already guarantees",
2137 prop.name,
2138 ty.display(tys)
2139 ),
2140 )
2141 .with_note(
2142 "a property earns its keep by asserting behaviour over valid inputs, not by restating the type's refinement",
2143 ),
2144 );
2145 }
2146
2147 if let Some((run_var, agent)) = &history_binding
2152 && history_restates_invariant(prop, run_var, agent)
2153 {
2154 errors.push(
2155 CompileError::new(
2156 "bynk.history.restates_invariant",
2157 prop.forall.body.span,
2158 format!(
2159 "history property `{}` merely re-checks a guarantee agent `{}`'s `invariant`/`transition` already enforces on every reached state",
2160 prop.name, agent.name.name
2161 ),
2162 )
2163 .with_note(
2164 "a history property earns its keep by asserting a cross-step protocol, not by restating a per-state invariant",
2165 ),
2166 );
2167 }
2168}
2169
2170fn record_type_refs_in_property(
2173 type_ref: &TypeRef,
2174 resolved: &ResolvedCommons,
2175 refs: &mut RefSink,
2176) {
2177 checker::record_type_refs(type_ref, &resolved.types, &HashSet::new(), refs);
2178}
2179
2180pub fn build_privileged_resolved(
2186 owning_unit: &str,
2187 unit_tables: &HashMap<String, UnitTable>,
2188 unit_uses: &HashMap<String, Vec<String>>,
2189 unit_consumes: &HashMap<String, Vec<String>>,
2190 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
2191) -> Option<(ResolvedCommons, ())> {
2192 let local = unit_tables.get(owning_unit)?;
2193 let mut types = local.types.clone();
2194 let mut fns = local.fns.clone();
2195 let mut methods = local.methods.clone();
2196 if let Some(targets) = unit_uses.get(owning_unit) {
2197 for t in targets {
2198 if let Some(used) = unit_tables.get(t) {
2199 for (n, d) in &used.types {
2200 types.entry(n.clone()).or_insert_with(|| d.clone());
2201 }
2202 for (n, d) in &used.fns {
2203 fns.entry(n.clone()).or_insert_with(|| d.clone());
2204 }
2205 for (n, mt) in &used.methods {
2206 let entry = methods.entry(n.clone()).or_default();
2207 for (m, decl) in &mt.instance {
2208 entry
2209 .instance
2210 .entry(m.clone())
2211 .or_insert_with(|| decl.clone());
2212 }
2213 for (m, decl) in &mt.statics {
2214 entry
2215 .statics
2216 .entry(m.clone())
2217 .or_insert_with(|| decl.clone());
2218 }
2219 }
2220 }
2221 }
2222 }
2223 if let Some(consumed) = unit_consumes.get(owning_unit) {
2225 for t in consumed {
2226 if let Some(used) = unit_tables.get(t) {
2227 for (n, d) in &used.types {
2228 types.entry(n.clone()).or_insert_with(|| d.clone());
2229 }
2230 for (n, mt) in &used.methods {
2231 let entry = methods.entry(n.clone()).or_default();
2232 for (m, decl) in &mt.instance {
2233 entry
2234 .instance
2235 .entry(m.clone())
2236 .or_insert_with(|| decl.clone());
2237 }
2238 }
2239 }
2240 }
2241 }
2242 let cross_context = build_cross_context_info(
2243 owning_unit,
2244 unit_consumes,
2245 unit_consumes_aliases,
2246 unit_uses,
2247 unit_tables,
2248 );
2249 let synthetic_commons = Commons {
2250 name: QualifiedName {
2251 parts: owning_unit
2252 .split('.')
2253 .map(|part| Ident {
2254 name: part.to_string(),
2255 span: Span::default(),
2256 })
2257 .collect(),
2258 span: Span::default(),
2259 },
2260 items: Vec::new(),
2261 uses: Vec::new(),
2262 documentation: None,
2263 form: CommonsForm::Brace,
2264 span: Span::default(),
2265 trivia: Trivia::default(),
2266 trailing_comments: Vec::new(),
2267 };
2268 let agents_for_resolved = unit_tables
2269 .get(owning_unit)
2270 .map(|t| t.agents.clone())
2271 .unwrap_or_default();
2272 let no_local_events = HashMap::new();
2273 let resolved = ResolvedCommons::new(
2274 synthetic_commons,
2275 types,
2276 &local.types,
2277 fns,
2278 methods,
2279 agents_for_resolved,
2280 &no_local_events,
2284 cross_context,
2285 HashMap::new(),
2286 false,
2287 HashSet::new(),
2288 );
2289 Some((resolved, ()))
2290}