1use std::collections::{HashMap, HashSet};
16
17use regex::Regex;
18
19use crate::builtin_names::methods::*;
20use crate::builtin_names::types::*;
21use crate::hints::HintSink;
22use crate::index::{RefSink, SymbolKind};
23use crate::locals::LocalsSink;
24use crate::requirements::{
25 Materialize, Requirement, RequirementSink, RequirementSource, StoreKind,
26};
27use crate::resolver::{MethodTable, ResolvedCommons};
28use bynk_syntax::ast::*;
29use bynk_syntax::error::{Applicability, CompileError};
30use bynk_syntax::span::Span;
31
32mod calls;
33mod expressions;
34mod kernels;
35mod linearity;
36mod refinements;
37
38use calls::*;
39use expressions::*;
40use kernels::*;
41use refinements::*;
42
43pub use calls::check_state_initialiser;
44pub use refinements::zero_value_ts;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Ty {
51 Base(BaseType),
53 Named { name: String, kind: NamedKind },
56 Result(Box<Ty>, Box<Ty>),
58 Option(Box<Ty>),
60 Effect(Box<Ty>),
62 HttpResult(Box<Ty>),
64 QueueResult,
66 List(Box<Ty>),
68 Map(Box<Ty>, Box<Ty>),
71 Query(Box<Ty>),
77 Stream(Box<Ty>),
84 Connection(Box<Ty>),
91 ValidationError,
93 JsonError,
96 Unit,
98 Actor(Box<Ty>),
102 ActorSum(Vec<(String, Ty)>),
108 Fn { params: Vec<Ty>, ret: Box<Ty> },
112 Var(String),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum NamedKind {
128 Refined(BaseType),
130 Record,
132 Sum,
134 Opaque(BaseType),
138}
139
140impl Ty {
141 pub fn display(&self) -> String {
143 match self {
144 Ty::Base(b) => b.name().to_string(),
145 Ty::Named { name, .. } => name.clone(),
146 Ty::Result(t, e) => format!("Result[{}, {}]", t.display(), e.display()),
147 Ty::Option(t) => format!("Option[{}]", t.display()),
148 Ty::Effect(t) => format!("Effect[{}]", t.display()),
149 Ty::HttpResult(t) => format!("HttpResult[{}]", t.display()),
150 Ty::QueueResult => "QueueResult".to_string(),
151 Ty::List(t) => format!("List[{}]", t.display()),
152 Ty::Map(k, v) => format!("Map[{}, {}]", k.display(), v.display()),
153 Ty::Query(t) => format!("Query[{}]", t.display()),
154 Ty::Stream(t) => format!("Stream[{}]", t.display()),
155 Ty::Connection(t) => format!("Connection[{}]", t.display()),
156 Ty::ValidationError => "ValidationError".to_string(),
157 Ty::JsonError => "JsonError".to_string(),
158 Ty::Unit => "()".to_string(),
159 Ty::Actor(id) => format!("actor[{}]", id.display()),
160 Ty::ActorSum(members) => members
161 .iter()
162 .map(|(name, _)| name.clone())
163 .collect::<Vec<_>>()
164 .join(" | "),
165 Ty::Fn { params, ret } => {
166 let params = match params.len() {
167 0 => "()".to_string(),
168 1 if !matches!(params[0], Ty::Fn { .. }) => params[0].display(),
171 _ => format!(
172 "({})",
173 params
174 .iter()
175 .map(|p| p.display())
176 .collect::<Vec<_>>()
177 .join(", ")
178 ),
179 };
180 format!("{params} -> {}", ret.display())
181 }
182 Ty::Var(name) => name.clone(),
183 }
184 }
185
186 pub fn is_effect(&self) -> bool {
188 matches!(self, Ty::Effect(_))
189 }
190
191 pub fn is_held(&self) -> bool {
196 matches!(self, Ty::Connection(_))
197 }
198
199 pub fn held_inner(&self) -> Option<&Ty> {
203 match self {
204 Ty::Connection(t) => Some(t),
205 _ => None,
206 }
207 }
208
209 pub fn base(&self) -> Option<BaseType> {
213 match self {
214 Ty::Base(b) => Some(*b),
215 Ty::Named {
216 kind: NamedKind::Refined(b),
217 ..
218 } => Some(*b),
219 _ => None,
220 }
221 }
222}
223
224pub struct TypedCommons {
226 pub commons: Commons,
227 pub types: HashMap<String, TypeDecl>,
228 pub fns: HashMap<String, FnDecl>,
229 pub methods: HashMap<String, MethodTable>,
230 pub expr_types: HashMap<Span, Ty>,
231 pub warnings: Vec<CompileError>,
235}
236
237pub struct RecordCheck {
243 pub result: Result<TypedCommons, Vec<CompileError>>,
244 pub partial_expr_types: HashMap<Span, Ty>,
245}
246
247pub fn check(input: ResolvedCommons) -> Result<TypedCommons, Vec<CompileError>> {
250 check_record(
251 input,
252 &mut RefSink::new(),
253 &mut HintSink::new(),
254 &mut LocalsSink::new(),
255 &mut RequirementSink::new(),
256 )
257 .result
258}
259
260pub fn check_record(
263 input: ResolvedCommons,
264 refs: &mut RefSink,
265 hints: &mut HintSink,
266 locals: &mut LocalsSink,
267 requirements: &mut RequirementSink,
268) -> RecordCheck {
269 let mut errors = Vec::new();
270 let mut expr_types: HashMap<Span, Ty> = HashMap::new();
271
272 for item in &input.commons.items {
274 if let CommonsItem::Type(t) = item {
275 check_type_decl(t, &input.types, &mut errors);
276 }
277 }
278
279 for item in &input.commons.items {
281 if let CommonsItem::Fn(f) = item {
282 refs.set_owner(f.name.display());
283 check_fn(
284 f,
285 &input,
286 &mut expr_types,
287 &mut errors,
288 refs,
289 hints,
290 locals,
291 requirements,
292 );
293 refs.clear_owner();
294 }
295 }
296
297 let (hard_errors, warnings) = bynk_syntax::partition_by_severity(errors);
303 if hard_errors.is_empty() {
304 RecordCheck {
305 result: Ok(TypedCommons {
306 commons: input.commons,
307 types: input.types,
308 fns: input.fns,
309 methods: input.methods,
310 expr_types,
311 warnings,
312 }),
313 partial_expr_types: HashMap::new(),
314 }
315 } else {
316 let mut all = hard_errors;
319 all.extend(warnings);
320 RecordCheck {
321 result: Err(all),
322 partial_expr_types: expr_types,
323 }
324 }
325}
326
327#[allow(clippy::too_many_arguments)]
333pub fn check_handler_body(
334 body: &Block,
335 return_type: &TypeRef,
336 return_ty_span: Span,
337 params: &[Param],
338 input: &ResolvedCommons,
339 expr_types: &mut HashMap<Span, Ty>,
340 errors: &mut Vec<CompileError>,
341 refs: &mut RefSink,
342 hints: &mut HintSink,
343 locals: &mut LocalsSink,
344 requirements: &mut RequirementSink,
345 capabilities: HashMap<String, CapabilityInfo>,
346 declared_capabilities: HashMap<String, CapabilityInfo>,
347 agent_state_ty: Option<Ty>,
348 agent_self_scope: Option<HashMap<String, Ty>>,
349 given: &[CapRef],
350 given_anchor: Option<Span>,
351 report_unused: bool,
352 actor_binding: Option<(String, Ty)>,
357 store_cells: HashMap<String, Ty>,
361 store_maps: HashMap<String, (Ty, Ty)>,
365 store_sets: HashMap<String, Ty>,
368 store_caches: HashMap<String, (Ty, Ty, i64)>,
372 store_logs: HashMap<String, Ty>,
377 borrowed_held: HashSet<String>,
382) {
383 let Some(return_ty) = resolve_type_ref(return_type, &input.types) else {
384 return;
385 };
386 let no_vars = HashSet::new();
387 record_type_refs(return_type, &input.types, &no_vars, refs);
388 let mut param_scope: HashMap<String, Ty> = HashMap::new();
390 for p in params {
391 if let Some(t) = resolve_type_ref(&p.type_ref, &input.types) {
392 record_type_refs(&p.type_ref, &input.types, &no_vars, refs);
393 if p.name.name != "_" {
395 locals.record(p.name.name.clone(), p.name.span, t.display(), body.span);
396 }
397 param_scope.insert(p.name.name.clone(), t);
398 }
399 }
400 if let Some((binder, binder_ty)) = actor_binding {
401 if binder != "_" {
402 locals.record(binder.clone(), body.span, "actor".to_string(), body.span);
403 }
404 param_scope.insert(binder, binder_ty);
405 }
406 if let Some(self_scope) = agent_self_scope {
407 param_scope.extend(self_scope);
408 }
409 let effectful = matches!(&return_ty, Ty::Effect(_));
410 let given_entries: Vec<(String, Span)> = given
411 .iter()
412 .map(|c| (c.key().to_string(), c.span))
413 .collect();
414 let given_remaining: HashSet<String> = given_entries.iter().map(|(k, _)| k.clone()).collect();
415 let mut ctx = Ctx {
416 input,
417 expr_types,
418 errors,
419 refs,
420 hints,
421 locals,
422 requirements,
423 scopes: vec![param_scope],
424 return_ty: return_ty.clone(),
425 return_ty_span,
426 effectful,
427 agent_state_ty,
428 commit_seen: false,
429 caps: CapabilityCtx {
430 capabilities,
431 declared_capabilities,
432 given_remaining,
433 given_used: HashSet::new(),
434 given_entries: given_entries.clone(),
435 given_anchor,
436 },
437 in_test_body: false,
438 test_services: HashSet::new(),
439 type_vars: HashSet::new(),
440 store_cells,
441 store_maps,
442 store_sets,
443 store_caches,
444 store_logs,
445 };
446 let Some(body_ty) = type_of_block(body, Some(&return_ty), &mut ctx) else {
448 return;
449 };
450 linearity::check(
453 body,
454 params,
455 &input.types,
456 ctx.expr_types,
457 &borrowed_held,
458 ctx.errors,
459 );
460 if !compatible(&body_ty, &return_ty) {
461 ctx.errors.push(
462 CompileError::new(
463 "bynk.types.return_mismatch",
464 body.tail.span,
465 format!(
466 "handler body has type `{}`, but the declared return type is `{}`",
467 body_ty.display(),
468 return_ty.display()
469 ),
470 )
471 .with_label(return_ty_span, "declared return type"),
472 );
473 }
474 let mut reported: HashSet<&str> = HashSet::new();
481 for (i, (c, _)) in given_entries.iter().enumerate() {
482 if !report_unused {
483 break;
484 }
485 if ctx.caps.given_used.contains(c) || !reported.insert(c) {
486 continue;
487 }
488 ctx.errors.push(
489 CompileError::new(
490 "bynk.given.unused_capability",
491 return_ty_span,
492 format!("capability `{c}` is declared in `given` but never used in the body"),
493 )
494 .with_note(
495 "remove the capability from the `given` clause, or use it in the handler body",
496 )
497 .with_suggestion(
501 format!("remove `{c}` from the `given` clause"),
502 vec![(
503 given_removal_span(&given_entries, i, return_ty_span),
504 String::new(),
505 )],
506 Applicability::MachineApplicable,
507 ),
508 );
509 }
510}
511
512#[allow(clippy::too_many_arguments)]
526pub fn check_invariants(
527 invariants: &[Invariant],
528 store_cells: &HashMap<String, Ty>,
531 agent_name: &str,
532 input: &ResolvedCommons,
533 expr_types: &mut HashMap<Span, Ty>,
534 errors: &mut Vec<CompileError>,
535 refs: &mut RefSink,
536 hints: &mut HintSink,
537 locals: &mut LocalsSink,
538 requirements: &mut RequirementSink,
539) {
540 let mut seen: HashMap<&str, ()> = HashMap::new();
542 for inv in invariants {
543 if seen.insert(inv.name.name.as_str(), ()).is_some() {
544 errors.push(
545 CompileError::new(
546 "bynk.invariant.duplicate_name",
547 inv.name.span,
548 format!(
549 "agent `{agent_name}` declares more than one invariant named `{}`",
550 inv.name.name
551 ),
552 )
553 .with_note("give each invariant a distinct name"),
554 );
555 }
556 }
557
558 let mut field_scope: HashMap<String, Ty> = HashMap::new();
561 for (name, ty) in store_cells {
562 field_scope.insert(name.clone(), ty.clone());
563 }
564
565 for inv in invariants {
566 if let Some(span) = predicate_cross_agent_ref(&inv.predicate, input) {
569 errors.push(
570 CompileError::new(
571 "bynk.invariant.cross_agent_reference",
572 span,
573 format!(
574 "invariant `{}` references another agent; invariants constrain a \
575 single agent's reachable states",
576 inv.name.name
577 ),
578 )
579 .with_note(
580 "a property that genuinely spans agents belongs in a saga or a scenario, \
581 not an invariant — see §14",
582 ),
583 );
584 continue;
585 }
586 if let Some(span) = predicate_impure_construct(&inv.predicate) {
587 errors.push(
588 CompileError::new(
589 "bynk.invariant.impure_predicate",
590 span,
591 format!(
592 "invariant `{}` uses an effectful or test-only construct; invariant \
593 predicates must be pure",
594 inv.name.name
595 ),
596 )
597 .with_note(
598 "an invariant predicate may read state fields and call pure value methods, \
599 but not perform effects",
600 ),
601 );
602 continue;
603 }
604
605 let bool_ty = Ty::Base(BaseType::Bool);
606 let mut ctx = Ctx {
607 input,
608 expr_types,
609 errors,
610 refs,
611 hints,
612 locals,
613 requirements,
614 scopes: vec![field_scope.clone()],
615 return_ty: bool_ty.clone(),
616 return_ty_span: inv.predicate.span,
617 effectful: false,
620 agent_state_ty: None,
621 commit_seen: false,
622 caps: CapabilityCtx {
623 capabilities: HashMap::new(),
624 declared_capabilities: HashMap::new(),
625 given_remaining: HashSet::new(),
626 given_used: HashSet::new(),
627 given_entries: Vec::new(),
628 given_anchor: None,
629 },
630 in_test_body: false,
631 test_services: HashSet::new(),
632 type_vars: HashSet::new(),
633 store_cells: HashMap::new(),
634 store_maps: HashMap::new(),
635 store_sets: HashMap::new(),
636 store_caches: HashMap::new(),
637 store_logs: HashMap::new(),
638 };
639 let pred_ty = type_of(&inv.predicate, Some(&bool_ty), &mut ctx);
640 if let Some(t) = pred_ty
641 && t.base() != Some(BaseType::Bool)
642 {
643 ctx.errors.push(
644 CompileError::new(
645 "bynk.invariant.not_bool",
646 inv.predicate.span,
647 format!(
648 "invariant `{}` predicate has type `{}`, but an invariant must be `Bool`",
649 inv.name.name,
650 t.display()
651 ),
652 )
653 .with_note("an invariant predicate is a `Bool`-valued property of the state"),
654 );
655 }
656 }
657}
658
659fn predicate_cross_agent_ref(e: &Expr, input: &ResolvedCommons) -> Option<Span> {
663 let is_agent = |name: &str| input.agents.contains_key(name);
664 match &e.kind {
665 ExprKind::Ident(id) if is_agent(&id.name) => Some(id.span),
666 ExprKind::Call { name, .. } if is_agent(&name.name) => Some(name.span),
667 ExprKind::ConstructorCall { type_name, .. } if is_agent(&type_name.name) => {
668 Some(type_name.span)
669 }
670 ExprKind::RecordConstruction { type_name, .. } if is_agent(&type_name.name) => {
671 Some(type_name.span)
672 }
673 _ => predicate_children(e)
674 .into_iter()
675 .find_map(|c| predicate_cross_agent_ref(c, input)),
676 }
677}
678
679fn predicate_impure_construct(e: &Expr) -> Option<Span> {
683 match &e.kind {
684 ExprKind::EffectPure(_)
685 | ExprKind::Question(_)
686 | ExprKind::Expect(_)
687 | ExprKind::Val { .. } => Some(e.span),
688 _ => predicate_children(e)
689 .into_iter()
690 .find_map(predicate_impure_construct),
691 }
692}
693
694fn expr_reads_ident(e: &Expr, name: &str) -> bool {
701 match &e.kind {
702 ExprKind::Ident(id) => id.name == name,
703 _ => predicate_children(e)
704 .into_iter()
705 .any(|c| expr_reads_ident(c, name)),
706 }
707}
708
709fn predicate_children(e: &Expr) -> Vec<&Expr> {
710 match &e.kind {
711 ExprKind::BinOp(_, l, r) => vec![l, r],
712 ExprKind::UnaryOp(_, inner)
713 | ExprKind::Paren(inner)
714 | ExprKind::Ok(inner)
715 | ExprKind::Err(inner)
716 | ExprKind::Question(inner)
717 | ExprKind::Some(inner)
718 | ExprKind::EffectPure(inner)
719 | ExprKind::Expect(inner) => vec![inner],
720 ExprKind::Is { value, .. } => vec![value.as_ref()],
721 ExprKind::FieldAccess { receiver, .. } => vec![receiver.as_ref()],
722 ExprKind::MethodCall { receiver, args, .. } => {
723 let mut v = vec![receiver.as_ref()];
724 v.extend(args.iter());
725 v
726 }
727 ExprKind::Call { args, .. }
728 | ExprKind::ConstructorCall { args, .. }
729 | ExprKind::Val { args, .. }
730 | ExprKind::ListLit(args) => args.iter().collect(),
731 ExprKind::RecordConstruction { fields, .. } => {
732 fields.iter().filter_map(|f| f.value.as_ref()).collect()
733 }
734 ExprKind::RecordSpread {
735 base, overrides, ..
736 } => {
737 let mut v = vec![base.as_ref()];
738 v.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
739 v
740 }
741 ExprKind::InterpStr(parts) => parts
742 .iter()
743 .filter_map(|p| match p {
744 InterpPart::Hole(e) => Some(e.as_ref()),
745 InterpPart::Chunk(_) => None,
746 })
747 .collect(),
748 ExprKind::If {
749 cond,
750 then_block,
751 else_block,
752 } => vec![cond.as_ref(), &then_block.tail, &else_block.tail],
753 ExprKind::Block(b) => vec![&b.tail],
754 ExprKind::Match { discriminant, .. } => vec![discriminant.as_ref()],
755 _ => Vec::new(),
756 }
757}
758
759enum ConstLit {
764 Int(i64),
765 Float(f64),
766 Str(String),
767 Bool(bool),
768 Unit,
769}
770
771impl ConstLit {
772 fn display(&self) -> String {
773 match self {
774 ConstLit::Int(n) => n.to_string(),
775 ConstLit::Float(v) => v.to_string(),
776 ConstLit::Str(s) => format!("{s:?}"),
777 ConstLit::Bool(b) => b.to_string(),
778 ConstLit::Unit => "()".to_string(),
779 }
780 }
781}
782
783#[derive(Default)]
788pub struct CapabilityCtx {
789 pub capabilities: HashMap<String, CapabilityInfo>,
792 pub declared_capabilities: HashMap<String, CapabilityInfo>,
796 pub given_remaining: HashSet<String>,
800 pub given_used: HashSet<String>,
802 pub given_entries: Vec<(String, Span)>,
807 pub given_anchor: Option<Span>,
812}
813
814pub struct Ctx<'a> {
815 pub input: &'a ResolvedCommons,
816 pub expr_types: &'a mut HashMap<Span, Ty>,
817 pub errors: &'a mut Vec<CompileError>,
818 pub refs: &'a mut RefSink,
824 pub hints: &'a mut HintSink,
828 pub locals: &'a mut LocalsSink,
832 pub requirements: &'a mut RequirementSink,
836 pub scopes: Vec<HashMap<String, Ty>>,
838 pub return_ty: Ty,
839 pub return_ty_span: Span,
840 pub effectful: bool,
843 pub agent_state_ty: Option<Ty>,
846 pub commit_seen: bool,
849 pub caps: CapabilityCtx,
852 pub in_test_body: bool,
855 pub test_services: HashSet<String>,
860 pub type_vars: HashSet<String>,
864 pub store_cells: HashMap<String, Ty>,
868 pub store_maps: HashMap<String, (Ty, Ty)>,
872 pub store_sets: HashMap<String, Ty>,
875 pub store_caches: HashMap<String, (Ty, Ty, i64)>,
879 pub store_logs: HashMap<String, Ty>,
883}
884
885#[derive(Debug, Clone)]
887pub struct CapabilityInfo {
888 pub name: String,
889 pub ops: Vec<CapabilityOpInfo>,
890}
891
892#[derive(Debug, Clone)]
893pub struct CapabilityOpInfo {
894 pub name: String,
895 pub params: Vec<Ty>,
896 pub return_ty: Ty,
897}
898
899impl<'a> Ctx<'a> {
900 pub fn lookup(&self, name: &str) -> Option<Ty> {
901 for scope in self.scopes.iter().rev() {
902 if let Some(t) = scope.get(name) {
903 return Some(t.clone());
904 }
905 }
906 None
907 }
908
909 pub fn lookup_root_ident(&self, expr: &Expr) -> Option<Ty> {
913 match &expr.kind {
914 ExprKind::Ident(id) => self.lookup(&id.name),
915 ExprKind::FieldAccess { receiver, .. } => self.lookup_root_ident(receiver),
916 ExprKind::MethodCall { receiver, .. } => self.lookup_root_ident(receiver),
917 _ => None,
918 }
919 }
920
921 pub fn push_scope(&mut self) {
922 self.scopes.push(HashMap::new());
923 }
924 pub fn pop_scope(&mut self) {
925 self.scopes.pop();
926 }
927 pub fn bind(&mut self, name: String, ty: Ty) {
928 self.scopes.last_mut().unwrap().insert(name, ty);
929 }
930}
931
932pub fn type_from_decl(id: &Ident, types: &HashMap<String, TypeDecl>) -> Option<Ty> {
936 let decl = types.get(&id.name)?;
937 Some(named_ty(decl))
938}
939
940pub fn named_ty(decl: &TypeDecl) -> Ty {
942 let kind = match &decl.body {
943 TypeBody::Refined { base, .. } => NamedKind::Refined(*base),
944 TypeBody::Record(_) => NamedKind::Record,
945 TypeBody::Sum(_) => NamedKind::Sum,
946 TypeBody::Opaque { base, .. } => NamedKind::Opaque(*base),
947 };
948 Ty::Named {
949 name: decl.name.name.clone(),
950 kind,
951 }
952}
953
954pub fn resolve_type_ref_in(
959 r: &TypeRef,
960 types: &HashMap<String, TypeDecl>,
961 vars: &HashSet<String>,
962) -> Option<Ty> {
963 match r {
964 TypeRef::Named(id) if vars.contains(&id.name) => Some(Ty::Var(id.name.clone())),
965 TypeRef::Result(t, e, _) => Some(Ty::Result(
966 Box::new(resolve_type_ref_in(t, types, vars)?),
967 Box::new(resolve_type_ref_in(e, types, vars)?),
968 )),
969 TypeRef::Option(t, _) => Some(Ty::Option(Box::new(resolve_type_ref_in(t, types, vars)?))),
970 TypeRef::Effect(t, _) => Some(Ty::Effect(Box::new(resolve_type_ref_in(t, types, vars)?))),
971 TypeRef::HttpResult(t, _) => Some(Ty::HttpResult(Box::new(resolve_type_ref_in(
972 t, types, vars,
973 )?))),
974 TypeRef::List(t, _) => Some(Ty::List(Box::new(resolve_type_ref_in(t, types, vars)?))),
975 TypeRef::Query(t, _) => Some(Ty::Query(Box::new(resolve_type_ref_in(t, types, vars)?))),
976 TypeRef::Stream(t, _) => Some(Ty::Stream(Box::new(resolve_type_ref_in(t, types, vars)?))),
977 TypeRef::Connection(t, _) => Some(Ty::Connection(Box::new(resolve_type_ref_in(
978 t, types, vars,
979 )?))),
980 TypeRef::Map(k, v, _) => Some(Ty::Map(
981 Box::new(resolve_type_ref_in(k, types, vars)?),
982 Box::new(resolve_type_ref_in(v, types, vars)?),
983 )),
984 TypeRef::Fn(params, ret, _) => {
985 let params: Option<Vec<Ty>> = params
986 .iter()
987 .map(|p| resolve_type_ref_in(p, types, vars))
988 .collect();
989 Some(Ty::Fn {
990 params: params?,
991 ret: Box::new(resolve_type_ref_in(ret, types, vars)?),
992 })
993 }
994 _ => resolve_type_ref(r, types),
995 }
996}
997
998fn substitute(t: &Ty, subst: &HashMap<String, Ty>) -> Ty {
1002 match t {
1003 Ty::Var(n) => subst.get(n).cloned().unwrap_or_else(|| t.clone()),
1004 Ty::Result(a, b) => Ty::Result(
1005 Box::new(substitute(a, subst)),
1006 Box::new(substitute(b, subst)),
1007 ),
1008 Ty::Option(a) => Ty::Option(Box::new(substitute(a, subst))),
1009 Ty::Effect(a) => Ty::Effect(Box::new(substitute(a, subst))),
1010 Ty::HttpResult(a) => Ty::HttpResult(Box::new(substitute(a, subst))),
1011 Ty::List(a) => Ty::List(Box::new(substitute(a, subst))),
1012 Ty::Stream(a) => Ty::Stream(Box::new(substitute(a, subst))),
1013 Ty::Connection(a) => Ty::Connection(Box::new(substitute(a, subst))),
1014 Ty::Map(k, v) => Ty::Map(
1015 Box::new(substitute(k, subst)),
1016 Box::new(substitute(v, subst)),
1017 ),
1018 Ty::Fn { params, ret } => Ty::Fn {
1019 params: params.iter().map(|p| substitute(p, subst)).collect(),
1020 ret: Box::new(substitute(ret, subst)),
1021 },
1022 _ => t.clone(),
1023 }
1024}
1025
1026fn contains_var(t: &Ty) -> bool {
1028 match t {
1029 Ty::Var(_) => true,
1030 Ty::Result(a, b) | Ty::Map(a, b) => contains_var(a) || contains_var(b),
1031 Ty::Option(a)
1032 | Ty::Effect(a)
1033 | Ty::HttpResult(a)
1034 | Ty::List(a)
1035 | Ty::Stream(a)
1036 | Ty::Connection(a) => contains_var(a),
1037 Ty::Fn { params, ret } => params.iter().any(contains_var) || contains_var(ret),
1038 _ => false,
1039 }
1040}
1041
1042fn contains_flexible_var(t: &Ty, rigid: &HashSet<String>) -> bool {
1047 match t {
1048 Ty::Var(n) => !rigid.contains(n),
1049 Ty::Result(a, b) | Ty::Map(a, b) => {
1050 contains_flexible_var(a, rigid) || contains_flexible_var(b, rigid)
1051 }
1052 Ty::Option(a)
1053 | Ty::Effect(a)
1054 | Ty::HttpResult(a)
1055 | Ty::List(a)
1056 | Ty::Stream(a)
1057 | Ty::Connection(a) => contains_flexible_var(a, rigid),
1058 Ty::Fn { params, ret } => {
1059 params.iter().any(|p| contains_flexible_var(p, rigid))
1060 || contains_flexible_var(ret, rigid)
1061 }
1062 _ => false,
1063 }
1064}
1065
1066fn unify(pattern: &Ty, actual: &Ty, subst: &mut HashMap<String, Ty>) -> bool {
1073 match (pattern, actual) {
1074 (Ty::Var(n), a) => match subst.get(n) {
1075 Some(bound) => bound == a,
1076 None => {
1077 subst.insert(n.clone(), a.clone());
1078 true
1079 }
1080 },
1081 (Ty::Result(a1, b1), Ty::Result(a2, b2)) | (Ty::Map(a1, b1), Ty::Map(a2, b2)) => {
1082 unify(a1, a2, subst) && unify(b1, b2, subst)
1083 }
1084 (Ty::Option(a1), Ty::Option(a2))
1085 | (Ty::Effect(a1), Ty::Effect(a2))
1086 | (Ty::HttpResult(a1), Ty::HttpResult(a2))
1087 | (Ty::List(a1), Ty::List(a2))
1088 | (Ty::Stream(a1), Ty::Stream(a2))
1089 | (Ty::Connection(a1), Ty::Connection(a2)) => unify(a1, a2, subst),
1090 (
1091 Ty::Fn {
1092 params: p1,
1093 ret: r1,
1094 },
1095 Ty::Fn {
1096 params: p2,
1097 ret: r2,
1098 },
1099 ) => {
1100 p1.len() == p2.len()
1101 && p1.iter().zip(p2).all(|(a, b)| unify(a, b, subst))
1102 && unify(r1, r2, subst)
1103 }
1104 _ => true,
1107 }
1108}
1109
1110pub fn record_type_refs(
1117 r: &TypeRef,
1118 types: &HashMap<String, TypeDecl>,
1119 skip: &HashSet<String>,
1120 refs: &mut RefSink,
1121) {
1122 match r {
1123 TypeRef::Named(id) => {
1124 if types.contains_key(&id.name) && !skip.contains(&id.name) {
1125 refs.record(id.span, SymbolKind::Type, &id.name);
1126 }
1127 }
1128 TypeRef::Fn(params, ret, _) => {
1129 for p in params {
1130 record_type_refs(p, types, skip, refs);
1131 }
1132 record_type_refs(ret, types, skip, refs);
1133 }
1134 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
1135 record_type_refs(a, types, skip, refs);
1136 record_type_refs(b, types, skip, refs);
1137 }
1138 TypeRef::Option(t, _)
1139 | TypeRef::Effect(t, _)
1140 | TypeRef::HttpResult(t, _)
1141 | TypeRef::Query(t, _)
1142 | TypeRef::Stream(t, _)
1143 | TypeRef::Connection(t, _)
1144 | TypeRef::List(t, _) => record_type_refs(t, types, skip, refs),
1145 TypeRef::Base(..)
1146 | TypeRef::QueueResult(_)
1147 | TypeRef::ValidationError(_)
1148 | TypeRef::JsonError(_)
1149 | TypeRef::Unit(_) => {}
1150 }
1151}
1152
1153pub fn resolve_type_ref(r: &TypeRef, types: &HashMap<String, TypeDecl>) -> Option<Ty> {
1154 match r {
1155 TypeRef::Base(b, _) => Some(Ty::Base(*b)),
1156 TypeRef::Named(id) => type_from_decl(id, types),
1157 TypeRef::Fn(params, ret, _) => {
1160 let params: Option<Vec<Ty>> =
1161 params.iter().map(|p| resolve_type_ref(p, types)).collect();
1162 Some(Ty::Fn {
1163 params: params?,
1164 ret: Box::new(resolve_type_ref(ret, types)?),
1165 })
1166 }
1167 TypeRef::Result(t, e, _) => {
1168 let t = resolve_type_ref(t, types)?;
1169 let e = resolve_type_ref(e, types)?;
1170 Some(Ty::Result(Box::new(t), Box::new(e)))
1171 }
1172 TypeRef::Option(t, _) => {
1173 let t = resolve_type_ref(t, types)?;
1174 Some(Ty::Option(Box::new(t)))
1175 }
1176 TypeRef::Effect(t, _) => {
1177 let t = resolve_type_ref(t, types)?;
1178 Some(Ty::Effect(Box::new(t)))
1179 }
1180 TypeRef::HttpResult(t, _) => {
1181 let t = resolve_type_ref(t, types)?;
1182 Some(Ty::HttpResult(Box::new(t)))
1183 }
1184 TypeRef::List(t, _) => {
1185 let t = resolve_type_ref(t, types)?;
1186 Some(Ty::List(Box::new(t)))
1187 }
1188 TypeRef::Query(t, _) => {
1189 let t = resolve_type_ref(t, types)?;
1190 Some(Ty::Query(Box::new(t)))
1191 }
1192 TypeRef::Stream(t, _) => {
1193 let t = resolve_type_ref(t, types)?;
1194 Some(Ty::Stream(Box::new(t)))
1195 }
1196 TypeRef::Connection(t, _) => {
1197 let t = resolve_type_ref(t, types)?;
1198 Some(Ty::Connection(Box::new(t)))
1199 }
1200 TypeRef::Map(k, v, _) => {
1201 let k = resolve_type_ref(k, types)?;
1202 let v = resolve_type_ref(v, types)?;
1203 Some(Ty::Map(Box::new(k), Box::new(v)))
1204 }
1205 TypeRef::QueueResult(_) => Some(Ty::QueueResult),
1206 TypeRef::ValidationError(_) => Some(Ty::ValidationError),
1207 TypeRef::JsonError(_) => Some(Ty::JsonError),
1208 TypeRef::Unit(_) => Some(Ty::Unit),
1209 }
1210}
1211
1212pub fn compatible(t: &Ty, u: &Ty) -> bool {
1214 match (t, u) {
1215 (Ty::Base(a), Ty::Base(b)) => a == b,
1216 (Ty::Named { name: a, kind: ka }, Ty::Named { name: b, kind: kb }) => a == b && ka == kb,
1217 (
1219 Ty::Named {
1220 kind: NamedKind::Refined(b),
1221 ..
1222 },
1223 Ty::Base(target),
1224 ) => b == target,
1225 (Ty::Base(_), Ty::Named { .. }) => false,
1226 (Ty::Result(t1, e1), Ty::Result(t2, e2)) => compatible(t1, t2) && compatible(e1, e2),
1227 (Ty::Option(a), Ty::Option(b)) => compatible(a, b),
1228 (Ty::Effect(a), Ty::Effect(b)) => compatible(a, b),
1229 (Ty::HttpResult(a), Ty::HttpResult(b)) => compatible(a, b),
1230 (Ty::List(a), Ty::List(b)) => compatible(a, b),
1234 (Ty::Stream(a), Ty::Stream(b)) => compatible(a, b),
1237 (Ty::Connection(a), Ty::Connection(b)) => compatible(a, b),
1241 (Ty::Map(k1, v1), Ty::Map(k2, v2)) => k1 == k2 && compatible(v1, v2),
1242 (Ty::QueueResult, Ty::QueueResult) => true,
1243 (Ty::ValidationError, Ty::ValidationError) => true,
1244 (Ty::JsonError, Ty::JsonError) => true,
1245 (Ty::Unit, Ty::Unit) => true,
1246 (Ty::Fn { params: p, ret: r }, Ty::Fn { params: q, ret: s }) => {
1254 p.len() == q.len() && p.iter().zip(q).all(|(a, b)| compatible(b, a)) && compatible(r, s)
1255 }
1256 (Ty::Var(a), Ty::Var(b)) => a == b,
1260 _ => false,
1261 }
1262}
1263
1264pub fn type_of_block(block: &Block, expected: Option<&Ty>, ctx: &mut Ctx) -> Option<Ty> {
1265 ctx.push_scope();
1266 for stmt in &block.statements {
1267 match stmt {
1268 Statement::Let(l) => {
1269 let annot_ty = l.type_annot.as_ref().and_then(|a| {
1270 let r = resolve_type_ref_in(a, &ctx.input.types, &ctx.type_vars);
1273 if r.is_none() {
1274 ctx.errors.push(CompileError::new(
1275 "bynk.resolve.unknown_type",
1276 a.span(),
1277 "type in `let` annotation does not resolve",
1278 ));
1279 } else {
1280 record_type_refs(a, &ctx.input.types, &ctx.type_vars, ctx.refs);
1281 }
1282 r
1283 });
1284 let rhs_ty = type_of(&l.value, annot_ty.as_ref(), ctx);
1285 let final_ty = match (annot_ty, rhs_ty) {
1286 (Some(annot), Some(rhs)) => {
1287 if !compatible(&rhs, &annot) {
1288 ctx.errors.push(
1289 CompileError::new(
1290 "bynk.types.let_annotation_mismatch",
1291 l.value.span,
1292 format!(
1293 "let binding's value has type `{}`, but the annotation declares `{}`",
1294 rhs.display(),
1295 annot.display()
1296 ),
1297 )
1298 .with_label(
1299 l.type_annot.as_ref().unwrap().span(),
1300 "declared type annotation",
1301 ),
1302 );
1303 }
1304 annot
1305 }
1306 (Some(annot), None) => annot,
1307 (None, Some(rhs)) => rhs,
1308 (None, None) => continue,
1309 };
1310 if l.name.name != "_" {
1311 if l.type_annot.is_none() {
1314 ctx.hints
1315 .record(l.name.span, format!(": {}", final_ty.display()));
1316 }
1317 ctx.locals.record(
1319 l.name.name.clone(),
1320 l.name.span,
1321 final_ty.display(),
1322 Span {
1323 start: l.span.end,
1324 end: block.span.end,
1325 },
1326 );
1327 ctx.bind(l.name.name.clone(), final_ty);
1328 }
1329 }
1330 Statement::EffectLet(l) => {
1331 if !ctx.effectful {
1332 ctx.errors.push(
1333 CompileError::new(
1334 "bynk.effect.bind_in_pure_context",
1335 l.span,
1336 "the `<-` operator can only be used inside an effectful body (one returning `Effect[T]`)",
1337 )
1338 .with_label(
1339 ctx.return_ty_span,
1340 format!("enclosing return type is `{}`", ctx.return_ty.display()),
1341 )
1342 .with_note(
1343 "change the enclosing function/handler's return type to `Effect[...]`, or use `let ... =` for a pure binding",
1344 ),
1345 );
1346 }
1347 let annot_ty = l.type_annot.as_ref().and_then(|a| {
1349 let r = resolve_type_ref_in(a, &ctx.input.types, &ctx.type_vars);
1352 if r.is_none() {
1353 ctx.errors.push(CompileError::new(
1354 "bynk.resolve.unknown_type",
1355 a.span(),
1356 "type in `let` annotation does not resolve",
1357 ));
1358 } else {
1359 record_type_refs(a, &ctx.input.types, &ctx.type_vars, ctx.refs);
1360 }
1361 r
1362 });
1363 let rhs_expected = annot_ty.as_ref().map(|t| Ty::Effect(Box::new(t.clone())));
1365 let rhs_ty = type_of(&l.value, rhs_expected.as_ref(), ctx);
1366 let inner_ty = match rhs_ty {
1367 Some(Ty::Effect(t)) => Some((*t).clone()),
1368 Some(other) => {
1369 ctx.errors.push(
1370 CompileError::new(
1371 "bynk.effect.bind_on_non_effect",
1372 l.value.span,
1373 format!(
1374 "the `<-` operator requires an `Effect[T]` value, but got `{}`",
1375 other.display()
1376 ),
1377 )
1378 .with_note(
1379 "use `let ... =` for a pure binding, or wrap the value with `Effect.pure(...)`",
1380 ),
1381 );
1382 None
1383 }
1384 None => None,
1385 };
1386 let final_ty = match (annot_ty, inner_ty) {
1387 (Some(annot), Some(rhs)) => {
1388 if !compatible(&rhs, &annot) {
1389 ctx.errors.push(CompileError::new(
1390 "bynk.types.let_annotation_mismatch",
1391 l.value.span,
1392 format!(
1393 "let-binding's value has type `Effect[{}]`, but the annotation declares `Effect[{}]`",
1394 rhs.display(),
1395 annot.display()
1396 ),
1397 ));
1398 }
1399 annot
1400 }
1401 (Some(annot), None) => annot,
1402 (None, Some(rhs)) => rhs,
1403 (None, None) => continue,
1404 };
1405 if l.name.name != "_" {
1406 if l.type_annot.is_none() {
1410 ctx.hints
1411 .record(l.name.span, format!(": {}", final_ty.display()));
1412 }
1413 ctx.locals.record(
1414 l.name.name.clone(),
1415 l.name.span,
1416 final_ty.display(),
1417 Span {
1418 start: l.span.end,
1419 end: block.span.end,
1420 },
1421 );
1422 ctx.bind(l.name.name.clone(), final_ty);
1423 }
1424 }
1425 Statement::Expect(a) => {
1426 if !ctx.in_test_body {
1427 ctx.errors.push(
1428 CompileError::new(
1429 "bynk.expect.outside_case",
1430 a.span,
1431 "`expect` is only valid inside a `case` body",
1432 )
1433 .with_note(
1434 "expectations verify predicates at test runtime; use them only inside `case \"...\" { ... }` blocks",
1435 ),
1436 );
1437 }
1438 let val_ty = type_of(&a.value, Some(&Ty::Base(BaseType::Bool)), ctx);
1439 if let Some(actual) = val_ty
1440 && !compatible(&actual, &Ty::Base(BaseType::Bool))
1441 {
1442 ctx.errors.push(CompileError::new(
1443 "bynk.expect.not_bool",
1444 a.value.span,
1445 format!(
1446 "`expect` predicate has type `{}`, but a `Bool` is required",
1447 actual.display(),
1448 ),
1449 ));
1450 }
1451 }
1452 Statement::Send(s) => {
1453 if !ctx.effectful {
1456 ctx.errors.push(
1457 CompileError::new(
1458 "bynk.send.in_pure_context",
1459 s.span,
1460 "the `~>` send can only be used inside an effectful body (one returning `Effect[T]`)",
1461 )
1462 .with_label(
1463 ctx.return_ty_span,
1464 format!("enclosing return type is `{}`", ctx.return_ty.display()),
1465 )
1466 .with_note(
1467 "change the enclosing function/handler's return type to `Effect[...]`",
1468 ),
1469 );
1470 }
1471 let expected = Ty::Effect(Box::new(Ty::Unit));
1476 let rhs_ty = type_of(&s.value, Some(&expected), ctx);
1477 match rhs_ty {
1478 Some(Ty::Effect(inner)) if matches!(*inner, Ty::Unit) => {}
1479 Some(Ty::Effect(inner)) => {
1480 ctx.errors.push(
1481 CompileError::new(
1482 "bynk.send.requires_unit",
1483 s.value.span,
1484 format!(
1485 "`~>` requires an `Effect[()]` reply, but this send returns `Effect[{}]` — its result would be silently dropped",
1486 inner.display()
1487 ),
1488 )
1489 .with_note(
1490 "a `~>` send never awaits a reply, so it is reserved for empty replies; to await and discard a real result, write `let _ <- ...` instead",
1491 ),
1492 );
1493 }
1494 Some(other) => {
1495 ctx.errors.push(
1496 CompileError::new(
1497 "bynk.send.non_effect",
1498 s.value.span,
1499 format!(
1500 "the `~>` send requires an `Effect[()]` value, but got `{}`",
1501 other.display()
1502 ),
1503 )
1504 .with_note("`~>` sends an effectful call; the target must be a call returning `Effect[()]`"),
1505 );
1506 }
1507 None => {}
1508 }
1509 }
1510 Statement::Assign(a) => {
1511 match ctx.store_cells.get(&a.target.name).cloned() {
1516 None => {
1517 ctx.errors.push(
1518 CompileError::new(
1519 "bynk.cell.invalid_target",
1520 a.target.span,
1521 format!(
1522 "`:=` writes a `Cell` store field, but `{}` is not one",
1523 a.target.name
1524 ),
1525 )
1526 .with_note(
1527 "the `:=` write form applies only to a `store <name>: Cell[T]` field",
1528 ),
1529 );
1530 type_of(&a.value, None, ctx);
1531 }
1532 Some(elem_ty) => {
1533 if expr_reads_ident(&a.value, &a.target.name) {
1537 ctx.errors.push(
1538 CompileError::new(
1539 "bynk.cell.self_reference",
1540 a.span,
1541 format!(
1542 "the `:=` right-hand side reads `{0}`, the cell being \
1543 written — this is a read-modify-write",
1544 a.target.name
1545 ),
1546 )
1547 .with_note(
1548 "use `<cell>.update(fn)` for a read-modify-write so the \
1549 dependency on the prior value is explicit",
1550 ),
1551 );
1552 }
1553 if let Some(vt) = type_of(&a.value, Some(&elem_ty), ctx)
1554 && !compatible(&vt, &elem_ty)
1555 {
1556 ctx.errors.push(CompileError::new(
1557 "bynk.types.type_mismatch",
1558 a.value.span,
1559 format!(
1560 "this `:=` writes `{}`, but the cell `{}` holds `{}`",
1561 vt.display(),
1562 a.target.name,
1563 elem_ty.display()
1564 ),
1565 ));
1566 }
1567 }
1568 }
1569 }
1570 }
1571 }
1572 let ty = type_of(&block.tail, expected, ctx);
1573 let ty = maybe_auto_lift(ty, expected);
1574 if let Some(ty) = &ty {
1575 ctx.expr_types.insert(block.span, ty.clone());
1576 }
1577 ctx.pop_scope();
1578 ty
1579}
1580
1581fn maybe_auto_lift(ty: Option<Ty>, expected: Option<&Ty>) -> Option<Ty> {
1586 if let Some(actual) = &ty
1587 && let Some(Ty::Effect(et)) = expected
1588 && !actual.is_effect()
1589 && compatible(actual, et)
1590 {
1591 return Some(Ty::Effect(Box::new(actual.clone())));
1592 }
1593 ty
1594}
1595
1596fn interpolable(ty: &Ty) -> bool {
1601 matches!(
1602 ty,
1603 Ty::Base(_)
1604 | Ty::Named {
1605 kind: NamedKind::Refined(_),
1606 ..
1607 }
1608 )
1609}
1610
1611pub fn type_of(expr: &Expr, expected: Option<&Ty>, ctx: &mut Ctx) -> Option<Ty> {
1612 let ty = match &expr.kind {
1613 ExprKind::Lambda(lambda) => check_lambda(lambda, expected, ctx),
1620 ExprKind::IntLit(_) => {
1621 admit_refined_literal(expr, expected, ctx).or(Some(Ty::Base(BaseType::Int)))
1622 }
1623 ExprKind::FloatLit { .. } => {
1624 admit_refined_literal(expr, expected, ctx).or(Some(Ty::Base(BaseType::Float)))
1625 }
1626 ExprKind::DurationLit { .. } => Some(Ty::Base(BaseType::Duration)),
1629 ExprKind::StrLit(_) => {
1630 admit_refined_literal(expr, expected, ctx).or(Some(Ty::Base(BaseType::String)))
1631 }
1632 ExprKind::InterpStr(parts) => {
1641 for part in parts {
1642 let InterpPart::Hole(hole) = part else {
1643 continue;
1644 };
1645 match type_of(hole, None, ctx) {
1646 Some(ty) if interpolable(&ty) => {}
1647 Some(other) => ctx.errors.push(
1648 CompileError::new(
1649 "bynk.types.interpolation_non_scalar",
1650 hole.span,
1651 format!("type `{}` has no string form here", other.display()),
1652 )
1653 .with_note(
1654 "interpolation holes accept the base scalar types (String, Int, Float, Bool) or a refinement of one; map other values to a String first",
1655 ),
1656 ),
1657 None => {}
1659 }
1660 }
1661 Some(Ty::Base(BaseType::String))
1662 }
1663 ExprKind::BoolLit(_) => Some(Ty::Base(BaseType::Bool)),
1664 ExprKind::ListLit(elems) => {
1668 let expected_elem = expected.and_then(peel_to_list);
1669 if elems.is_empty() {
1670 match expected_elem {
1671 Some(t) => Some(Ty::List(Box::new(t))),
1672 None => {
1673 ctx.errors.push(
1674 CompileError::new(
1675 "bynk.types.uninferable_element_type",
1676 expr.span,
1677 "an empty `[]` has no inferable element type",
1678 )
1679 .with_note(
1680 "annotate the binding (`let xs: List[T] = []`) or use the empty list where a `List[T]` is expected",
1681 ),
1682 );
1683 None
1684 }
1685 }
1686 } else {
1687 let mut elem_ty: Option<Ty> = expected_elem;
1688 for e in elems {
1689 let Some(t) = type_of(e, elem_ty.as_ref(), ctx) else {
1690 continue;
1691 };
1692 match &elem_ty {
1693 Some(et) => {
1694 if !compatible(&t, et) {
1695 ctx.errors.push(CompileError::new(
1696 "bynk.types.list_element_mismatch",
1697 e.span,
1698 format!(
1699 "list element has type `{}`, but the list's element type is `{}`",
1700 t.display(),
1701 et.display()
1702 ),
1703 ));
1704 }
1705 }
1706 None => elem_ty = Some(t),
1707 }
1708 }
1709 elem_ty.map(|t| Ty::List(Box::new(t)))
1710 }
1711 }
1712 ExprKind::Ident(id) => {
1713 if ctx.lookup(id.name.as_str()).is_none()
1718 && let Some((_, v)) = ctx.store_maps.get(&id.name).cloned()
1719 {
1720 Some(Ty::Query(Box::new(v)))
1721 }
1722 else if ctx.lookup(id.name.as_str()).is_none()
1728 && let Some(v) = http_variant(&id.name)
1729 {
1730 let user_owns = ctx.input.types.values().any(|t| {
1731 matches!(&t.body, TypeBody::Sum(s)
1732 if s.variants.iter().any(|var| var.name.name == id.name))
1733 });
1734 let http_implied = expected
1735 .map(|t| peel_to_http_result(t).is_some())
1736 .unwrap_or(false)
1737 || peel_to_http_result(&ctx.return_ty).is_some();
1738 if http_implied || !user_owns {
1739 check_http_variant(id.span, v, &[], expected, ctx)
1740 } else {
1741 check_ident(id, expected, ctx)
1742 }
1743 } else if ctx.lookup(id.name.as_str()).is_none()
1744 && let Some(qv) = queue_variant(&id.name)
1745 && (expected.is_some_and(peel_to_queue_result)
1746 || peel_to_queue_result(&ctx.return_ty))
1747 {
1748 check_queue_variant(id.span, qv, &[], ctx)
1750 } else {
1751 check_ident(id, expected, ctx)
1752 }
1753 }
1754 ExprKind::Paren(inner) => type_of(inner, expected, ctx),
1755 ExprKind::Call {
1756 name,
1757 type_args,
1758 args,
1759 } => {
1760 let user_owners: usize = ctx
1765 .input
1766 .types
1767 .values()
1768 .filter(|t| {
1769 matches!(&t.body, TypeBody::Sum(s)
1770 if s.variants.iter().any(|v| v.name.name == name.name))
1771 })
1772 .count();
1773 let http_implied = expected
1774 .map(|t| peel_to_http_result(t).is_some())
1775 .unwrap_or(false)
1776 || peel_to_http_result(&ctx.return_ty).is_some();
1777 let unowned = !ctx.input.fns.contains_key(&name.name) && user_owners == 0;
1778 if let Some(v) = http_variant(&name.name)
1779 && (http_implied || unowned)
1780 {
1781 check_http_variant(expr.span, v, args, expected, ctx)
1782 } else if let Some(qv) = queue_variant(&name.name)
1783 && (expected.is_some_and(peel_to_queue_result)
1784 || peel_to_queue_result(&ctx.return_ty))
1785 {
1786 check_queue_variant(expr.span, qv, args, ctx)
1788 } else {
1789 check_call(name, type_args, args, expr.span, ctx)
1790 }
1791 }
1792 ExprKind::UnaryOp(op, inner) => check_unary(*op, inner, expr.span, ctx),
1793 ExprKind::BinOp(op, lhs, rhs) => check_binop(*op, lhs, rhs, ctx),
1794 ExprKind::Block(b) => type_of_block(b, expected, ctx),
1795 ExprKind::If {
1796 cond,
1797 then_block,
1798 else_block,
1799 } => check_if(cond, then_block, else_block, expr.span, expected, ctx),
1800 ExprKind::Ok(inner) => check_ok(inner, expr.span, expected, ctx),
1801 ExprKind::Err(inner) => check_err(inner, expr.span, expected, ctx),
1802 ExprKind::Some(inner) => check_some(inner, expr.span, expected, ctx),
1803 ExprKind::None => check_none(expr.span, expected, ctx),
1804 ExprKind::Question(inner) => check_question(inner, expr.span, ctx),
1805 ExprKind::ConstructorCall {
1806 type_name,
1807 method,
1808 args,
1809 } => {
1810 if type_name.name == HTTP_RESULT {
1811 if let Some(v) = http_variant(&method.name) {
1812 check_http_variant(expr.span, v, args, expected, ctx)
1813 } else {
1814 ctx.errors.push(CompileError::new(
1815 "bynk.types.unknown_static_member",
1816 method.span,
1817 format!("`HttpResult` has no variant named `{}`", method.name),
1818 ));
1819 None
1820 }
1821 } else if type_name.name == QUEUE_RESULT {
1822 if let Some(qv) = queue_variant(&method.name) {
1823 check_queue_variant(expr.span, qv, args, ctx)
1824 } else {
1825 ctx.errors.push(CompileError::new(
1826 "bynk.types.unknown_static_member",
1827 method.span,
1828 format!("`QueueResult` has no variant named `{}`", method.name),
1829 ));
1830 None
1831 }
1832 } else {
1833 check_static_call(type_name, method, args, expr.span, ctx)
1834 }
1835 }
1836 ExprKind::RecordConstruction { type_name, fields } => {
1837 check_record_construction(type_name, fields, expr.span, ctx)
1838 }
1839 ExprKind::FieldAccess { receiver, field } => {
1840 if let ExprKind::Ident(id) = &receiver.kind
1842 && ctx.lookup(id.name.as_str()).is_none()
1843 && id.name == HTTP_RESULT
1844 {
1845 if let Some(v) = http_variant(&field.name) {
1846 if !matches!(v.payload, HttpVariantPayload::None) {
1847 ctx.errors.push(CompileError::new(
1848 "bynk.types.variant_missing_payload",
1849 field.span,
1850 format!(
1851 "`HttpResult.{}` has a payload — call it with an argument",
1852 v.name
1853 ),
1854 ));
1855 return None;
1856 }
1857 check_http_variant(field.span, v, &[], expected, ctx)
1858 } else {
1859 ctx.errors.push(CompileError::new(
1860 "bynk.types.unknown_static_member",
1861 field.span,
1862 format!("`HttpResult` has no variant named `{}`", field.name),
1863 ));
1864 None
1865 }
1866 } else {
1867 check_field_access(receiver, field, ctx)
1868 }
1869 }
1870 ExprKind::MethodCall {
1871 receiver,
1872 method,
1873 type_args,
1874 args,
1875 } => {
1876 if let ExprKind::Ident(id) = &receiver.kind
1880 && let Some((k, v)) = ctx.store_maps.get(&id.name).cloned()
1881 {
1882 if is_query_op(&method.name) {
1886 ctx.expr_types
1895 .insert(receiver.span, Ty::Query(Box::new(v.clone())));
1896 check_query_kernel_method(method, args, &v, expr.span, ctx)
1897 } else {
1898 check_store_map_op(method, args, &k, &v, expr.span, ctx)
1899 }
1900 }
1901 else if let ExprKind::Ident(id) = &receiver.kind
1904 && let Some(t) = ctx.store_sets.get(&id.name).cloned()
1905 {
1906 check_store_set_op(method, args, &t, expr.span, ctx)
1907 }
1908 else if let ExprKind::Ident(id) = &receiver.kind
1911 && let Some((k, v, _ttl)) = ctx.store_caches.get(&id.name).cloned()
1912 {
1913 check_store_cache_op(method, args, &k, &v, expr.span, ctx)
1914 }
1915 else if let ExprKind::Ident(id) = &receiver.kind
1920 && let Some(t) = ctx.store_logs.get(&id.name).cloned()
1921 {
1922 check_store_log_op(method, args, &t, expr.span, ctx)
1923 }
1924 else if let ExprKind::Ident(id) = &receiver.kind
1928 && let Some(t) = ctx.store_cells.get(&id.name).cloned()
1929 {
1930 check_store_cell_op(method, args, &t, expr.span, ctx)
1931 }
1932 else if let ExprKind::Ident(id) = &receiver.kind
1934 && ctx.lookup(id.name.as_str()).is_none()
1935 && id.name == HTTP_RESULT
1936 {
1937 if let Some(v) = http_variant(&method.name) {
1938 check_http_variant(expr.span, v, args, expected, ctx)
1939 } else {
1940 ctx.errors.push(CompileError::new(
1941 "bynk.types.unknown_static_member",
1942 method.span,
1943 format!("`HttpResult` has no variant named `{}`", method.name),
1944 ));
1945 None
1946 }
1947 } else {
1948 check_method_call(receiver, method, type_args, args, expr.span, expected, ctx)
1949 }
1950 }
1951 ExprKind::Match { discriminant, arms } => {
1952 check_match(discriminant, arms, expr.span, expected, ctx)
1953 }
1954 ExprKind::Is { value, pattern } => check_is(value, pattern, expr.span, ctx),
1955 ExprKind::UnitLit => Some(Ty::Unit),
1956 ExprKind::EffectPure(inner) => {
1957 let expected_inner = match expected {
1958 Some(Ty::Effect(t)) => Some((**t).clone()),
1959 _ => None,
1960 };
1961 let inner_ty = type_of(inner, expected_inner.as_ref(), ctx)?;
1962 Some(Ty::Effect(Box::new(inner_ty)))
1963 }
1964 ExprKind::RecordSpread {
1965 type_name,
1966 base,
1967 overrides,
1968 } => check_record_spread(
1969 type_name.as_ref(),
1970 base,
1971 overrides,
1972 expr.span,
1973 expected,
1974 ctx,
1975 ),
1976 ExprKind::Expect(inner) => check_expect(inner, expr.span, ctx),
1977 ExprKind::Val { type_ref, args } => check_val(type_ref, args, expr.span, ctx),
1978 };
1979 if let Some(ty) = &ty {
1980 ctx.expr_types.insert(expr.span, ty.clone());
1981 }
1982 ty
1983}
1984
1985fn peel_to_http_result(ty: &Ty) -> Option<Ty> {
1989 match ty {
1990 Ty::HttpResult(inner) => Some((**inner).clone()),
1991 Ty::Effect(inner) => peel_to_http_result(inner),
1992 _ => None,
1993 }
1994}
1995
1996fn peel_to_queue_result(ty: &Ty) -> bool {
1998 match ty {
1999 Ty::QueueResult => true,
2000 Ty::Effect(inner) => peel_to_queue_result(inner),
2001 _ => false,
2002 }
2003}
2004
2005fn surrounding_result(expected: Option<&Ty>, return_ty: &Ty) -> Option<(Ty, Ty)> {
2006 if let Some(t) = expected
2007 && let Some(pair) = peel_to_result(t)
2008 {
2009 return Some(pair);
2010 }
2011 peel_to_result(return_ty)
2012}
2013
2014fn peel_to_result(ty: &Ty) -> Option<(Ty, Ty)> {
2019 match ty {
2020 Ty::Result(t, e) => Some(((**t).clone(), (**e).clone())),
2021 Ty::Effect(inner) => peel_to_result(inner),
2022 _ => None,
2023 }
2024}
2025
2026fn peel_to_option(ty: &Ty) -> Option<Ty> {
2028 match ty {
2029 Ty::Option(t) => Some((**t).clone()),
2030 Ty::Effect(inner) => peel_to_option(inner),
2031 _ => None,
2032 }
2033}
2034
2035fn peel_to_list(ty: &Ty) -> Option<Ty> {
2039 match ty {
2040 Ty::List(t) => Some((**t).clone()),
2041 Ty::Effect(inner) => peel_to_list(inner),
2042 _ => None,
2043 }
2044}
2045
2046fn peel_to_map(ty: &Ty) -> Option<(Ty, Ty)> {
2048 match ty {
2049 Ty::Map(k, v) => Some(((**k).clone(), (**v).clone())),
2050 Ty::Effect(inner) => peel_to_map(inner),
2051 _ => None,
2052 }
2053}
2054
2055struct VariantInfo {
2059 name: String,
2060 payload: Vec<(String, Ty)>,
2061}
2062
2063fn rebrand_return_type(t: &Ty, caller_types: &HashMap<String, TypeDecl>) -> Ty {
2067 match t {
2068 Ty::Named { name, kind } => {
2069 if let Some(decl) = caller_types.get(name) {
2073 named_ty(decl)
2074 } else {
2075 Ty::Named {
2076 name: name.clone(),
2077 kind: kind.clone(),
2078 }
2079 }
2080 }
2081 Ty::Result(t, e) => Ty::Result(
2082 Box::new(rebrand_return_type(t, caller_types)),
2083 Box::new(rebrand_return_type(e, caller_types)),
2084 ),
2085 Ty::Option(t) => Ty::Option(Box::new(rebrand_return_type(t, caller_types))),
2086 Ty::Effect(t) => Ty::Effect(Box::new(rebrand_return_type(t, caller_types))),
2087 Ty::HttpResult(t) => Ty::HttpResult(Box::new(rebrand_return_type(t, caller_types))),
2088 Ty::List(t) => Ty::List(Box::new(rebrand_return_type(t, caller_types))),
2089 Ty::Query(t) => Ty::Query(Box::new(rebrand_return_type(t, caller_types))),
2090 Ty::Stream(t) => Ty::Stream(Box::new(rebrand_return_type(t, caller_types))),
2091 Ty::Connection(t) => Ty::Connection(Box::new(rebrand_return_type(t, caller_types))),
2092 Ty::Map(k, v) => Ty::Map(
2093 Box::new(rebrand_return_type(k, caller_types)),
2094 Box::new(rebrand_return_type(v, caller_types)),
2095 ),
2096 Ty::Base(_)
2097 | Ty::QueueResult
2098 | Ty::ValidationError
2099 | Ty::JsonError
2100 | Ty::Unit
2101 | Ty::Actor(_)
2102 | Ty::ActorSum(_) => t.clone(),
2103 Ty::Fn { .. } | Ty::Var(_) => t.clone(),
2107 }
2108}
2109
2110fn structurally_compatible(
2115 arg: &Ty,
2116 param: &Ty,
2117 arg_types: &HashMap<String, TypeDecl>,
2118 param_types: &HashMap<String, TypeDecl>,
2119) -> bool {
2120 structurally_compatible_inner(arg, param, arg_types, param_types, &mut HashSet::new())
2121}
2122
2123fn structurally_compatible_inner(
2124 arg: &Ty,
2125 param: &Ty,
2126 arg_types: &HashMap<String, TypeDecl>,
2127 param_types: &HashMap<String, TypeDecl>,
2128 visited: &mut HashSet<(String, String)>,
2129) -> bool {
2130 match (arg, param) {
2131 (Ty::Base(a), Ty::Base(b)) => a == b,
2132 (Ty::ValidationError, Ty::ValidationError) => true,
2133 (Ty::JsonError, Ty::JsonError) => true,
2134 (Ty::Unit, Ty::Unit) => true,
2135 (Ty::Result(t1, e1), Ty::Result(t2, e2)) => {
2136 structurally_compatible_inner(t1, t2, arg_types, param_types, visited)
2137 && structurally_compatible_inner(e1, e2, arg_types, param_types, visited)
2138 }
2139 (Ty::Option(a), Ty::Option(b)) => {
2140 structurally_compatible_inner(a, b, arg_types, param_types, visited)
2141 }
2142 (Ty::Effect(a), Ty::Effect(b)) => {
2143 structurally_compatible_inner(a, b, arg_types, param_types, visited)
2144 }
2145 (Ty::HttpResult(a), Ty::HttpResult(b)) => {
2146 structurally_compatible_inner(a, b, arg_types, param_types, visited)
2147 }
2148 (Ty::Named { name: an, .. }, Ty::Named { name: bn, .. }) => {
2149 let key = (an.clone(), bn.clone());
2152 if !visited.insert(key.clone()) {
2153 return true;
2154 }
2155 let ok = structural_compare_named(an, bn, arg_types, param_types, visited);
2156 visited.remove(&key);
2157 ok
2158 }
2159 (
2163 Ty::Named {
2164 kind: NamedKind::Refined(b),
2165 ..
2166 },
2167 Ty::Base(target),
2168 ) => b == target,
2169 _ => false,
2170 }
2171}
2172
2173fn structural_compare_named(
2174 arg_name: &str,
2175 param_name: &str,
2176 arg_types: &HashMap<String, TypeDecl>,
2177 param_types: &HashMap<String, TypeDecl>,
2178 visited: &mut HashSet<(String, String)>,
2179) -> bool {
2180 let Some(arg_decl) = arg_types.get(arg_name) else {
2183 return false;
2184 };
2185 let Some(param_decl) = param_types.get(param_name) else {
2186 return false;
2187 };
2188 match (&arg_decl.body, ¶m_decl.body) {
2189 (
2190 TypeBody::Refined {
2191 base: ab,
2192 refinement: ar,
2193 ..
2194 },
2195 TypeBody::Refined {
2196 base: bb,
2197 refinement: br,
2198 ..
2199 },
2200 ) => {
2201 if ab != bb {
2202 return false;
2203 }
2204 refinements_match(ar.as_ref(), br.as_ref())
2205 }
2206 (
2207 TypeBody::Opaque {
2208 base: ab,
2209 refinement: ar,
2210 ..
2211 },
2212 TypeBody::Opaque {
2213 base: bb,
2214 refinement: br,
2215 ..
2216 },
2217 ) => {
2218 if arg_name != param_name {
2221 return false;
2222 }
2223 if ab != bb {
2224 return false;
2225 }
2226 refinements_match(ar.as_ref(), br.as_ref())
2227 }
2228 (TypeBody::Record(a), TypeBody::Record(b)) => {
2229 if a.fields.len() != b.fields.len() {
2230 return false;
2231 }
2232 for af in &a.fields {
2233 let Some(bf) = b.fields.iter().find(|f| f.name.name == af.name.name) else {
2234 return false;
2235 };
2236 let at = resolve_type_ref(&af.type_ref, arg_types);
2237 let bt = resolve_type_ref(&bf.type_ref, param_types);
2238 let (Some(at), Some(bt)) = (at, bt) else {
2239 return false;
2240 };
2241 if !structurally_compatible_inner(&at, &bt, arg_types, param_types, visited) {
2242 return false;
2243 }
2244 }
2245 true
2246 }
2247 (TypeBody::Sum(a), TypeBody::Sum(b)) => {
2248 if a.variants.len() != b.variants.len() {
2249 return false;
2250 }
2251 for av in &a.variants {
2252 let Some(bv) = b.variants.iter().find(|v| v.name.name == av.name.name) else {
2253 return false;
2254 };
2255 if av.payload.len() != bv.payload.len() {
2256 return false;
2257 }
2258 for (af, bf) in av.payload.iter().zip(bv.payload.iter()) {
2259 if af.name.name != bf.name.name {
2260 return false;
2261 }
2262 let at = resolve_type_ref(&af.type_ref, arg_types);
2263 let bt = resolve_type_ref(&bf.type_ref, param_types);
2264 let (Some(at), Some(bt)) = (at, bt) else {
2265 return false;
2266 };
2267 if !structurally_compatible_inner(&at, &bt, arg_types, param_types, visited) {
2268 return false;
2269 }
2270 }
2271 }
2272 true
2273 }
2274 _ => false,
2275 }
2276}
2277
2278fn refinements_match(a: Option<&Refinement>, b: Option<&Refinement>) -> bool {
2279 match (a, b) {
2280 (None, None) => true,
2281 (Some(_), None) => true, (None, Some(_)) => false,
2283 (Some(a), Some(b)) => {
2284 if a.predicates.len() != b.predicates.len() {
2285 return false;
2286 }
2287 for (pa, pb) in a.predicates.iter().zip(b.predicates.iter()) {
2291 if !predicate_eq(&pa.kind, &pb.kind) {
2292 return false;
2293 }
2294 }
2295 true
2296 }
2297 }
2298}
2299
2300fn predicate_eq(a: &PredKind, b: &PredKind) -> bool {
2301 match (a, b) {
2302 (PredKind::Matches(x), PredKind::Matches(y)) => x == y,
2303 (PredKind::InRange(a1, a2), PredKind::InRange(b1, b2)) => {
2304 a1.value == b1.value && a2.value == b2.value
2305 }
2306 (PredKind::InRangeF(a1, a2), PredKind::InRangeF(b1, b2)) => {
2307 a1.value == b1.value && a2.value == b2.value
2308 }
2309 (PredKind::MinLength(a), PredKind::MinLength(b)) => a == b,
2310 (PredKind::MaxLength(a), PredKind::MaxLength(b)) => a == b,
2311 (PredKind::Length(a), PredKind::Length(b)) => a == b,
2312 (PredKind::NonNegative, PredKind::NonNegative) => true,
2313 (PredKind::Positive, PredKind::Positive) => true,
2314 (PredKind::NonEmpty, PredKind::NonEmpty) => true,
2315 _ => false,
2316 }
2317}
2318
2319fn variants_of(ty: &Ty, types: &HashMap<String, TypeDecl>) -> Option<Vec<VariantInfo>> {
2320 match ty {
2321 Ty::Named {
2322 kind: NamedKind::Sum,
2323 name,
2324 } => {
2325 let decl = types.get(name)?;
2326 if let TypeBody::Sum(s) = &decl.body {
2327 Some(
2328 s.variants
2329 .iter()
2330 .map(|v| VariantInfo {
2331 name: v.name.name.clone(),
2332 payload: v
2333 .payload
2334 .iter()
2335 .map(|f| {
2336 let t = resolve_type_ref(&f.type_ref, types)
2337 .unwrap_or(Ty::Base(BaseType::Int));
2338 (f.name.name.clone(), t)
2339 })
2340 .collect(),
2341 })
2342 .collect(),
2343 )
2344 } else {
2345 None
2346 }
2347 }
2348 Ty::Result(t, e) => Some(vec![
2349 VariantInfo {
2350 name: "Ok".to_string(),
2351 payload: vec![("value".to_string(), (**t).clone())],
2352 },
2353 VariantInfo {
2354 name: "Err".to_string(),
2355 payload: vec![("error".to_string(), (**e).clone())],
2356 },
2357 ]),
2358 Ty::Option(t) => Some(vec![
2359 VariantInfo {
2360 name: "Some".to_string(),
2361 payload: vec![("value".to_string(), (**t).clone())],
2362 },
2363 VariantInfo {
2364 name: "None".to_string(),
2365 payload: vec![],
2366 },
2367 ]),
2368 Ty::ActorSum(members) => Some(
2374 members
2375 .iter()
2376 .map(|(name, id)| VariantInfo {
2377 name: name.clone(),
2378 payload: match id {
2379 Ty::Unit => vec![],
2380 _ => vec![("identity".to_string(), id.clone())],
2381 },
2382 })
2383 .collect(),
2384 ),
2385 Ty::HttpResult(t) => Some(
2386 HTTP_VARIANTS
2387 .iter()
2388 .map(|v| VariantInfo {
2389 name: v.name.to_string(),
2390 payload: match v.payload {
2391 HttpVariantPayload::None => vec![],
2392 HttpVariantPayload::Value => vec![("value".to_string(), (**t).clone())],
2393 HttpVariantPayload::Message => {
2394 vec![("message".to_string(), Ty::Base(BaseType::String))]
2395 }
2396 HttpVariantPayload::Location => {
2397 vec![("location".to_string(), Ty::Base(BaseType::String))]
2398 }
2399 HttpVariantPayload::Streamed => vec![(
2400 "stream".to_string(),
2401 Ty::Stream(Box::new(Ty::Base(BaseType::String))),
2402 )],
2403 HttpVariantPayload::Raw => vec![
2409 ("body".to_string(), Ty::Base(BaseType::Bytes)),
2410 ("contentType".to_string(), Ty::Base(BaseType::String)),
2411 ],
2412 },
2413 })
2414 .collect(),
2415 ),
2416 _ => None,
2417 }
2418}
2419
2420#[cfg(test)]
2431mod generics_tests {
2432 use super::*;
2433
2434 fn var(n: &str) -> Ty {
2435 Ty::Var(n.to_string())
2436 }
2437 fn int() -> Ty {
2438 Ty::Base(BaseType::Int)
2439 }
2440
2441 #[test]
2442 fn unify_binds_and_holds() {
2443 let mut s = HashMap::new();
2444 assert!(unify(&var("A"), &int(), &mut s));
2445 assert_eq!(s.get("A"), Some(&int()));
2446 assert!(unify(&var("A"), &int(), &mut s));
2448 assert!(!unify(&var("A"), &Ty::Base(BaseType::String), &mut s));
2449 }
2450
2451 #[test]
2452 fn unify_walks_structure() {
2453 let mut s = HashMap::new();
2454 let pattern = Ty::Fn {
2455 params: vec![var("A")],
2456 ret: Box::new(Ty::Effect(Box::new(var("B")))),
2457 };
2458 let actual = Ty::Fn {
2459 params: vec![int()],
2460 ret: Box::new(Ty::Effect(Box::new(Ty::Base(BaseType::String)))),
2461 };
2462 assert!(unify(&pattern, &actual, &mut s));
2463 assert_eq!(s.get("A"), Some(&int()));
2464 assert_eq!(s.get("B"), Some(&Ty::Base(BaseType::String)));
2465 }
2466
2467 #[test]
2468 fn substitute_grounds_fully() {
2469 let mut s = HashMap::new();
2470 s.insert("A".to_string(), int());
2471 let t = Ty::Option(Box::new(Ty::Fn {
2472 params: vec![var("A")],
2473 ret: Box::new(var("A")),
2474 }));
2475 let g = substitute(&t, &s);
2476 assert!(!contains_var(&g));
2477 }
2478
2479 #[test]
2484 fn var_bearing_expected_is_benign() {
2485 assert!(!compatible(&int(), &var("A")));
2486 assert!(!compatible(&var("A"), &int()));
2487 assert!(compatible(&var("A"), &var("A")));
2489 assert!(!compatible(&var("A"), &var("B")));
2490 }
2491}
2492
2493#[cfg(test)]
2498mod pure_helper_pins {
2499 use super::*;
2500 use bynk_syntax::ast::{FloatBound, RefinementPred};
2501
2502 fn sp() -> Span {
2505 Span::new(0, 0)
2506 }
2507 fn ident(n: &str) -> Ident {
2508 Ident {
2509 name: n.to_string(),
2510 span: sp(),
2511 }
2512 }
2513 fn var(n: &str) -> Ty {
2514 Ty::Var(n.to_string())
2515 }
2516 fn int() -> Ty {
2517 Ty::Base(BaseType::Int)
2518 }
2519 fn string() -> Ty {
2520 Ty::Base(BaseType::String)
2521 }
2522 fn expr(kind: ExprKind) -> Expr {
2523 Expr { kind, span: sp() }
2524 }
2525 fn pred(kind: PredKind) -> RefinementPred {
2526 RefinementPred { kind, span: sp() }
2527 }
2528 fn refinement(preds: Vec<PredKind>) -> Refinement {
2529 Refinement {
2530 predicates: preds.into_iter().map(pred).collect(),
2531 span: sp(),
2532 }
2533 }
2534 fn fbound(value: f64) -> FloatBound {
2535 FloatBound {
2536 value,
2537 lexeme: value.to_string(),
2538 span: Span::new(0, 0),
2539 }
2540 }
2541 fn ibound(value: i64) -> IntBound {
2542 IntBound {
2543 value,
2544 span: Span::new(0, 0),
2545 }
2546 }
2547 fn in_range(lo: i64, hi: i64) -> PredKind {
2549 PredKind::InRange(ibound(lo), ibound(hi))
2550 }
2551 fn refined_decl(name: &str, base: BaseType, refinement: Option<Refinement>) -> TypeDecl {
2552 TypeDecl {
2553 name: ident(name),
2554 body: TypeBody::Refined {
2555 base,
2556 base_span: sp(),
2557 refinement,
2558 },
2559 documentation: None,
2560 span: sp(),
2561 trivia: bynk_syntax::ast::Trivia::default(),
2562 }
2563 }
2564 fn record_decl(name: &str) -> TypeDecl {
2565 TypeDecl {
2566 name: ident(name),
2567 body: TypeBody::Record(bynk_syntax::ast::RecordBody {
2568 fields: vec![],
2569 span: sp(),
2570 }),
2571 documentation: None,
2572 span: sp(),
2573 trivia: bynk_syntax::ast::Trivia::default(),
2574 }
2575 }
2576
2577 #[test]
2580 fn unify_identical_concrete_types() {
2581 let mut s = HashMap::new();
2582 assert!(unify(&int(), &int(), &mut s));
2583 assert!(s.is_empty());
2584 }
2585
2586 #[test]
2587 fn unify_var_binds_in_subst() {
2588 let mut s = HashMap::new();
2589 assert!(unify(&var("A"), &string(), &mut s));
2590 assert_eq!(s.get("A"), Some(&string()));
2591 }
2592
2593 #[test]
2594 fn unify_nested_generic_binds() {
2595 let mut s = HashMap::new();
2597 let pat = Ty::List(Box::new(var("A")));
2598 let act = Ty::List(Box::new(int()));
2599 assert!(unify(&pat, &act, &mut s));
2600 assert_eq!(s.get("A"), Some(&int()));
2601 }
2602
2603 #[test]
2604 fn unify_surprise_concrete_mismatch_returns_true() {
2605 let mut s = HashMap::new();
2610 assert!(unify(&int(), &string(), &mut s));
2611 assert!(unify(
2612 &Ty::List(Box::new(int())),
2613 &Ty::Option(Box::new(int())),
2614 &mut s,
2615 ));
2616 let mut s2 = HashMap::new();
2618 assert!(unify(&var("A"), &int(), &mut s2));
2619 assert!(!unify(&var("A"), &string(), &mut s2));
2620 let mut s3 = HashMap::new();
2621 let f1 = Ty::Fn {
2622 params: vec![int()],
2623 ret: Box::new(int()),
2624 };
2625 let f2 = Ty::Fn {
2626 params: vec![int(), int()],
2627 ret: Box::new(int()),
2628 };
2629 assert!(!unify(&f1, &f2, &mut s3));
2630 }
2631
2632 #[test]
2635 fn substitute_replaces_bound_var() {
2636 let mut s = HashMap::new();
2637 s.insert("A".to_string(), int());
2638 assert_eq!(substitute(&var("A"), &s), int());
2639 }
2640
2641 #[test]
2642 fn substitute_recurses_into_nested() {
2643 let mut s = HashMap::new();
2644 s.insert("A".to_string(), string());
2645 let t = Ty::Map(Box::new(var("A")), Box::new(int()));
2646 assert_eq!(
2647 substitute(&t, &s),
2648 Ty::Map(Box::new(string()), Box::new(int())),
2649 );
2650 }
2651
2652 #[test]
2653 fn substitute_leaves_unbound_var_alone() {
2654 let s = HashMap::new();
2655 assert_eq!(substitute(&var("Z"), &s), var("Z"));
2656 }
2657
2658 #[test]
2661 fn contains_var_positive_and_negative() {
2662 assert!(contains_var(&Ty::Option(Box::new(var("A")))));
2663 assert!(!contains_var(&Ty::Option(Box::new(int()))));
2664 assert!(!contains_var(&int()));
2665 }
2666
2667 #[test]
2668 fn contains_flexible_var_respects_rigid_set() {
2669 let mut rigid = HashSet::new();
2670 rigid.insert("A".to_string());
2671 assert!(!contains_flexible_var(&var("A"), &rigid));
2673 assert!(contains_flexible_var(&var("B"), &rigid));
2675 assert!(!contains_flexible_var(&int(), &rigid));
2677 }
2678
2679 #[test]
2682 fn peel_to_result_matches_and_misses() {
2683 let r = Ty::Result(Box::new(int()), Box::new(string()));
2684 assert_eq!(peel_to_result(&r), Some((int(), string())));
2685 assert_eq!(peel_to_result(&int()), None);
2686 assert_eq!(
2688 peel_to_result(&Ty::Effect(Box::new(r))),
2689 Some((int(), string()))
2690 );
2691 }
2692
2693 #[test]
2694 fn peel_to_option_matches_and_misses() {
2695 assert_eq!(peel_to_option(&Ty::Option(Box::new(int()))), Some(int()));
2696 assert_eq!(peel_to_option(&int()), None);
2697 }
2698
2699 #[test]
2700 fn peel_to_list_matches_and_misses() {
2701 assert_eq!(peel_to_list(&Ty::List(Box::new(string()))), Some(string()));
2702 assert_eq!(peel_to_list(&int()), None);
2703 }
2704
2705 #[test]
2706 fn peel_to_map_matches_and_misses() {
2707 let m = Ty::Map(Box::new(string()), Box::new(int()));
2708 assert_eq!(peel_to_map(&m), Some((string(), int())));
2709 assert_eq!(peel_to_map(&int()), None);
2710 }
2711
2712 #[test]
2713 fn peel_to_http_result_matches_and_misses() {
2714 assert_eq!(
2715 peel_to_http_result(&Ty::HttpResult(Box::new(int()))),
2716 Some(int()),
2717 );
2718 assert_eq!(peel_to_http_result(&int()), None);
2719 }
2720
2721 #[test]
2724 fn maybe_auto_lift_lifts_into_expected_effect() {
2725 let expected = Ty::Effect(Box::new(int()));
2727 let lifted = maybe_auto_lift(Some(int()), Some(&expected));
2728 assert_eq!(lifted, Some(Ty::Effect(Box::new(int()))));
2729 }
2730
2731 #[test]
2732 fn maybe_auto_lift_leaves_non_matching_alone() {
2733 let expected = Ty::Effect(Box::new(int()));
2735 assert_eq!(
2736 maybe_auto_lift(Some(Ty::Effect(Box::new(int()))), Some(&expected)),
2737 Some(Ty::Effect(Box::new(int()))),
2738 );
2739 assert_eq!(maybe_auto_lift(Some(int()), Some(&int())), Some(int()));
2741 assert_eq!(maybe_auto_lift(None, Some(&expected)), None);
2743 }
2744
2745 #[test]
2748 fn const_literal_extracts_literals() {
2749 assert!(matches!(
2750 const_literal(&expr(ExprKind::IntLit(7))),
2751 Some(ConstLit::Int(7)),
2752 ));
2753 assert!(matches!(
2754 const_literal(&expr(ExprKind::BoolLit(true))),
2755 Some(ConstLit::Bool(true)),
2756 ));
2757 assert!(matches!(
2758 const_literal(&expr(ExprKind::StrLit("hi".into()))),
2759 Some(ConstLit::Str(s)) if s == "hi",
2760 ));
2761 assert!(matches!(
2762 const_literal(&expr(ExprKind::FloatLit {
2763 value: 1.5,
2764 lexeme: "1.5".into(),
2765 })),
2766 Some(ConstLit::Float(_)),
2767 ));
2768 let neg = expr(ExprKind::UnaryOp(
2770 UnaryOp::Neg,
2771 Box::new(expr(ExprKind::IntLit(3))),
2772 ));
2773 assert!(matches!(const_literal(&neg), Some(ConstLit::Int(-3))));
2774 }
2775
2776 #[test]
2777 fn const_literal_rejects_non_literals() {
2778 assert!(const_literal(&expr(ExprKind::Ident(ident("x")))).is_none());
2779 }
2780
2781 #[test]
2784 fn eval_predicate_int_and_float() {
2785 assert!(eval_predicate(&PredKind::NonNegative, &ConstLit::Int(0)));
2786 assert!(!eval_predicate(&PredKind::NonNegative, &ConstLit::Int(-1)));
2787 assert!(eval_predicate(&PredKind::Positive, &ConstLit::Int(1)));
2788 assert!(!eval_predicate(&PredKind::Positive, &ConstLit::Int(0)));
2789 assert!(eval_predicate(&in_range(1, 10), &ConstLit::Int(5),));
2790 assert!(!eval_predicate(&in_range(1, 10), &ConstLit::Int(11),));
2791 }
2792
2793 #[test]
2794 fn eval_predicate_string() {
2795 assert!(eval_predicate(
2796 &PredKind::MinLength(2),
2797 &ConstLit::Str("ab".into()),
2798 ));
2799 assert!(!eval_predicate(
2800 &PredKind::MinLength(3),
2801 &ConstLit::Str("ab".into()),
2802 ));
2803 assert!(eval_predicate(
2804 &PredKind::NonEmpty,
2805 &ConstLit::Str("x".into()),
2806 ));
2807 assert!(!eval_predicate(
2808 &PredKind::NonEmpty,
2809 &ConstLit::Str(String::new()),
2810 ));
2811 assert!(eval_predicate(
2812 &PredKind::Matches("[a-z]+".into()),
2813 &ConstLit::Str("abc".into()),
2814 ));
2815 assert!(!eval_predicate(
2816 &PredKind::Matches("[a-z]+".into()),
2817 &ConstLit::Str("ABC".into()),
2818 ));
2819 }
2820
2821 #[test]
2822 fn eval_predicate_base_mismatch_is_vacuously_true() {
2823 assert!(eval_predicate(&PredKind::MinLength(5), &ConstLit::Int(0),));
2827 }
2828
2829 #[test]
2832 fn literal_matches_base_pairs() {
2833 assert!(literal_matches_base(&ConstLit::Int(1), BaseType::Int));
2834 assert!(literal_matches_base(
2835 &ConstLit::Str("x".into()),
2836 BaseType::String,
2837 ));
2838 assert!(!literal_matches_base(&ConstLit::Int(1), BaseType::String));
2839 assert!(!literal_matches_base(&ConstLit::Unit, BaseType::Int));
2840 }
2841
2842 #[test]
2845 fn type_decl_base_refined_vs_record() {
2846 let refined = refined_decl("Age", BaseType::Int, None);
2847 assert_eq!(type_decl_base(&refined), Some(BaseType::Int));
2848 assert_eq!(type_decl_base(&record_decl("Pt")), None);
2849 }
2850
2851 #[test]
2852 fn type_decl_refinement_present_vs_absent() {
2853 let with = refined_decl(
2854 "Age",
2855 BaseType::Int,
2856 Some(refinement(vec![PredKind::Positive])),
2857 );
2858 assert!(type_decl_refinement(&with).is_some());
2859 let without = refined_decl("Raw", BaseType::Int, None);
2860 assert!(type_decl_refinement(&without).is_none());
2861 assert!(type_decl_refinement(&record_decl("Pt")).is_none());
2862 }
2863
2864 #[test]
2867 fn int_refinement_consistency() {
2868 let mut errs = vec![];
2870 check_int_refinement_consistency(
2871 &refinement(vec![PredKind::Positive, in_range(1, 10)]),
2872 &mut errs,
2873 );
2874 assert!(errs.is_empty());
2875 let mut errs = vec![];
2877 check_int_refinement_consistency(&refinement(vec![in_range(10, 1)]), &mut errs);
2878 assert_eq!(errs.len(), 1);
2879 assert_eq!(errs[0].category, "bynk.types.empty_refinement");
2880 }
2881
2882 #[test]
2883 fn float_refinement_consistency() {
2884 let mut errs = vec![];
2886 check_float_refinement_consistency(
2887 &refinement(vec![PredKind::InRangeF(fbound(0.0), fbound(1.0))]),
2888 &mut errs,
2889 );
2890 assert!(errs.is_empty());
2891 let mut errs = vec![];
2893 check_float_refinement_consistency(
2894 &refinement(vec![PredKind::InRangeF(fbound(5.0), fbound(1.0))]),
2895 &mut errs,
2896 );
2897 assert_eq!(errs.len(), 1);
2898 assert_eq!(errs[0].category, "bynk.types.empty_refinement");
2899 let mut errs = vec![];
2902 check_float_refinement_consistency(
2903 &refinement(vec![
2904 PredKind::Positive,
2905 PredKind::InRangeF(fbound(0.0), fbound(0.0)),
2906 ]),
2907 &mut errs,
2908 );
2909 assert_eq!(errs.len(), 1);
2910 }
2911
2912 #[test]
2913 fn string_refinement_consistency() {
2914 let mut errs = vec![];
2916 check_string_refinement_consistency(
2917 &refinement(vec![PredKind::MinLength(1), PredKind::MaxLength(10)]),
2918 &mut errs,
2919 );
2920 assert!(errs.is_empty());
2921 let mut errs = vec![];
2923 check_string_refinement_consistency(
2924 &refinement(vec![PredKind::MinLength(10), PredKind::MaxLength(2)]),
2925 &mut errs,
2926 );
2927 assert_eq!(errs.len(), 1);
2928 assert_eq!(errs[0].category, "bynk.types.empty_refinement");
2929 let mut errs = vec![];
2934 check_string_refinement_consistency(
2935 &refinement(vec![PredKind::Length(3), PredKind::Length(5)]),
2936 &mut errs,
2937 );
2938 assert_eq!(errs.len(), 2);
2939 assert!(
2940 errs.iter()
2941 .all(|e| e.category == "bynk.types.empty_refinement")
2942 );
2943 }
2944
2945 #[test]
2948 fn numeric_mix_int_float_pairs() {
2949 assert!(numeric_mix(Some(BaseType::Int), Some(BaseType::Float)));
2950 assert!(numeric_mix(Some(BaseType::Float), Some(BaseType::Int)));
2951 assert!(!numeric_mix(Some(BaseType::Int), Some(BaseType::Int)));
2952 assert!(!numeric_mix(Some(BaseType::Float), Some(BaseType::Float)));
2953 assert!(!numeric_mix(None, Some(BaseType::Int)));
2954 }
2955}