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)]
1080pub fn typecheck_case_body(
1081 target_name: &str,
1082 body: &Block,
1083 unit_span: Span,
1084 unit_tables: &HashMap<String, UnitTable>,
1085 resolved: &ResolvedCommons,
1086 errors: &mut Vec<CompileError>,
1087 refs: &mut RefSink,
1088 initial_scope: HashMap<String, checker::TyId>,
1091 tys: &Arc<Types>,
1092) -> HashMap<ExprId, checker::TypedExpr> {
1093 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
1094 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
1095 let synthetic_return = TypeRef::Effect(
1099 Box::new(TypeRef::Result(
1100 Box::new(TypeRef::Unit(unit_span)),
1101 Box::new(TypeRef::ValidationError(unit_span)),
1102 unit_span,
1103 )),
1104 unit_span,
1105 );
1106
1107 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
1110 if let Some(table) = unit_tables.get(target_name) {
1111 for (name, decl) in &table.capabilities {
1112 let ops = decl
1113 .ops
1114 .iter()
1115 .map(|op| build_capability_op_info(op, &resolved.types, tys))
1116 .collect();
1117 capability_info_map.insert(
1118 name.clone(),
1119 checker::CapabilityInfo {
1120 name: name.clone(),
1121 ops,
1122 },
1123 );
1124 }
1125 }
1126
1127 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
1131
1132 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
1133 let return_ty_span = unit_span;
1134 let mut no_hints = HintSink::new();
1136 let mut no_locals = LocalsSink::new();
1137 let mut no_requirements = RequirementSink::new();
1139 let _ = checker::check_body(
1140 resolved,
1141 body,
1142 return_ty,
1143 return_ty_span,
1144 initial_scope,
1145 checker::CapabilityCtx {
1146 capabilities: capability_info_map.clone(),
1147 declared_capabilities: capability_info_map,
1148 given_remaining: given_declared.iter().cloned().collect(),
1149 given_used: HashSet::new(),
1150 given_entries: Vec::new(),
1151 given_anchor: None,
1152 },
1153 target_test_services(unit_tables.get(target_name)),
1154 target_test_actors(unit_tables.get(target_name)),
1155 None,
1156 checker::CheckSinks {
1157 tys,
1158 expr_types: &mut expr_types,
1159 errors,
1160 refs,
1161 hints: &mut no_hints,
1162 locals: &mut no_locals,
1163 requirements: &mut no_requirements,
1164 callees: &mut callees,
1165 },
1166 );
1167 expr_types
1168}
1169
1170#[allow(clippy::too_many_arguments)]
1171fn check_test_case_body(
1172 target_name: &str,
1173 target_kind: UnitKind,
1174 case: &Case,
1175 unit_tables: &HashMap<String, UnitTable>,
1176 unit_uses: &HashMap<String, Vec<String>>,
1177 unit_consumes: &HashMap<String, Vec<String>>,
1178 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1179 errors: &mut Vec<CompileError>,
1180 refs: &mut RefSink,
1181 tys: &Arc<Types>,
1182) {
1183 let Some((mut resolved, _)) = build_privileged_resolved(
1184 target_name,
1185 unit_tables,
1186 unit_uses,
1187 unit_consumes,
1188 unit_consumes_aliases,
1189 ) else {
1190 return;
1191 };
1192 register_call_record_types(&mut resolved, target_name, unit_tables);
1193 let _ = target_kind;
1194 let _ = typecheck_case_body(
1195 target_name,
1196 &case.body,
1197 case.span,
1198 unit_tables,
1199 &resolved,
1200 errors,
1201 refs,
1202 HashMap::new(),
1203 tys,
1204 );
1205 check_restated_contract(&case.body, &resolved, errors);
1216}
1217
1218fn check_restated_contract(
1225 body: &Block,
1226 resolved: &ResolvedCommons,
1227 errors: &mut Vec<CompileError>,
1228) {
1229 let mut bound: HashMap<String, (&FnDecl, &[Expr])> = HashMap::new();
1232 for stmt in &body.statements {
1233 let (name, value) = match stmt {
1234 Statement::Let(l) | Statement::EffectLet(l) => (&l.name.name, &l.value),
1235 _ => continue,
1236 };
1237 if let ExprKind::Call {
1238 name: callee, args, ..
1239 } = &value.kind
1240 && let Some(f) = resolved.fns.get(&callee.name)
1241 && matches!(&f.name, FnName::Free(_))
1242 && !f.ensures.is_empty()
1243 && f.params.len() == args.len()
1244 {
1245 bound.insert(name.clone(), (f, args.as_slice()));
1246 }
1247 }
1248 if bound.is_empty() {
1249 return;
1250 }
1251 for stmt in &body.statements {
1252 let Statement::Expect(e) = stmt else { continue };
1253 for (result_name, (f, args)) in &bound {
1254 let result_ident = Expr {
1256 id: ExprId::SYNTHETIC,
1257 kind: ExprKind::Ident(Ident {
1258 name: result_name.clone(),
1259 span: e.span,
1260 }),
1261 span: e.span,
1262 };
1263 let mut subst: HashMap<&str, &Expr> = HashMap::new();
1264 subst.insert("result", &result_ident);
1265 for (p, a) in f.params.iter().zip(args.iter()) {
1266 subst.insert(p.name.name.as_str(), a);
1267 }
1268 for c in &f.ensures {
1269 if expr_alpha_eq_subst(&c.predicate, &e.value, &subst) {
1270 let FnName::Free(fname) = &f.name else {
1271 continue;
1272 };
1273 errors.push(
1274 CompileError::new(
1275 "bynk.contract.restated_by_test",
1276 e.span,
1277 format!(
1278 "this `expect` restates the `ensures {}` contract of `{}`, which is already checked at every call and by the runner",
1279 c.name.name, fname.name
1280 ),
1281 )
1282 .with_note(
1283 "a contract is checked everywhere for free — delete the restating test, or keep a `case` only for a specific witnessed value",
1284 ),
1285 );
1286 break;
1287 }
1288 }
1289 }
1290 }
1291}
1292
1293fn expr_alpha_eq_subst(pattern: &Expr, actual: &Expr, subst: &HashMap<&str, &Expr>) -> bool {
1299 if let ExprKind::Ident(id) = &pattern.kind
1300 && let Some(replacement) = subst.get(id.name.as_str())
1301 {
1302 return expr_struct_eq(replacement, actual);
1303 }
1304 match (&pattern.kind, &actual.kind) {
1305 (ExprKind::Ident(a), ExprKind::Ident(b)) => a.name == b.name,
1306 (ExprKind::IntLit { value: a, .. }, ExprKind::IntLit { value: b, .. }) => a == b,
1307 (ExprKind::BoolLit(a), ExprKind::BoolLit(b)) => a == b,
1308 (ExprKind::StrLit(a), ExprKind::StrLit(b)) => a == b,
1309 (ExprKind::Paren(a), _) => expr_alpha_eq_subst(a, actual, subst),
1310 (_, ExprKind::Paren(b)) => expr_alpha_eq_subst(pattern, b, subst),
1311 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1312 oa == ob && expr_alpha_eq_subst(la, lb, subst) && expr_alpha_eq_subst(ra, rb, subst)
1313 }
1314 (ExprKind::UnaryOp(oa, a), ExprKind::UnaryOp(ob, b)) => {
1315 oa == ob && expr_alpha_eq_subst(a, b, subst)
1316 }
1317 (
1318 ExprKind::MethodCall {
1319 receiver: ra,
1320 method: ma,
1321 args: aa,
1322 ..
1323 },
1324 ExprKind::MethodCall {
1325 receiver: rb,
1326 method: mb,
1327 args: ab,
1328 ..
1329 },
1330 ) => {
1331 ma.name == mb.name
1332 && aa.len() == ab.len()
1333 && expr_alpha_eq_subst(ra, rb, subst)
1334 && aa
1335 .iter()
1336 .zip(ab.iter())
1337 .all(|(x, y)| expr_alpha_eq_subst(x, y, subst))
1338 }
1339 _ => false,
1340 }
1341}
1342
1343fn expr_struct_eq(a: &Expr, b: &Expr) -> bool {
1346 match (&a.kind, &b.kind) {
1347 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
1348 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
1349 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
1350 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
1351 (ExprKind::Paren(x), _) => expr_struct_eq(x, b),
1352 (_, ExprKind::Paren(y)) => expr_struct_eq(a, y),
1353 (ExprKind::BinOp(oa, la, ra), ExprKind::BinOp(ob, lb, rb)) => {
1354 oa == ob && expr_struct_eq(la, lb) && expr_struct_eq(ra, rb)
1355 }
1356 (ExprKind::UnaryOp(oa, x), ExprKind::UnaryOp(ob, y)) => oa == ob && expr_struct_eq(x, y),
1357 (
1358 ExprKind::MethodCall {
1359 receiver: ra,
1360 method: ma,
1361 args: aa,
1362 ..
1363 },
1364 ExprKind::MethodCall {
1365 receiver: rb,
1366 method: mb,
1367 args: ab,
1368 ..
1369 },
1370 ) => {
1371 ma.name == mb.name
1372 && aa.len() == ab.len()
1373 && expr_struct_eq(ra, rb)
1374 && aa.iter().zip(ab.iter()).all(|(x, y)| expr_struct_eq(x, y))
1375 }
1376 _ => false,
1377 }
1378}
1379
1380pub const PROP_GEN_DEPTH: u32 = 12;
1383
1384pub fn prop_binding_generable(
1389 ty: checker::TyId,
1390 types: &HashMap<String, Arc<TypeDecl>>,
1391 depth: u32,
1392 tys: &Arc<Types>,
1393) -> bool {
1394 if depth == 0 {
1395 return false;
1396 }
1397 match &*tys.get(ty) {
1398 checker::Ty::Base(_) => true,
1399 checker::Ty::Named { name, .. } => {
1400 let Some(decl) = types.get(name) else {
1401 return false;
1402 };
1403 match &decl.body {
1404 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1405 !refinement.as_ref().is_some_and(|r| {
1406 r.predicates
1407 .iter()
1408 .any(|p| matches!(p.kind, PredKind::Matches(_)))
1409 })
1410 }
1411 TypeBody::Sum(s) => s.variants.first().is_some_and(|v| {
1412 v.payload.iter().all(|f| {
1413 checker::resolve_type_ref(&f.type_ref, types, tys)
1414 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1415 })
1416 }),
1417 TypeBody::Record(r) => r.fields.iter().all(|f| {
1418 checker::resolve_type_ref(&f.type_ref, types, tys)
1419 .is_some_and(|t| prop_binding_generable(t, types, depth - 1, tys))
1420 }),
1421 }
1422 }
1423 _ => false,
1424 }
1425}
1426
1427fn named_refinement<'a>(
1430 ty: checker::TyId,
1431 types: &'a HashMap<String, Arc<TypeDecl>>,
1432 tys: &Arc<Types>,
1433) -> Option<&'a Refinement> {
1434 let node = tys.get(ty);
1435 let checker::Ty::Named { name, .. } = &*node else {
1436 return None;
1437 };
1438 match &types.get(name)?.body {
1439 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
1440 refinement.as_ref()
1441 }
1442 _ => None,
1443 }
1444}
1445
1446fn predicate_restates_refinement(pred: &Expr, bound_var: &str, refinement: &Refinement) -> bool {
1452 let ExprKind::BinOp(op, lhs, rhs) = &pred.kind else {
1453 return false;
1454 };
1455 let ExprKind::Ident(id) = &lhs.kind else {
1457 return false;
1458 };
1459 if id.name != bound_var {
1460 return false;
1461 }
1462 let ExprKind::IntLit { value: n, .. } = &rhs.kind else {
1463 return false;
1464 };
1465 let n = *n;
1466 let positive = refinement
1467 .predicates
1468 .iter()
1469 .any(|p| matches!(p.kind, PredKind::Positive));
1470 let non_negative = refinement
1471 .predicates
1472 .iter()
1473 .any(|p| matches!(p.kind, PredKind::NonNegative));
1474 match op {
1475 BinOp::Gt if n == 0 => positive,
1477 BinOp::GtEq if n == 1 => positive,
1478 BinOp::GtEq if n == 0 => non_negative,
1480 _ => false,
1481 }
1482}
1483
1484#[derive(Clone, Copy)]
1487enum HistoryRestate {
1488 Invariant,
1490 Transition,
1492}
1493
1494fn as_new_field<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
1497 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
1498 return None;
1499 };
1500 let ExprKind::FieldAccess {
1501 receiver: inner,
1502 field: which,
1503 } = &receiver.kind
1504 else {
1505 return None;
1506 };
1507 let ExprKind::Ident(id) = &inner.kind else {
1508 return None;
1509 };
1510 (id.name == s && which.name == "new").then_some(field.name.as_str())
1511}
1512
1513fn as_step_root<'a>(e: &'a Expr, s: &str) -> Option<&'a str> {
1516 let ExprKind::FieldAccess { receiver, field } = &e.kind else {
1517 return None;
1518 };
1519 let ExprKind::Ident(id) = &receiver.kind else {
1520 return None;
1521 };
1522 (id.name == s && (field.name == "old" || field.name == "new")).then_some(field.name.as_str())
1523}
1524
1525fn history_pred_matches(body: &Expr, s: &str, decl: &Expr, mode: HistoryRestate) -> bool {
1530 match mode {
1532 HistoryRestate::Invariant => {
1533 if let (Some(f), ExprKind::Ident(id)) = (as_new_field(body, s), &decl.kind) {
1534 return f == id.name;
1535 }
1536 }
1537 HistoryRestate::Transition => {
1538 if let (Some(root), ExprKind::Ident(id)) = (as_step_root(body, s), &decl.kind) {
1539 return root == id.name;
1540 }
1541 }
1542 }
1543 match (&body.kind, &decl.kind) {
1544 (ExprKind::Paren(x), _) => history_pred_matches(x, s, decl, mode),
1545 (_, ExprKind::Paren(y)) => history_pred_matches(body, s, y, mode),
1546 (ExprKind::IntLit { value: x, .. }, ExprKind::IntLit { value: y, .. }) => x == y,
1547 (ExprKind::BoolLit(x), ExprKind::BoolLit(y)) => x == y,
1548 (ExprKind::StrLit(x), ExprKind::StrLit(y)) => x == y,
1549 (ExprKind::Ident(x), ExprKind::Ident(y)) => x.name == y.name,
1550 (ExprKind::None, ExprKind::None) => true,
1551 (ExprKind::Some(x), ExprKind::Some(y)) => history_pred_matches(x, s, y, mode),
1552 (ExprKind::UnaryOp(o1, x), ExprKind::UnaryOp(o2, y)) => {
1553 o1 == o2 && history_pred_matches(x, s, y, mode)
1554 }
1555 (ExprKind::BinOp(o1, l1, r1), ExprKind::BinOp(o2, l2, r2)) => {
1556 o1 == o2
1557 && history_pred_matches(l1, s, l2, mode)
1558 && history_pred_matches(r1, s, r2, mode)
1559 }
1560 (
1561 ExprKind::FieldAccess {
1562 receiver: r1,
1563 field: f1,
1564 },
1565 ExprKind::FieldAccess {
1566 receiver: r2,
1567 field: f2,
1568 },
1569 ) => f1.name == f2.name && history_pred_matches(r1, s, r2, mode),
1570 (
1571 ExprKind::MethodCall {
1572 receiver: r1,
1573 method: m1,
1574 args: a1,
1575 ..
1576 },
1577 ExprKind::MethodCall {
1578 receiver: r2,
1579 method: m2,
1580 args: a2,
1581 ..
1582 },
1583 ) => {
1584 m1.name == m2.name
1585 && a1.len() == a2.len()
1586 && history_pred_matches(r1, s, r2, mode)
1587 && a1
1588 .iter()
1589 .zip(a2)
1590 .all(|(x, y)| history_pred_matches(x, s, y, mode))
1591 }
1592 (
1593 ExprKind::Call {
1594 name: n1, args: a1, ..
1595 },
1596 ExprKind::Call {
1597 name: n2, args: a2, ..
1598 },
1599 ) => {
1600 n1.name == n2.name
1601 && a1.len() == a2.len()
1602 && a1
1603 .iter()
1604 .zip(a2)
1605 .all(|(x, y)| history_pred_matches(x, s, y, mode))
1606 }
1607 _ => false,
1608 }
1609}
1610
1611fn history_restates_invariant(prop: &PropertyDecl, run_var: &str, agent: &AgentDecl) -> bool {
1618 let [stmt] = prop.forall.body.statements.as_slice() else {
1619 return false;
1620 };
1621 let Statement::Expect(e) = stmt else {
1622 return false;
1623 };
1624 let ExprKind::MethodCall {
1626 receiver,
1627 method,
1628 args,
1629 ..
1630 } = &e.value.kind
1631 else {
1632 return false;
1633 };
1634 if method.name != "all" && method.name != "any" {
1635 return false;
1636 }
1637 let ExprKind::Ident(recv) = &receiver.kind else {
1638 return false;
1639 };
1640 if recv.name != run_var {
1641 return false;
1642 }
1643 let [arg] = args.as_slice() else {
1644 return false;
1645 };
1646 let ExprKind::Lambda(lam) = &arg.kind else {
1647 return false;
1648 };
1649 let [param] = lam.params.as_slice() else {
1650 return false;
1651 };
1652 let s = ¶m.name.name;
1653 agent
1654 .invariants
1655 .iter()
1656 .any(|inv| history_pred_matches(&lam.body, s, &inv.predicate, HistoryRestate::Invariant))
1657 || agent
1658 .transitions
1659 .iter()
1660 .any(|tr| history_pred_matches(&lam.body, s, &tr.predicate, HistoryRestate::Transition))
1661}
1662
1663fn history_call_type_name(agent: &str) -> String {
1667 format!("__History_{agent}_Call")
1668}
1669fn history_step_type_name(agent: &str) -> String {
1670 format!("__History_{agent}_Step")
1671}
1672fn history_state_type_name(agent: &str) -> String {
1673 format!("__History_{agent}_State")
1674}
1675
1676pub fn history_variant_name(handler: &str) -> String {
1680 let mut chars = handler.chars();
1681 match chars.next() {
1682 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1683 None => handler.to_string(),
1684 }
1685}
1686
1687pub fn history_handlers(agent: &AgentDecl) -> Vec<&Handler> {
1691 agent
1692 .handlers
1693 .iter()
1694 .filter(|h| matches!(h.kind, HandlerKind::Call) && h.method_name.is_some())
1695 .collect()
1696}
1697
1698pub fn check_history_binding(
1706 inner: &TypeRef,
1707 span: Span,
1708 resolved: &mut ResolvedCommons,
1709 refs: &mut RefSink,
1710 tys: &Arc<Types>,
1711) -> Result<checker::Ty, CompileError> {
1712 let TypeRef::Named(agent_id) = inner else {
1715 return Err(CompileError::new(
1716 "bynk.history.not_an_agent",
1717 span,
1718 format!(
1719 "`for all` cannot generate `History[{}]` — only an agent has handlers to sequence",
1720 ts_type_ref_display(inner)
1721 ),
1722 )
1723 .with_note("generate a driven call-history over an agent: `for all run: History[Agent]`"));
1724 };
1725 let Some(agent) = resolved.agents.get(&agent_id.name).cloned() else {
1726 return Err(CompileError::new(
1727 "bynk.history.not_an_agent",
1728 span,
1729 format!(
1730 "`for all run: History[{}]` names `{}`, which is not an agent in scope",
1731 agent_id.name, agent_id.name
1732 ),
1733 )
1734 .with_note(
1735 "only an agent (with handlers and reachable state) can be driven as a history",
1736 ));
1737 };
1738 refs.record(agent_id.span, SymbolKind::Type, &agent_id.name);
1739
1740 let handlers = history_handlers(&agent);
1741 for h in &handlers {
1745 for p in &h.params {
1746 let generable = checker::resolve_type_ref(&p.type_ref, &resolved.types, tys)
1747 .is_some_and(|t| prop_binding_generable(t, &resolved.types, PROP_GEN_DEPTH, tys));
1748 if !generable {
1749 return Err(CompileError::new(
1750 "bynk.history.not_generable",
1751 span,
1752 format!(
1753 "`History[{}]` cannot be driven — handler `{}`'s parameter `{}: {}` is not generable (e.g. a `Matches` refinement)",
1754 agent_id.name,
1755 h.method_name.as_ref().map(|m| m.name.as_str()).unwrap_or(""),
1756 p.name.name,
1757 ts_type_ref_display(&p.type_ref),
1758 ),
1759 )
1760 .with_note(
1761 "every handler parameter must be refinement-generable for the run to be seeded",
1762 ));
1763 }
1764 }
1765 }
1766
1767 let state_name = history_state_type_name(&agent_id.name);
1772 let call_name = history_call_type_name(&agent_id.name);
1773 let step_name = history_step_type_name(&agent_id.name);
1774
1775 let state_fields: Vec<RecordField> = agent
1778 .store_fields
1779 .iter()
1780 .filter(|f| f.kind.head.name == "Cell" && f.kind.args.len() == 1)
1781 .map(|f| RecordField {
1782 name: f.name.clone(),
1783 type_ref: f.kind.args[0].clone(),
1784 refinement: None,
1785 init: None,
1786 span: f.span,
1787 })
1788 .collect();
1789 resolved.types.insert(
1790 state_name.clone(),
1791 Arc::new(TypeDecl {
1792 type_params: Vec::new(),
1793 name: Ident {
1794 name: state_name.clone(),
1795 span,
1796 },
1797 body: TypeBody::Record(RecordBody {
1798 fields: state_fields,
1799 span,
1800 }),
1801 documentation: None,
1802 span,
1803 trivia: Trivia::default(),
1804 }),
1805 );
1806
1807 let variants: Vec<Variant> = handlers
1810 .iter()
1811 .map(|h| {
1812 let hname = h.method_name.as_ref().expect("call handler has a name");
1813 Variant {
1814 name: Ident {
1815 name: history_variant_name(&hname.name),
1816 span: hname.span,
1817 },
1818 payload: h
1819 .params
1820 .iter()
1821 .map(|p| VariantField {
1822 name: p.name.clone(),
1823 type_ref: p.type_ref.clone(),
1824 span: p.span,
1825 })
1826 .collect(),
1827 span: hname.span,
1828 }
1829 })
1830 .collect();
1831 resolved.types.insert(
1832 call_name.clone(),
1833 Arc::new(TypeDecl {
1834 type_params: Vec::new(),
1835 name: Ident {
1836 name: call_name.clone(),
1837 span,
1838 },
1839 body: TypeBody::Sum(SumBody {
1840 variants,
1841 embeds: Vec::new(),
1842 span,
1843 }),
1844 documentation: None,
1845 span,
1846 trivia: Trivia::default(),
1847 }),
1848 );
1849
1850 let step_fields = vec![
1853 RecordField {
1854 name: Ident {
1855 name: "call".to_string(),
1856 span,
1857 },
1858 type_ref: TypeRef::Named(Ident {
1859 name: call_name.clone(),
1860 span,
1861 }),
1862 refinement: None,
1863 init: None,
1864 span,
1865 },
1866 RecordField {
1867 name: Ident {
1868 name: "accepted".to_string(),
1869 span,
1870 },
1871 type_ref: TypeRef::Base(BaseType::Bool, span),
1872 refinement: None,
1873 init: None,
1874 span,
1875 },
1876 RecordField {
1877 name: Ident {
1878 name: "old".to_string(),
1879 span,
1880 },
1881 type_ref: TypeRef::Named(Ident {
1882 name: state_name.clone(),
1883 span,
1884 }),
1885 refinement: None,
1886 init: None,
1887 span,
1888 },
1889 RecordField {
1890 name: Ident {
1891 name: "new".to_string(),
1892 span,
1893 },
1894 type_ref: TypeRef::Named(Ident {
1895 name: state_name.clone(),
1896 span,
1897 }),
1898 refinement: None,
1899 init: None,
1900 span,
1901 },
1902 ];
1903 resolved.types.insert(
1904 step_name.clone(),
1905 Arc::new(TypeDecl {
1906 type_params: Vec::new(),
1907 name: Ident {
1908 name: step_name.clone(),
1909 span,
1910 },
1911 body: TypeBody::Record(RecordBody {
1912 fields: step_fields,
1913 span,
1914 }),
1915 documentation: None,
1916 span,
1917 trivia: Trivia::default(),
1918 }),
1919 );
1920
1921 Ok(checker::Ty::List(tys.intern(checker::Ty::Named {
1922 name: step_name,
1923 kind: checker::NamedKind::Record,
1924 args: Vec::new(),
1925 })))
1926}
1927
1928#[allow(clippy::too_many_arguments)]
1936fn check_property_body(
1937 target_name: &str,
1938 target_kind: UnitKind,
1939 prop: &PropertyDecl,
1940 unit_tables: &HashMap<String, UnitTable>,
1941 unit_uses: &HashMap<String, Vec<String>>,
1942 unit_consumes: &HashMap<String, Vec<String>>,
1943 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1944 errors: &mut Vec<CompileError>,
1945 refs: &mut RefSink,
1946 tys: &Arc<Types>,
1947) {
1948 let Some((mut resolved, _)) = build_privileged_resolved(
1949 target_name,
1950 unit_tables,
1951 unit_uses,
1952 unit_consumes,
1953 unit_consumes_aliases,
1954 ) else {
1955 return;
1956 };
1957 register_call_record_types(&mut resolved, target_name, unit_tables);
1958 let _ = target_kind;
1959
1960 let mut binding_scope: HashMap<String, checker::TyId> = HashMap::new();
1962 let mut binding_types: Vec<(String, Option<checker::TyId>)> = Vec::new();
1963 let mut history_binding: Option<(String, AgentDecl)> = None;
1966 for b in &prop.forall.bindings {
1967 if let TypeRef::History(inner, hspan) = &b.type_ref {
1970 match check_history_binding(inner, *hspan, &mut resolved, refs, tys) {
1971 Ok(step_ty) => {
1972 if let TypeRef::Named(agent_id) = &**inner
1973 && let Some(agent) = resolved.agents.get(&agent_id.name)
1974 {
1975 history_binding = Some((b.name.name.clone(), agent.clone()));
1976 }
1977 binding_scope.insert(b.name.name.clone(), tys.intern(step_ty.clone()));
1978 binding_types.push((b.name.name.clone(), Some(tys.intern(step_ty))));
1979 }
1980 Err(err) => {
1981 errors.push(err);
1982 binding_types.push((b.name.name.clone(), None));
1983 }
1984 }
1985 continue;
1986 }
1987 if let TypeRef::Named(id) = &b.type_ref
1990 && resolved.agents.contains_key(&id.name)
1991 {
1992 errors.push(
1993 CompileError::new(
1994 "bynk.val.agent_not_generable",
1995 b.type_ref.span(),
1996 format!(
1997 "`for all {}: {}` cannot generate an agent — a fabricated agent state need not be reachable",
1998 b.name.name, id.name
1999 ),
2000 )
2001 .with_note(
2002 "generate behaviour over an agent via handler sequences (the history rung), not fabricated states",
2003 ),
2004 );
2005 binding_types.push((b.name.name.clone(), None));
2006 continue;
2007 }
2008 let ty = match checker::resolve_type_ref(&b.type_ref, &resolved.types, tys) {
2009 Some(t) => {
2010 record_type_refs_in_property(&b.type_ref, &resolved, refs);
2011 t
2012 }
2013 None => {
2014 errors.push(CompileError::new(
2015 "bynk.val.unknown_type",
2016 b.type_ref.span(),
2017 format!(
2018 "`for all {}: {}` names a type that does not resolve",
2019 b.name.name,
2020 ts_type_ref_display(&b.type_ref)
2021 ),
2022 ));
2023 binding_types.push((b.name.name.clone(), None));
2024 continue;
2025 }
2026 };
2027 if !prop_binding_generable(ty, &resolved.types, PROP_GEN_DEPTH, tys) {
2028 errors.push(
2029 CompileError::new(
2030 "bynk.val.needs_pin",
2031 b.type_ref.span(),
2032 format!(
2033 "`for all {}: {}` cannot generate a value (e.g. a `Matches` refinement); a property cannot bind it",
2034 b.name.name,
2035 ts_type_ref_display(&b.type_ref)
2036 ),
2037 )
2038 .with_note("supply the witness in a `case` with a pinned `Val[T](...)` instead"),
2039 );
2040 }
2041 binding_scope.insert(b.name.name.clone(), ty);
2042 binding_types.push((b.name.name.clone(), Some(ty)));
2043 }
2044
2045 let mut expr_types: HashMap<ExprId, checker::TypedExpr> = HashMap::new();
2048 let mut callees: HashMap<ExprId, checker::Callee> = HashMap::new();
2049 let unit_span = prop.span;
2050 let synthetic_return = TypeRef::Effect(
2051 Box::new(TypeRef::Result(
2052 Box::new(TypeRef::Unit(unit_span)),
2053 Box::new(TypeRef::ValidationError(unit_span)),
2054 unit_span,
2055 )),
2056 unit_span,
2057 );
2058 let mut capability_info_map: HashMap<String, checker::CapabilityInfo> = HashMap::new();
2059 if let Some(table) = unit_tables.get(target_name) {
2060 for (name, decl) in &table.capabilities {
2061 let ops = decl
2062 .ops
2063 .iter()
2064 .map(|op| build_capability_op_info(op, &resolved.types, tys))
2065 .collect();
2066 capability_info_map.insert(
2067 name.clone(),
2068 checker::CapabilityInfo {
2069 name: name.clone(),
2070 ops,
2071 },
2072 );
2073 }
2074 }
2075 let given_declared: Vec<String> = capability_info_map.keys().cloned().collect();
2076 let return_ty = checker::resolve_type_ref(&synthetic_return, &resolved.types, tys).unwrap();
2077 let return_ty_span = prop.span;
2078 let mut no_hints = HintSink::new();
2079 let mut no_locals = LocalsSink::new();
2080 let mut no_requirements = RequirementSink::new();
2081 let _ = checker::check_body(
2085 &resolved,
2086 &prop.forall.body,
2087 return_ty,
2088 return_ty_span,
2089 binding_scope,
2090 checker::CapabilityCtx {
2091 capabilities: capability_info_map.clone(),
2092 declared_capabilities: capability_info_map,
2093 given_remaining: given_declared.iter().cloned().collect(),
2094 given_used: HashSet::new(),
2095 given_entries: Vec::new(),
2096 given_anchor: None,
2097 },
2098 target_test_services(unit_tables.get(target_name)),
2099 target_test_actors(unit_tables.get(target_name)),
2100 prop.forall.where_pred.as_ref(),
2101 checker::CheckSinks {
2102 tys,
2103 expr_types: &mut expr_types,
2104 errors,
2105 refs,
2106 hints: &mut no_hints,
2107 locals: &mut no_locals,
2108 requirements: &mut no_requirements,
2109 callees: &mut callees,
2110 },
2111 );
2112
2113 if let [(var, Some(ty))] = binding_types.as_slice()
2116 && let Some(refinement) = named_refinement(*ty, &resolved.types, tys)
2117 && let [stmt] = prop.forall.body.statements.as_slice()
2118 && let Statement::Expect(e) = stmt
2119 && predicate_restates_refinement(&e.value, var, refinement)
2120 {
2121 errors.push(
2122 CompileError::new(
2123 "bynk.property.restates_refinement",
2124 prop.forall.body.span,
2125 format!(
2126 "property `{}` merely re-checks a refinement type `{}` already guarantees",
2127 prop.name,
2128 ty.display(tys)
2129 ),
2130 )
2131 .with_note(
2132 "a property earns its keep by asserting behaviour over valid inputs, not by restating the type's refinement",
2133 ),
2134 );
2135 }
2136
2137 if let Some((run_var, agent)) = &history_binding
2142 && history_restates_invariant(prop, run_var, agent)
2143 {
2144 errors.push(
2145 CompileError::new(
2146 "bynk.history.restates_invariant",
2147 prop.forall.body.span,
2148 format!(
2149 "history property `{}` merely re-checks a guarantee agent `{}`'s `invariant`/`transition` already enforces on every reached state",
2150 prop.name, agent.name.name
2151 ),
2152 )
2153 .with_note(
2154 "a history property earns its keep by asserting a cross-step protocol, not by restating a per-state invariant",
2155 ),
2156 );
2157 }
2158}
2159
2160fn record_type_refs_in_property(
2163 type_ref: &TypeRef,
2164 resolved: &ResolvedCommons,
2165 refs: &mut RefSink,
2166) {
2167 checker::record_type_refs(type_ref, &resolved.types, &HashSet::new(), refs);
2168}
2169
2170pub fn build_privileged_resolved(
2176 owning_unit: &str,
2177 unit_tables: &HashMap<String, UnitTable>,
2178 unit_uses: &HashMap<String, Vec<String>>,
2179 unit_consumes: &HashMap<String, Vec<String>>,
2180 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
2181) -> Option<(ResolvedCommons, ())> {
2182 let local = unit_tables.get(owning_unit)?;
2183 let mut types = local.types.clone();
2184 let mut fns = local.fns.clone();
2185 let mut methods = local.methods.clone();
2186 if let Some(targets) = unit_uses.get(owning_unit) {
2187 for t in targets {
2188 if let Some(used) = unit_tables.get(t) {
2189 for (n, d) in &used.types {
2190 types.entry(n.clone()).or_insert_with(|| d.clone());
2191 }
2192 for (n, d) in &used.fns {
2193 fns.entry(n.clone()).or_insert_with(|| d.clone());
2194 }
2195 for (n, mt) in &used.methods {
2196 let entry = methods.entry(n.clone()).or_default();
2197 for (m, decl) in &mt.instance {
2198 entry
2199 .instance
2200 .entry(m.clone())
2201 .or_insert_with(|| decl.clone());
2202 }
2203 for (m, decl) in &mt.statics {
2204 entry
2205 .statics
2206 .entry(m.clone())
2207 .or_insert_with(|| decl.clone());
2208 }
2209 }
2210 }
2211 }
2212 }
2213 if let Some(consumed) = unit_consumes.get(owning_unit) {
2215 for t in consumed {
2216 if let Some(used) = unit_tables.get(t) {
2217 for (n, d) in &used.types {
2218 types.entry(n.clone()).or_insert_with(|| d.clone());
2219 }
2220 for (n, mt) in &used.methods {
2221 let entry = methods.entry(n.clone()).or_default();
2222 for (m, decl) in &mt.instance {
2223 entry
2224 .instance
2225 .entry(m.clone())
2226 .or_insert_with(|| decl.clone());
2227 }
2228 }
2229 }
2230 }
2231 }
2232 let cross_context = build_cross_context_info(
2233 owning_unit,
2234 unit_consumes,
2235 unit_consumes_aliases,
2236 unit_uses,
2237 unit_tables,
2238 );
2239 let synthetic_commons = Commons {
2240 name: QualifiedName {
2241 parts: owning_unit
2242 .split('.')
2243 .map(|part| Ident {
2244 name: part.to_string(),
2245 span: Span::default(),
2246 })
2247 .collect(),
2248 span: Span::default(),
2249 },
2250 items: Vec::new(),
2251 uses: Vec::new(),
2252 documentation: None,
2253 form: CommonsForm::Brace,
2254 span: Span::default(),
2255 trivia: Trivia::default(),
2256 trailing_comments: Vec::new(),
2257 };
2258 let agents_for_resolved = unit_tables
2259 .get(owning_unit)
2260 .map(|t| t.agents.clone())
2261 .unwrap_or_default();
2262 let no_local_events = HashMap::new();
2263 let resolved = ResolvedCommons::new(
2264 synthetic_commons,
2265 types,
2266 &local.types,
2267 fns,
2268 methods,
2269 agents_for_resolved,
2270 &no_local_events,
2274 cross_context,
2275 HashMap::new(),
2276 false,
2277 HashSet::new(),
2278 );
2279 Some((resolved, ()))
2280}