1use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38
39use bynk_check::checker::{self, Callee, CheckedProgram, NamedKind, Ty, TyId, TypedCommons, Types};
40use bynk_check::resolver::MethodTable;
41use bynk_syntax::ast::{
42 ActorDecl, AgentDecl, BaseType, BinOp, Block, CapRef, CapabilityDecl, CapabilityOp,
43 CommonsItem, EventPattern, EventPatternValue, Expr, ExprId, ExprKind, FieldInit, FnDecl,
44 FnName, Handler, HandlerKind, HttpMethod, InterpPart, Invariant, LambdaExpr, LiteralValue,
45 MatchArm, MatchBody, Pattern, PatternBindingKind, ProviderDecl, ProviderOp, QualifiedName,
46 ServiceDecl, ServiceProtocol, Statement, StoreField, Transition, TypeBody, TypeDecl, TypeRef,
47 UnaryOp, expr_children,
48};
49use bynk_syntax::span::Span;
50
51use bynk_ir::{
52 ActorBinder, ActorSeamIr, BindingMode, CacheIr, CapRefIr, CommitShape, ConnectionBinder,
53 ConstVal, CorsIr, EventPatternIr, EventPatternValueIr, EventSubscriberShape, Exhaustive, FnSig,
54 GlobalRef, IndexIr, IrArm, IrBinOp, IrExpr, IrExprKind, IrHandler, IrHandlerKind, IrHttpMethod,
55 IrInterpPart, IrItem, IrPat, IrPredicate, IrStmt, MUTATING_CELL_OPS, MUTATING_LOG_OPS,
56 MUTATING_MAP_CACHE_OPS, MUTATING_SET_OPS, MatchForm, OpSig, PolicyIr, ProtocolIr, ProviderBody,
57 ProviderOpIr, SecurityIr, StoreFieldIr, StoreKindIr, TypeShape, block_uses_emit,
58 match_needs_if_chain,
59};
60
61pub struct LowerIrCtx<'a> {
75 program: &'a TypedCommons,
76 scopes: Vec<HashMap<String, TyId>>,
77 type_vars: HashSet<String>,
78 tmp_counter: usize,
91 return_ty: Option<TyId>,
114 store_queryable: HashSet<String>,
125}
126
127impl<'a> LowerIrCtx<'a> {
128 fn new(program: &'a CheckedProgram, type_vars: HashSet<String>) -> Self {
129 Self::from_commons(program.program(), type_vars)
130 }
131
132 fn from_commons(commons: &'a TypedCommons, type_vars: HashSet<String>) -> Self {
137 Self {
138 program: commons,
139 scopes: vec![HashMap::new()],
140 type_vars,
141 tmp_counter: 0,
142 return_ty: None,
143 store_queryable: HashSet::new(),
144 }
145 }
146
147 fn set_return_ty(&mut self, ty: Option<TyId>) {
154 self.return_ty = ty;
155 }
156
157 fn set_store_queryable(&mut self, names: HashSet<String>) {
162 self.store_queryable = names;
163 }
164
165 fn fresh_tmp(&mut self, prefix: &str) -> String {
169 let n = self.tmp_counter;
170 self.tmp_counter += 1;
171 format!("{prefix}_{n}")
172 }
173
174 fn fresh_spread_tmp(&mut self) -> String {
175 self.fresh_tmp("__spread_base")
176 }
177
178 fn resolve_type_ref(&self, r: &bynk_syntax::ast::TypeRef) -> Option<TyId> {
184 checker::resolve_type_ref_in(
185 r,
186 &self.program.types,
187 &self.type_vars,
188 &self.program.ty_intern,
189 )
190 }
191
192 fn push_scope(&mut self) {
193 self.scopes.push(HashMap::new());
194 }
195
196 fn pop_scope(&mut self) {
197 self.scopes.pop();
198 }
199
200 fn bind(&mut self, name: String, ty: TyId) {
201 self.scopes
202 .last_mut()
203 .expect("bynk internal error: LowerIrCtx's scope stack is never empty")
204 .insert(name, ty);
205 }
206
207 fn lookup(&self, name: &str) -> Option<TyId> {
208 self.scopes.iter().rev().find_map(|s| s.get(name).copied())
209 }
210
211 fn expr_ty(&self, id: ExprId) -> TyId {
217 self.program
218 .expr_types
219 .get(&id)
220 .unwrap_or_else(|| {
221 panic!(
222 "bynk internal error (ADR 0334): no recorded type for {id:?} — \
223 bynk_lower and bynk-check disagree about which \
224 expressions this certified unit contains"
225 )
226 })
227 .ty
228 }
229
230 fn peel_effect(&self, ty: TyId) -> TyId {
237 match &*self.program.ty_intern.get(ty) {
238 Ty::Effect(inner) => *inner,
239 _ => ty,
240 }
241 }
242
243 fn unit_ty(&self) -> TyId {
244 self.program.ty_intern.intern(Ty::Unit)
245 }
246
247 fn callee(&self, id: ExprId) -> Option<&Callee> {
259 self.program.callees.get(&id)
260 }
261}
262
263fn fn_rigid_type_vars(f: &FnDecl, program: &TypedCommons) -> HashSet<String> {
274 let mut type_vars: HashSet<String> = f
275 .type_params
276 .iter()
277 .map(|tp| tp.name.name.clone())
278 .collect();
279 if let FnName::Method { type_name, .. } = &f.name
280 && let Some(decl) = program.types.get(&type_name.name)
281 {
282 type_vars.extend(decl.type_params.iter().map(|tp| tp.name.name.clone()));
283 }
284 type_vars
285}
286
287fn fn_receiver_ty(f: &FnDecl, program: &TypedCommons) -> Option<TyId> {
295 let FnName::Method { type_name, .. } = &f.name else {
296 return None;
297 };
298 if !f.has_self {
299 return None;
300 }
301 let decl = program.types.get(&type_name.name)?;
302 let self_args = decl
303 .type_params
304 .iter()
305 .map(|tp| program.ty_intern.intern(Ty::Var(tp.name.name.clone())))
306 .collect();
307 Some(checker::named_ty_with_args(
308 decl,
309 self_args,
310 &program.ty_intern,
311 ))
312}
313
314fn wrap_body_return(block: IrExpr) -> IrExpr {
322 let IrExpr {
323 kind: IrExprKind::Block { stmts, tail },
324 ty,
325 span,
326 } = block
327 else {
328 unreachable!("lower_block_ir always returns IrExprKind::Block");
329 };
330 let tail_ty = tail.ty;
331 let tail_span = tail.span;
332 IrExpr {
333 kind: IrExprKind::Block {
334 stmts,
335 tail: Box::new(IrExpr {
336 kind: IrExprKind::Return { value: tail },
337 ty: tail_ty,
338 span: tail_span,
339 }),
340 },
341 ty,
342 span,
343 }
344}
345
346pub fn lower_fn_body_ir(f: &FnDecl, program: &CheckedProgram) -> IrExpr {
371 let type_vars = fn_rigid_type_vars(f, program.program());
372 let mut cx = LowerIrCtx::new(program, type_vars);
373 cx.set_return_ty(cx.resolve_type_ref(&f.return_type));
374 if let Some(self_ty) = fn_receiver_ty(f, program.program()) {
379 cx.bind("self".to_string(), self_ty);
380 }
381 for p in &f.params {
382 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
383 panic!(
384 "bynk internal error (ADR 0334): parameter `{}`'s type does not resolve in this \
385 pass's own rigid-variable scope, but the checker already accepted this fn body — \
386 bynk_lower's type_vars disagrees with bynk-check's Ctx::type_vars",
387 p.name.name
388 )
389 });
390 cx.bind(p.name.name.clone(), ty);
391 }
392 let block = lower_block_ir(&f.body, &mut cx);
393 wrap_body_return(block)
394}
395
396fn lower_handler_body_ir(
433 h: &Handler,
434 store_cells: &HashMap<String, TyId>,
435 store_queryable: &HashSet<String>,
436 state_ty: TyId,
437 program: &CheckedProgram,
438) -> IrExpr {
439 let mut cx = LowerIrCtx::new(program, HashSet::new());
440 cx.set_return_ty(cx.resolve_type_ref(&h.return_type));
441 cx.set_store_queryable(store_queryable.clone());
442 cx.bind("self".to_string(), state_ty);
443 for (name, ty) in store_cells {
444 cx.bind(name.clone(), *ty);
445 }
446 for p in &h.params {
447 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
448 panic!(
449 "bynk internal error (ADR 0334): handler parameter `{}`'s type does not resolve \
450 in this pass's own scope, but the checker already accepted this handler",
451 p.name.name
452 )
453 });
454 cx.bind(p.name.name.clone(), ty);
455 }
456 let block = lower_block_ir(&h.body, &mut cx);
457 wrap_body_return(block)
458}
459
460pub fn lower_handler_ir(
488 h: &Handler,
489 store_cells: &HashMap<String, TyId>,
490 store_queryable: &HashSet<String>,
491 state_ty: TyId,
492 invariants: &[IrPredicate],
493 transitions: &[IrPredicate],
494 program: &CheckedProgram,
495) -> IrHandler {
496 assert!(
504 h.by_clause.is_none(),
505 "bynk internal error (ADR 0334): lower_handler_ir is agent-only (DECISION D) — an agent \
506 handler cannot carry a `by` clause (bynk.actor.by_on_agent), so a handler that does is a \
507 service handler reaching the wrong entry point"
508 );
509 let cx = LowerIrCtx::new(program, HashSet::new());
510 let (params, given, ret, effectful) = lower_handler_signature_ir(h, &cx);
511 let emits = block_uses_emit(&h.body, &program.program().callees);
512 let commit = lower_commit_shape_ir(&h.body, invariants, transitions, emits, program);
513 let body = lower_handler_body_ir(h, store_cells, store_queryable, state_ty, program);
514 IrHandler {
515 kind: lower_handler_kind_ir(&h.kind),
516 params,
517 given,
518 actors: Vec::new(),
521 binder: None,
522 connection: None,
527 body,
528 commit,
529 ret,
530 effectful,
531 method_name: h.method_name.as_ref().map(|i| i.name.clone()),
532 }
533}
534
535pub type HandlerSignatureIr = (Vec<(String, TyId)>, Vec<String>, TyId, bool);
541
542fn lower_handler_signature_ir(h: &Handler, cx: &LowerIrCtx) -> HandlerSignatureIr {
551 let params: Vec<(String, TyId)> = h
552 .params
553 .iter()
554 .map(|p| {
555 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
556 panic!(
557 "bynk internal error (ADR 0334): handler parameter `{}`'s type does not \
558 resolve in this pass's own scope, but the checker already accepted this \
559 handler",
560 p.name.name
561 )
562 });
563 (p.name.name.clone(), ty)
564 })
565 .collect();
566 let given: Vec<String> = h.given.iter().map(|c| c.key().to_string()).collect();
567 let ret = cx.resolve_type_ref(&h.return_type).unwrap_or_else(|| {
568 panic!(
569 "bynk internal error (ADR 0334): a handler's return type does not resolve in this \
570 pass's own scope, but the checker already accepted this handler"
571 )
572 });
573 let effectful = matches!(&*cx.program.ty_intern.get(ret), Ty::Effect(_));
574 (params, given, ret, effectful)
575}
576
577pub fn is_effectful_return(r: &TypeRef) -> bool {
591 matches!(r, TypeRef::Effect(_, _))
592}
593
594pub fn lower_service_handler_signature_ir(
646 h: &Handler,
647 program: &CheckedProgram,
648) -> HandlerSignatureIr {
649 let cx = LowerIrCtx::new(program, HashSet::new());
650 let params: Vec<(String, TyId)> = h
651 .params
652 .iter()
653 .map(|p| {
654 let ty = cx
655 .resolve_type_ref(&p.type_ref)
656 .unwrap_or_else(|| cx.unit_ty());
657 (p.name.name.clone(), ty)
658 })
659 .collect();
660 let given: Vec<String> = h.given.iter().map(|c| c.key().to_string()).collect();
661 let ret = cx
662 .resolve_type_ref(&h.return_type)
663 .unwrap_or_else(|| cx.unit_ty());
664 let effectful = is_effectful_return(&h.return_type);
665 (params, given, ret, effectful)
666}
667
668fn lower_service_handler_body_ir(
686 h: &Handler,
687 binder: Option<&ActorBinder>,
688 connection: Option<&ConnectionBinder>,
689 program: &CheckedProgram,
690) -> IrExpr {
691 let mut cx = LowerIrCtx::new(program, HashSet::new());
692 cx.set_return_ty(cx.resolve_type_ref(&h.return_type));
693 if let Some(connection) = connection {
694 cx.bind("connection".to_string(), connection.ty);
695 }
696 for p in &h.params {
710 let ty = cx
711 .resolve_type_ref(&p.type_ref)
712 .unwrap_or_else(|| cx.unit_ty());
713 cx.bind(p.name.name.clone(), ty);
714 }
715 if let Some(binder) = binder {
716 cx.bind(binder.binder.clone(), binder.ty);
717 }
718 let block = lower_block_ir(&h.body, &mut cx);
719 wrap_body_return(block)
720}
721
722pub fn lower_service_handler_ir(
790 h: &Handler,
791 protocol: &ServiceProtocol,
792 program: &CheckedProgram,
793) -> IrHandler {
794 let cx = LowerIrCtx::new(program, HashSet::new());
795 let (params, given, ret, effectful) = lower_service_handler_signature_ir(h, program);
806 let actors: Vec<String> = h
813 .by_clause
814 .as_ref()
815 .map(|by| by.actors.iter().map(|a| a.name.clone()).collect())
816 .unwrap_or_default();
817 let binder = program
818 .program()
819 .actor_binding(h.span)
820 .map(|(name, ty)| ActorBinder {
821 binder: name.clone(),
822 ty: *ty,
823 });
824 let connection = match (&h.kind, protocol) {
825 (
826 HandlerKind::Open | HandlerKind::Message | HandlerKind::Close,
827 ServiceProtocol::WebSocket { out_type, .. },
828 ) => {
829 let conn_ref =
830 bynk_syntax::ast::TypeRef::Connection(Box::new(out_type.clone()), h.span);
831 let ty = cx.resolve_type_ref(&conn_ref).unwrap_or_else(|| {
832 panic!(
833 "bynk internal error (ADR 0334): a `from websocket` lifecycle handler's \
834 synthetic `connection: Connection[out]` type does not resolve in this \
835 pass's own scope, but the checker already accepted this handler"
836 )
837 });
838 Some(ConnectionBinder {
839 ty,
840 borrowed: matches!(h.kind, HandlerKind::Message | HandlerKind::Close),
841 })
842 }
843 _ => None,
844 };
845 let emits = block_uses_emit(&h.body, &program.program().callees);
846 let commit = lower_commit_shape_ir(&h.body, &[], &[], emits, program);
847 let body = lower_service_handler_body_ir(h, binder.as_ref(), connection.as_ref(), program);
848 IrHandler {
849 kind: lower_handler_kind_ir(&h.kind),
850 params,
851 given,
852 actors,
853 binder,
854 connection,
855 body,
856 commit,
857 ret,
858 effectful,
859 method_name: h.method_name.as_ref().map(|i| i.name.clone()),
860 }
861}
862
863pub fn lower_type_item_ir(decl: &Arc<TypeDecl>, program: &CheckedProgram) -> IrItem {
880 let program = program.program();
881 let type_vars: HashSet<String> = decl
882 .type_params
883 .iter()
884 .map(|tp| tp.name.name.clone())
885 .collect();
886 let resolve = |r: &bynk_syntax::ast::TypeRef| {
887 checker::resolve_type_ref_in(r, &program.types, &type_vars, &program.ty_intern)
888 };
889 let shape = match &decl.body {
890 TypeBody::Record(r) => TypeShape::Record {
891 fields: r
892 .fields
893 .iter()
894 .map(|f| {
895 let ty = resolve(&f.type_ref).unwrap_or_else(|| {
896 panic!(
897 "bynk internal error (ADR 0334): field `{}` of type `{}` does not \
898 resolve, but the checker already accepted this declaration",
899 f.name.name, decl.name.name
900 )
901 });
902 (f.name.name.clone(), ty)
903 })
904 .collect(),
905 },
906 TypeBody::Sum(s) => TypeShape::Sum {
907 variants: s
908 .variants
909 .iter()
910 .map(|v| {
911 let payload = v
912 .payload
913 .iter()
914 .map(|vf| {
915 let ty = resolve(&vf.type_ref).unwrap_or_else(|| {
916 panic!(
917 "bynk internal error (ADR 0334): field `{}` of variant `{}` \
918 of type `{}` does not resolve, but the checker already \
919 accepted this declaration",
920 vf.name.name, v.name.name, decl.name.name
921 )
922 });
923 (vf.name.name.clone(), ty)
924 })
925 .collect();
926 (v.name.name.clone(), payload)
927 })
928 .collect(),
929 embeds: s
930 .embeds
931 .iter()
932 .map(|e| {
933 let source = resolve(&e.source_type).unwrap_or_else(|| {
934 panic!(
935 "bynk internal error (ADR 0334): `embeds` clause source type on \
936 variant `{}` of type `{}` does not resolve, but the checker \
937 already accepted this declaration",
938 e.variant.name, decl.name.name
939 )
940 });
941 (source, e.variant.name.clone())
942 })
943 .collect(),
944 },
945 TypeBody::Refined {
946 base, refinement, ..
947 } => TypeShape::Refined {
948 base: *base,
949 refinement: refinement.clone(),
950 opaque: false,
951 },
952 TypeBody::Opaque {
953 base, refinement, ..
954 } => TypeShape::Refined {
955 base: *base,
956 refinement: refinement.clone(),
957 opaque: true,
958 },
959 };
960 IrItem::Type { shape }
961}
962
963pub fn lower_fn_item_ir(f: &Arc<FnDecl>, program: &CheckedProgram) -> IrItem {
978 let type_vars = fn_rigid_type_vars(f, program.program());
979 let cx = LowerIrCtx::new(program, type_vars);
980 let receiver = fn_receiver_ty(f, program.program());
981 let params: Vec<(String, TyId)> = f
982 .params
983 .iter()
984 .map(|p| {
985 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
986 panic!(
987 "bynk internal error (ADR 0334): parameter `{}`'s type does not resolve in \
988 this pass's own rigid-variable scope, but the checker already accepted \
989 this fn — bynk_lower's type_vars disagrees with bynk-check's \
990 Ctx::type_vars",
991 p.name.name
992 )
993 });
994 (p.name.name.clone(), ty)
995 })
996 .collect();
997 let ret = cx.resolve_type_ref(&f.return_type).unwrap_or_else(|| {
998 panic!(
999 "bynk internal error (ADR 0334): the return type of `{}` does not resolve in this \
1000 pass's own rigid-variable scope, but the checker already accepted this fn",
1001 f.name.display()
1002 )
1003 });
1004 let effectful = matches!(&*program.program().ty_intern.get(ret), Ty::Effect(_));
1005 IrItem::Fn {
1006 receiver,
1007 params,
1008 ret,
1009 body: lower_fn_body_ir(f, program),
1010 effectful,
1011 }
1012}
1013
1014fn resolve_store_field_ty(cx: &LowerIrCtx, r: &bynk_syntax::ast::TypeRef) -> TyId {
1034 cx.resolve_type_ref(r).unwrap_or_else(|| cx.unit_ty())
1035}
1036
1037fn duration_millis_annotation(
1050 annotations: &[bynk_syntax::ast::Annotation],
1051 name: &str,
1052) -> Option<i64> {
1053 annotations
1054 .iter()
1055 .find(|a| a.name.name == name)
1056 .and_then(|a| match a.args.first().map(|arg| &arg.value.kind) {
1057 Some(ExprKind::DurationLit { millis, .. }) => Some(*millis),
1058 _ => None,
1059 })
1060}
1061
1062pub fn lower_store_field_ir(f: &StoreField, program: &CheckedProgram) -> StoreFieldIr {
1113 let mut cx = LowerIrCtx::new(program, HashSet::new());
1114 let (kind, indexed) = store_field_kind_and_indexed(f, &cx);
1115 let init = match &kind {
1117 StoreKindIr::Cell(_) => f.init.as_ref().map(|e| lower_expr_ir(e, &mut cx)),
1118 _ => None,
1119 };
1120 StoreFieldIr {
1121 field: f.name.name.clone(),
1122 kind,
1123 init,
1124 indexed,
1125 }
1126}
1127
1128fn store_field_kind_and_indexed(f: &StoreField, cx: &LowerIrCtx) -> (StoreKindIr, Vec<IndexIr>) {
1134 let head = f.kind.head.name.as_str();
1135 let kind = match head {
1136 "Cell" => StoreKindIr::Cell(resolve_store_field_ty(cx, &f.kind.args[0])),
1137 "Map" => StoreKindIr::Map(
1138 resolve_store_field_ty(cx, &f.kind.args[0]),
1139 resolve_store_field_ty(cx, &f.kind.args[1]),
1140 ),
1141 "Set" => StoreKindIr::Set(resolve_store_field_ty(cx, &f.kind.args[0])),
1142 "Cache" => {
1143 let k = resolve_store_field_ty(cx, &f.kind.args[0]);
1144 let v = resolve_store_field_ty(cx, &f.kind.args[1]);
1145 let ttl = duration_millis_annotation(&f.annotations, "ttl").unwrap_or_else(|| {
1146 panic!(
1147 "bynk internal error (ADR 0334): `Cache` field `{}` has no resolvable \
1148 `@ttl` millis, but the checker already accepted this declaration — \
1149 bynk.store.cache_ttl_required gates a missing or malformed `@ttl` \
1150 before certify",
1151 f.name.name
1152 )
1153 });
1154 StoreKindIr::Cache(k, v, ttl)
1155 }
1156 "Log" => {
1157 let elem = resolve_store_field_ty(cx, &f.kind.args[0]);
1158 let retain = duration_millis_annotation(&f.annotations, "retain");
1159 StoreKindIr::Log(elem, retain)
1160 }
1161 other => panic!(
1162 "bynk internal error (ADR 0334): store field `{}` has storage kind `{other}`, which \
1163 cannot reach a certified program — only Cell/Map/Set/Cache/Log are functional \
1164 (Queue is gated by bynk.store.kind_unsupported before certify)",
1165 f.name.name
1166 ),
1167 };
1168 let mut indexed: Vec<IndexIr> = Vec::new();
1178 for arg in f
1179 .annotations
1180 .iter()
1181 .filter(|a| a.name.name == "indexed")
1182 .flat_map(|a| &a.args)
1183 {
1184 if let (Some(l), ExprKind::Ident(k)) = (&arg.label, &arg.value.kind)
1185 && l.name == "by"
1186 && !indexed.contains(&k.name)
1187 {
1188 indexed.push(k.name.clone());
1189 }
1190 }
1191 (kind, indexed)
1192}
1193
1194pub fn lower_store_field_shape_ir(f: &StoreField, program: &CheckedProgram) -> StoreFieldIr {
1212 let cx = LowerIrCtx::new(program, HashSet::new());
1213 let (kind, indexed) = store_field_kind_and_indexed(f, &cx);
1214 StoreFieldIr {
1215 field: f.name.name.clone(),
1216 kind,
1217 init: None,
1218 indexed,
1219 }
1220}
1221
1222pub fn lower_invariant_ir(
1235 inv: &Invariant,
1236 store_cells: &HashMap<String, TyId>,
1237 program: &CheckedProgram,
1238) -> IrPredicate {
1239 let mut cx = LowerIrCtx::new(program, HashSet::new());
1240 for (name, ty) in store_cells {
1241 cx.bind(name.clone(), *ty);
1242 }
1243 IrPredicate {
1244 name: inv.name.name.clone(),
1245 predicate: lower_expr_ir(&inv.predicate, &mut cx),
1246 }
1247}
1248
1249pub fn lower_transition_ir(
1259 tr: &Transition,
1260 state_ty: TyId,
1261 program: &CheckedProgram,
1262) -> IrPredicate {
1263 let mut cx = LowerIrCtx::new(program, HashSet::new());
1264 cx.bind("old".to_string(), state_ty);
1265 cx.bind("new".to_string(), state_ty);
1266 IrPredicate {
1267 name: tr.name.name.clone(),
1268 predicate: lower_expr_ir(&tr.predicate, &mut cx),
1269 }
1270}
1271
1272pub fn body_writes_state(body: &Block, program: &TypedCommons) -> bool {
1299 fn is_mutating_store_write(e: &Expr, program: &TypedCommons) -> bool {
1300 match program.callees.get(&e.id) {
1301 Some(Callee::Store { op, .. }) => {
1302 MUTATING_MAP_CACHE_OPS.contains(&op.as_str())
1303 || MUTATING_SET_OPS.contains(&op.as_str())
1304 || MUTATING_LOG_OPS.contains(&op.as_str())
1305 || MUTATING_CELL_OPS.contains(&op.as_str())
1306 }
1307 _ => false,
1308 }
1309 }
1310 fn stmt(s: &Statement, program: &TypedCommons) -> bool {
1311 match s {
1312 Statement::Assign(_) => true,
1313 Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value, program),
1314 Statement::Expect(a) => expr(&a.value, program),
1315 Statement::Send(s) => expr(&s.value, program),
1316 Statement::Do(d) => expr(&d.value, program),
1317 }
1318 }
1319 fn expr(e: &Expr, program: &TypedCommons) -> bool {
1320 if is_mutating_store_write(e, program) {
1321 return true;
1322 }
1323 match &e.kind {
1324 ExprKind::Block(b) => body_writes_state(b, program),
1325 ExprKind::If {
1326 cond,
1327 then_block,
1328 else_block,
1329 } => {
1330 expr(cond, program)
1331 || body_writes_state(then_block, program)
1332 || body_writes_state(else_block, program)
1333 }
1334 ExprKind::Match { discriminant, arms } => {
1335 expr(discriminant, program)
1336 || arms.iter().any(|a| match &a.body {
1337 MatchBody::Expr(e) => expr(e, program),
1338 MatchBody::Block(b) => body_writes_state(b, program),
1339 })
1340 }
1341 _ => expr_children(e).into_iter().any(|c| expr(c, program)),
1342 }
1343 }
1344 body.statements.iter().any(|s| stmt(s, program)) || expr(&body.tail, program)
1345}
1346
1347pub fn lower_commit_shape_ir(
1368 body: &Block,
1369 invariants: &[IrPredicate],
1370 transitions: &[IrPredicate],
1371 emits: bool,
1372 program: &CheckedProgram,
1373) -> CommitShape {
1374 if body_writes_state(body, program.program()) {
1375 CommitShape::Transactional {
1376 invariants: invariants.to_vec(),
1377 transitions: transitions.to_vec(),
1378 }
1379 } else if emits {
1380 CommitShape::FlushEvents
1381 } else {
1382 CommitShape::ReadOnly
1383 }
1384}
1385
1386fn lower_event_pattern_ir(pattern: &EventPattern) -> EventPatternIr {
1394 EventPatternIr {
1395 fields: pattern
1396 .fields
1397 .iter()
1398 .map(|f| {
1399 let value = match &f.value {
1400 EventPatternValue::Literal { value, .. } => {
1401 EventPatternValueIr::Const(match value {
1402 LiteralValue::Int(n) => ConstVal::Int(*n),
1403 LiteralValue::Str(s) => ConstVal::Str(s.clone()),
1404 LiteralValue::Bool(b) => ConstVal::Bool(*b),
1405 })
1406 }
1407 EventPatternValue::Variant { variant, .. } => EventPatternValueIr::Variant {
1408 tag: variant.name.clone(),
1409 },
1410 };
1411 (f.name.name.clone(), value)
1412 })
1413 .collect(),
1414 }
1415}
1416
1417pub fn lower_protocol_ir(protocol: &ServiceProtocol, program: &CheckedProgram) -> ProtocolIr {
1436 lower_protocol_ir_from_commons(protocol, program.program())
1437}
1438
1439pub fn lower_protocol_ir_from_commons(
1448 protocol: &ServiceProtocol,
1449 commons: &TypedCommons,
1450) -> ProtocolIr {
1451 let cx = LowerIrCtx::from_commons(commons, HashSet::new());
1452 match protocol {
1453 ServiceProtocol::Call => ProtocolIr::Call,
1454 ServiceProtocol::Http => ProtocolIr::Http,
1455 ServiceProtocol::Cron => ProtocolIr::Cron,
1456 ServiceProtocol::Queue { name } => ProtocolIr::Queue { name: name.clone() },
1457 ServiceProtocol::WebSocket { in_type, out_type } => ProtocolIr::WebSocket {
1458 in_ty: cx.resolve_type_ref(in_type).unwrap_or_else(|| cx.unit_ty()),
1459 out_ty: cx
1460 .resolve_type_ref(out_type)
1461 .unwrap_or_else(|| cx.unit_ty()),
1462 },
1463 ServiceProtocol::Events {
1464 event_type,
1465 pattern,
1466 schema_dispatch,
1467 } => ProtocolIr::Events {
1468 event: cx
1469 .resolve_type_ref(event_type)
1470 .unwrap_or_else(|| cx.unit_ty()),
1471 pattern: pattern.as_ref().map(lower_event_pattern_ir),
1472 schema_dispatch: schema_dispatch.as_ref().map(|d| {
1473 let bynk_syntax::ast::SchemaVersionPattern::Literal(version) = d.pattern;
1474 version
1475 }),
1476 },
1477 }
1478}
1479
1480fn lower_policy_ir(service: &ServiceDecl) -> Option<PolicyIr> {
1508 if !matches!(service.protocol, ServiceProtocol::Http) {
1509 return None;
1510 }
1511 Some(PolicyIr {
1512 cors: service.cors.as_ref().map(|p| CorsIr {
1513 origins: p.origins(),
1514 credentials: p.credentials(),
1515 allow_headers: p.allow_headers(),
1516 max_age_secs: p.max_age_secs(),
1517 }),
1518 security: match &service.security {
1519 Some(p) => SecurityIr {
1520 nosniff: p.nosniff(),
1521 hsts_max_age_secs: p.hsts_max_age_secs(),
1522 },
1523 None => SecurityIr {
1524 nosniff: true,
1525 hsts_max_age_secs: None,
1526 },
1527 },
1528 max_body_bytes: service.limits.as_ref().and_then(|p| p.max_body()),
1529 })
1530}
1531
1532pub fn lower_route_cache_ir(h: &Handler) -> Option<CacheIr> {
1545 if !matches!(
1546 h.kind,
1547 HandlerKind::Http {
1548 method: HttpMethod::Get,
1549 ..
1550 }
1551 ) {
1552 return None;
1553 }
1554 let ann = h.annotations.iter().find(|a| a.name.name == "cache")?;
1555 let mut max_age_millis: Option<i64> = None;
1556 let mut scope = "private";
1557 for arg in &ann.args {
1558 match arg.label.as_ref().map(|l| l.name.as_str()) {
1559 Some("maxAge") => {
1560 if let ExprKind::DurationLit { millis, .. } = &arg.value.kind {
1561 max_age_millis = Some(*millis);
1562 }
1563 }
1564 Some("scope") => {
1565 if let ExprKind::Ident(id) = &arg.value.kind
1566 && id.name == "public"
1567 {
1568 scope = "public";
1569 }
1570 }
1571 _ => {}
1572 }
1573 }
1574 Some(CacheIr {
1575 max_age_secs: max_age_millis? / 1000,
1576 scope,
1577 })
1578}
1579
1580pub fn lower_route_limit_ir(h: &Handler) -> Option<i64> {
1593 let ann = h.annotations.iter().find(|a| a.name.name == "limit")?;
1594 for arg in &ann.args {
1595 if arg.label.as_ref().map(|l| l.name.as_str()) == Some("maxBody")
1596 && let ExprKind::IntLit { value: n, .. } = &arg.value.kind
1597 && *n > 0
1598 {
1599 return Some(*n);
1600 }
1601 }
1602 None
1603}
1604
1605pub fn lower_agent_item_ir(agent: &AgentDecl, program: &CheckedProgram) -> IrItem {
1628 let cx = LowerIrCtx::new(program, HashSet::new());
1629 let key_ty = cx
1640 .resolve_type_ref(&agent.key_type)
1641 .unwrap_or_else(|| cx.unit_ty());
1642 let state: Vec<StoreFieldIr> = agent
1643 .store_fields
1644 .iter()
1645 .map(|f| lower_store_field_ir(f, program))
1646 .collect();
1647 let store_cells: HashMap<String, TyId> = state
1648 .iter()
1649 .filter_map(|f| match f.kind {
1650 StoreKindIr::Cell(ty) => Some((f.field.clone(), ty)),
1651 _ => None,
1652 })
1653 .collect();
1654 let store_queryable: HashSet<String> = state
1672 .iter()
1673 .filter_map(|f| match f.kind {
1674 StoreKindIr::Map(_, _) => Some(f.field.clone()),
1675 _ => None,
1676 })
1677 .collect();
1678 let state_ty = program.program().ty_intern.intern(Ty::Named {
1679 name: format!("{}State", agent.name.name),
1680 kind: checker::NamedKind::Record,
1681 args: Vec::new(),
1682 });
1683 let invariants: Vec<IrPredicate> = agent
1684 .invariants
1685 .iter()
1686 .map(|inv| lower_invariant_ir(inv, &store_cells, program))
1687 .collect();
1688 let transitions: Vec<IrPredicate> = agent
1689 .transitions
1690 .iter()
1691 .map(|tr| lower_transition_ir(tr, state_ty, program))
1692 .collect();
1693 let handlers: Vec<IrHandler> = agent
1694 .handlers
1695 .iter()
1696 .map(|h| {
1697 lower_handler_ir(
1698 h,
1699 &store_cells,
1700 &store_queryable,
1701 state_ty,
1702 &invariants,
1703 &transitions,
1704 program,
1705 )
1706 })
1707 .collect();
1708 IrItem::Agent {
1709 def: agent.name.name.clone(),
1710 key: (agent.key_name.name.clone(), key_ty),
1711 state,
1712 handlers,
1713 invariants,
1714 transitions,
1715 }
1716}
1717
1718pub fn lower_service_item_ir(service: &ServiceDecl, program: &CheckedProgram) -> IrItem {
1741 IrItem::Service {
1742 def: service.name.name.clone(),
1743 protocol: lower_protocol_ir(&service.protocol, program),
1744 handlers: service
1745 .handlers
1746 .iter()
1747 .map(|h| lower_service_handler_ir(h, &service.protocol, program))
1748 .collect(),
1749 policy: lower_policy_ir(service),
1750 }
1751}
1752
1753pub fn lower_event_subscriber_shapes_ir(
1767 program: &CheckedProgram,
1768) -> HashMap<String, EventSubscriberShape> {
1769 let mut out = HashMap::new();
1770 for item in &program.program().commons.items {
1771 if let CommonsItem::Service(s) = item
1772 && matches!(&s.protocol, ServiceProtocol::Events { .. })
1773 {
1774 let IrItem::Service {
1775 protocol:
1776 ProtocolIr::Events {
1777 schema_dispatch, ..
1778 },
1779 handlers,
1780 ..
1781 } = lower_service_item_ir(s, program)
1782 else {
1783 panic!(
1784 "bynk internal error: lower_service_item_ir did not return \
1785 IrItem::Service{{ protocol: ProtocolIr::Events, .. }} for a service \
1786 whose own AST protocol is ServiceProtocol::Events"
1787 )
1788 };
1789 let two_param_handler = handlers
1790 .iter()
1791 .find(|h| matches!(h.kind, IrHandlerKind::Event))
1792 .is_some_and(|h| h.params.len() == 2);
1793 out.insert(
1794 s.name.name.clone(),
1795 EventSubscriberShape {
1796 two_param_handler,
1797 schema_dispatch: schema_dispatch.is_some(),
1798 },
1799 );
1800 }
1801 }
1802 out
1803}
1804
1805pub fn lower_capability_item_ir(cap: &CapabilityDecl, program: &CheckedProgram) -> IrItem {
1812 IrItem::Capability {
1813 def: cap.name.name.clone(),
1814 ops: cap
1815 .ops
1816 .iter()
1817 .map(|op| lower_op_sig_ir(op, program))
1818 .collect(),
1819 }
1820}
1821
1822pub fn capability_op_sig_from_commons(
1837 commons: &TypedCommons,
1838 cap: &str,
1839 op: &str,
1840) -> Option<OpSig> {
1841 commons.commons.items.iter().find_map(|item| {
1842 let CommonsItem::Capability(c) = item else {
1843 return None;
1844 };
1845 if c.name.name != cap {
1846 return None;
1847 }
1848 c.ops
1849 .iter()
1850 .find(|o| o.name.name == op)
1851 .map(|o| lower_op_sig_ir_from_commons(o, commons))
1852 })
1853}
1854
1855fn lower_op_sig_ir(op: &CapabilityOp, program: &CheckedProgram) -> OpSig {
1885 lower_op_sig_ir_from_commons(op, program.program())
1886}
1887
1888pub fn lower_op_sig_ir_from_commons(op: &CapabilityOp, commons: &TypedCommons) -> OpSig {
1907 let type_vars: HashSet<String> = op
1908 .type_params
1909 .iter()
1910 .map(|tp| tp.name.name.clone())
1911 .collect();
1912 let cx = LowerIrCtx::from_commons(commons, type_vars);
1913 let params: Vec<(String, TyId)> = op
1914 .params
1915 .iter()
1916 .map(|p| {
1917 let ty = cx
1918 .resolve_type_ref(&p.type_ref)
1919 .unwrap_or_else(|| cx.unit_ty());
1920 (p.name.name.clone(), ty)
1921 })
1922 .collect();
1923 let return_ty = cx
1924 .resolve_type_ref(&op.return_type)
1925 .unwrap_or_else(|| cx.unit_ty());
1926 OpSig {
1927 name: op.name.name.clone(),
1928 type_params: op
1929 .type_params
1930 .iter()
1931 .map(|tp| tp.name.name.clone())
1932 .collect(),
1933 params,
1934 return_ty,
1935 }
1936}
1937
1938pub fn lower_fn_sig_ir_from_types(
1961 f: &FnDecl,
1962 types: &HashMap<String, Arc<TypeDecl>>,
1963 tys: &Types,
1964) -> FnSig {
1965 let type_vars: HashSet<String> = f
1966 .type_params
1967 .iter()
1968 .map(|tp| tp.name.name.clone())
1969 .collect();
1970 let unit_ty = || tys.intern(Ty::Unit);
1971 let params: Vec<(String, TyId)> = f
1972 .params
1973 .iter()
1974 .map(|p| {
1975 let ty = checker::resolve_type_ref_in(&p.type_ref, types, &type_vars, tys)
1976 .unwrap_or_else(unit_ty);
1977 (p.name.name.clone(), ty)
1978 })
1979 .collect();
1980 let return_ty = checker::resolve_type_ref_in(&f.return_type, types, &type_vars, tys)
1981 .unwrap_or_else(unit_ty);
1982 let name = match &f.name {
1983 FnName::Method { method_name, .. } => method_name.name.clone(),
1984 FnName::Free(id) => id.name.clone(),
1985 };
1986 FnSig {
1987 name,
1988 has_self: f.has_self,
1989 params,
1990 return_ty,
1991 }
1992}
1993
1994pub fn lower_attached_fn_sig_ir_from_types(
2005 mt: &MethodTable,
2006 types: &HashMap<String, Arc<TypeDecl>>,
2007 tys: &Types,
2008) -> Vec<FnSig> {
2009 mt.instance
2010 .values()
2011 .chain(mt.statics.values())
2012 .filter(|f| matches!(f.name, FnName::Method { .. }))
2013 .map(|f| lower_fn_sig_ir_from_types(f, types, tys))
2014 .collect()
2015}
2016
2017pub fn lower_handler_kind_ir(k: &HandlerKind) -> IrHandlerKind {
2022 match k {
2023 HandlerKind::Call => IrHandlerKind::Call,
2024 HandlerKind::Http { method, path } => IrHandlerKind::Http {
2025 method: lower_http_method_ir(*method),
2026 path: path.clone(),
2027 },
2028 HandlerKind::Cron { expr } => IrHandlerKind::Cron { expr: expr.clone() },
2029 HandlerKind::Message => IrHandlerKind::Message,
2030 HandlerKind::Open => IrHandlerKind::Open,
2031 HandlerKind::Close => IrHandlerKind::Close,
2032 HandlerKind::Event => IrHandlerKind::Event,
2033 }
2034}
2035
2036fn lower_http_method_ir(m: HttpMethod) -> IrHttpMethod {
2038 match m {
2039 HttpMethod::Get => IrHttpMethod::Get,
2040 HttpMethod::Post => IrHttpMethod::Post,
2041 HttpMethod::Put => IrHttpMethod::Put,
2042 HttpMethod::Patch => IrHttpMethod::Patch,
2043 HttpMethod::Delete => IrHttpMethod::Delete,
2044 }
2045}
2046
2047pub fn lower_provider_item_ir(provider: &ProviderDecl, program: &CheckedProgram) -> IrItem {
2061 let given = lower_provider_given_ir(provider);
2062 let body = if provider.external {
2063 ProviderBody::External { given }
2064 } else {
2065 ProviderBody::Bynk {
2066 given,
2067 ops: provider
2068 .ops
2069 .iter()
2070 .map(|op| lower_provider_op_ir(op, program))
2071 .collect(),
2072 }
2073 };
2074 IrItem::Provider {
2075 def: provider.provider_name.name.clone(),
2076 cap: provider.capability.name.clone(),
2077 body,
2078 }
2079}
2080
2081pub fn lower_provider_given_ir(provider: &ProviderDecl) -> Vec<CapRefIr> {
2102 provider.given.iter().map(lower_cap_ref_ir).collect()
2103}
2104
2105pub fn lower_handler_given_ir(h: &Handler) -> Vec<CapRefIr> {
2114 h.given.iter().map(lower_cap_ref_ir).collect()
2115}
2116
2117pub fn lower_actor_seam_ir(handler: &Handler, actors: &HashMap<String, ActorDecl>) -> ActorSeamIr {
2139 if let Some(members) = bynk_check::actors::sum_members_for(handler, actors) {
2140 return ActorSeamIr::Sum(members);
2141 }
2142 if let Some(seam) = bynk_check::actors::bearer_seam_for(handler, actors) {
2143 return ActorSeamIr::Bearer(seam);
2144 }
2145 if let Some(seam) = bynk_check::actors::oidc_seam_for(handler, actors) {
2146 return ActorSeamIr::Oidc(seam);
2147 }
2148 if let Some(binder) = bynk_check::actors::caller_binder_for(handler, actors) {
2149 return ActorSeamIr::Caller(binder);
2150 }
2151 ActorSeamIr::None
2152}
2153
2154fn lower_cap_ref_ir(cap_ref: &CapRef) -> CapRefIr {
2159 CapRefIr {
2160 context: cap_ref.context.as_ref().map(QualifiedName::joined),
2161 name: cap_ref.name.name.clone(),
2162 }
2163}
2164
2165fn lower_provider_op_ir(op: &ProviderOp, program: &CheckedProgram) -> ProviderOpIr {
2186 let mut cx = LowerIrCtx::new(program, HashSet::new());
2187 cx.set_return_ty(cx.resolve_type_ref(&op.return_type));
2195 let params: Vec<(String, TyId)> = op
2196 .params
2197 .iter()
2198 .map(|p| {
2199 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
2200 panic!(
2201 "bynk internal error (ADR 0334): parameter `{}`'s type does not resolve in \
2202 this pass's own scope, but the checker already accepted this provider op's \
2203 body via check_handler_body — bynk_lower's resolution disagrees \
2204 with bynk-check's",
2205 p.name.name
2206 )
2207 });
2208 cx.bind(p.name.name.clone(), ty);
2209 (p.name.name.clone(), ty)
2210 })
2211 .collect();
2212 let return_ty = cx.resolve_type_ref(&op.return_type).unwrap_or_else(|| {
2213 panic!(
2214 "bynk internal error (ADR 0334): the return type of provider op `{}` does not \
2215 resolve in this pass's own scope, but the checker already accepted this provider \
2216 op's body via check_handler_body",
2217 op.name.name
2218 )
2219 });
2220 let body = wrap_body_return(lower_block_ir(&op.body, &mut cx));
2221 ProviderOpIr {
2222 name: op.name.name.clone(),
2223 params,
2224 return_ty,
2225 body,
2226 }
2227}
2228
2229pub fn lower_block_ir(block: &Block, cx: &mut LowerIrCtx) -> IrExpr {
2233 cx.push_scope();
2234 let stmts: Vec<IrStmt> = block
2235 .statements
2236 .iter()
2237 .map(|s| lower_stmt_ir(s, cx))
2238 .collect();
2239 let tail = lower_expr_ir(&block.tail, cx);
2240 cx.pop_scope();
2241 let ty = tail.ty;
2242 IrExpr {
2243 kind: IrExprKind::Block {
2244 stmts,
2245 tail: Box::new(tail),
2246 },
2247 ty,
2248 span: block.span,
2249 }
2250}
2251
2252fn lower_stmt_ir(s: &Statement, cx: &mut LowerIrCtx) -> IrStmt {
2253 match s {
2254 Statement::Let(l) => {
2255 let value = lower_expr_ir(&l.value, cx);
2256 let bound_ty = l.type_annot.as_ref().map_or(value.ty, |a| {
2267 cx.resolve_type_ref(a).unwrap_or_else(|| {
2268 panic!(
2269 "bynk internal error (ADR 0334): `let` annotation for `{}` does not \
2270 resolve in this pass's own rigid-variable scope, but the checker \
2271 already accepted this binding",
2272 l.name.name
2273 )
2274 })
2275 });
2276 if l.name.name != "_" {
2278 cx.bind(l.name.name.clone(), bound_ty);
2279 }
2280 IrStmt::Let {
2281 local: l.name.name.clone(),
2282 value,
2283 }
2284 }
2285 Statement::EffectLet(l) => {
2286 let effect = lower_expr_ir(&l.value, cx);
2287 let span = effect.span;
2288 let ty = cx.peel_effect(effect.ty);
2289 let bound_ty = l.type_annot.as_ref().map_or(ty, |a| {
2293 cx.resolve_type_ref(a).unwrap_or_else(|| {
2294 panic!(
2295 "bynk internal error (ADR 0334): `let <-` annotation for `{}` does not \
2296 resolve in this pass's own rigid-variable scope, but the checker \
2297 already accepted this binding",
2298 l.name.name
2299 )
2300 })
2301 });
2302 if l.name.name != "_" {
2303 cx.bind(l.name.name.clone(), bound_ty);
2304 }
2305 IrStmt::Let {
2306 local: l.name.name.clone(),
2307 value: IrExpr {
2308 kind: IrExprKind::Await {
2309 effect: Box::new(effect),
2310 },
2311 ty,
2312 span,
2313 },
2314 }
2315 }
2316 Statement::Send(send) => {
2317 let effect = lower_expr_ir(&send.value, cx);
2318 let span = effect.span;
2319 IrStmt::Expr {
2320 value: IrExpr {
2321 kind: IrExprKind::Send {
2322 effect: Box::new(effect),
2323 },
2324 ty: cx.unit_ty(),
2325 span,
2326 },
2327 }
2328 }
2329 Statement::Do(d) => {
2330 let effect = lower_expr_ir(&d.value, cx);
2331 let span = effect.span;
2332 let ty = cx.peel_effect(effect.ty);
2333 IrStmt::Expr {
2334 value: IrExpr {
2335 kind: IrExprKind::Await {
2336 effect: Box::new(effect),
2337 },
2338 ty,
2339 span,
2340 },
2341 }
2342 }
2343 Statement::Expect(_) => todo!(
2346 "Statement::Expect has no IrStmt target — not named by any rule this track commissions"
2347 ),
2348 Statement::Assign(a) => IrStmt::Assign {
2358 field: a.target.name.clone(),
2359 value: lower_expr_ir(&a.value, cx),
2360 },
2361 }
2362}
2363
2364fn lower_question_ir(inner: &Expr, ty: TyId, span: Span, cx: &mut LowerIrCtx) -> IrExpr {
2389 let scrutinee = Box::new(lower_expr_ir(inner, cx));
2390 let operand_ty = scrutinee.ty;
2391 let is_option = matches!(&*cx.program.ty_intern.get(operand_ty), Ty::Option(_));
2392
2393 let value_local = cx.fresh_tmp("__question_value");
2394 let (ok_tag, err_tag) = if is_option {
2395 ("Some", "None")
2396 } else {
2397 ("Ok", "Err")
2398 };
2399 let ok_variant = variant_info_of(operand_ty, ok_tag, cx.program);
2400 let ok_field_ty = ok_variant
2401 .payload
2402 .first()
2403 .map(|(_, t)| *t)
2404 .unwrap_or_else(|| cx.unit_ty());
2405 let ok_arm = IrArm {
2406 pat: IrPat::Variant {
2407 scrutinee_ty: operand_ty,
2408 tag: ok_tag.to_string(),
2409 fields: vec![(
2410 "value".to_string(),
2411 Box::new(IrPat::Bind {
2412 local: value_local.clone(),
2413 }),
2414 )],
2415 },
2416 guard: None,
2417 body: IrExpr {
2418 kind: IrExprKind::Local(value_local.clone()),
2419 ty: ok_field_ty,
2420 span,
2421 },
2422 binds: vec![value_local],
2423 binding_mode: BindingMode::Direct,
2424 };
2425
2426 let err_local = cx.fresh_tmp("__question_error");
2427 let (err_fields, err_binds, err_body) = if is_option {
2428 let http_result_ty = cx
2440 .return_ty
2441 .map(|rt| peel_effect_ty(rt, cx.program.tys()))
2442 .filter(|rt| matches!(&*cx.program.tys().get(*rt), Ty::HttpResult(_)))
2443 .unwrap_or_else(|| cx.unit_ty());
2444 (
2445 Vec::new(),
2446 Vec::new(),
2447 IrExpr {
2448 kind: IrExprKind::Return {
2449 value: Box::new(IrExpr {
2450 kind: IrExprKind::HttpResultNotFound,
2451 ty: http_result_ty,
2452 span,
2453 }),
2454 },
2455 ty: http_result_ty,
2456 span,
2457 },
2458 )
2459 } else {
2460 let err_variant = variant_info_of(operand_ty, "Err", cx.program);
2461 let err_field_ty = err_variant
2462 .payload
2463 .first()
2464 .map(|(_, t)| *t)
2465 .unwrap_or_else(|| cx.unit_ty());
2466 let err_value = IrExpr {
2467 kind: IrExprKind::Local(err_local.clone()),
2468 ty: err_field_ty,
2469 span,
2470 };
2471 let converted = match embed_conversion_ir(err_field_ty, cx) {
2474 Some((embed_ty, variant)) => IrExpr {
2475 kind: IrExprKind::Variant {
2476 tag: variant,
2477 payload: vec![err_value],
2478 },
2479 ty: embed_ty,
2480 span,
2481 },
2482 None => err_value,
2483 };
2484 let result_ty = cx
2493 .return_ty
2494 .map(|rt| peel_effect_ty(rt, cx.program.tys()))
2495 .filter(|rt| matches!(&*cx.program.tys().get(*rt), Ty::Result(..)))
2496 .unwrap_or(operand_ty);
2497 let err_construction = IrExpr {
2498 kind: IrExprKind::Variant {
2499 tag: "Err".to_string(),
2500 payload: vec![converted],
2501 },
2502 ty: result_ty,
2503 span,
2504 };
2505 (
2506 vec![(
2507 "error".to_string(),
2508 Box::new(IrPat::Bind {
2509 local: err_local.clone(),
2510 }),
2511 )],
2512 vec![err_local],
2513 IrExpr {
2514 kind: IrExprKind::Return {
2515 value: Box::new(err_construction),
2519 },
2520 ty: result_ty,
2521 span,
2522 },
2523 )
2524 };
2525 let err_arm = IrArm {
2526 pat: IrPat::Variant {
2527 scrutinee_ty: operand_ty,
2528 tag: err_tag.to_string(),
2529 fields: err_fields,
2530 },
2531 guard: None,
2532 body: err_body,
2533 binds: err_binds,
2534 binding_mode: BindingMode::Direct,
2535 };
2536
2537 IrExpr {
2538 kind: IrExprKind::Match {
2539 scrutinee,
2540 arms: vec![ok_arm, err_arm],
2541 exhaustive: Exhaustive::Total,
2542 form: MatchForm::Flat,
2543 },
2544 ty,
2545 span,
2546 }
2547}
2548
2549fn peel_effect_ty(ty: TyId, tys: &Types) -> TyId {
2557 match &*tys.get(ty) {
2558 Ty::Effect(inner) => peel_effect_ty(*inner, tys),
2559 _ => ty,
2560 }
2561}
2562
2563fn embed_conversion_ir(source_err_ty: TyId, cx: &LowerIrCtx) -> Option<(TyId, String)> {
2576 let tys = cx.program.tys();
2577 let target_ty = peel_effect_ty(cx.return_ty?, tys);
2578 let Ty::Result(_, target_err_ty) = &*tys.get(target_ty) else {
2579 return None;
2580 };
2581 if checker::compatible(source_err_ty, *target_err_ty, tys) {
2586 return None;
2587 }
2588 let (_ty_name, variant) =
2589 checker::embedding_for(*target_err_ty, source_err_ty, &cx.program.types, tys)?;
2590 Some((*target_err_ty, variant))
2591}
2592
2593fn lower_is_ir(
2616 value: &Expr,
2617 pattern: &Pattern,
2618 ty: TyId,
2619 span: Span,
2620 cx: &mut LowerIrCtx,
2621) -> IrExpr {
2622 let scrutinee = lower_expr_ir(value, cx);
2623 let scrutinee_ty = scrutinee.ty;
2624 let recv_local = cx.fresh_tmp("__is_receiver");
2625 let recv = IrExpr {
2626 kind: IrExprKind::Local(recv_local.clone()),
2627 ty: scrutinee_ty,
2628 span,
2629 };
2630
2631 let bool_expr = match pattern {
2632 Pattern::Wildcard(_) | Pattern::Binding(_) => IrExpr {
2633 kind: IrExprKind::Const(ConstVal::Bool(true)),
2634 ty,
2635 span,
2636 },
2637 Pattern::Variant {
2638 variant, bindings, ..
2639 } if bindings.is_empty() && is_refined_is_check_ir(scrutinee_ty, &variant.name, cx) => {
2640 refined_check_ir(&recv, &variant.name, ty, span, cx)
2641 }
2642 _ => {
2643 let ir_pat = lower_pattern_ir(pattern, scrutinee_ty, cx.program);
2644 let tests = lower_pattern_test_ir(&recv, scrutinee_ty, &ir_pat, ty, cx.program);
2645 fold_and_ir(tests, ty, span)
2646 }
2647 };
2648
2649 IrExpr {
2650 kind: IrExprKind::Block {
2651 stmts: vec![IrStmt::Let {
2652 local: recv_local,
2653 value: scrutinee,
2654 }],
2655 tail: Box::new(bool_expr),
2656 },
2657 ty,
2658 span,
2659 }
2660}
2661
2662fn is_refined_is_check_ir(operand_ty: TyId, name: &str, cx: &LowerIrCtx) -> bool {
2667 let value_baseish = matches!(
2668 &*cx.program.ty_intern.get(operand_ty),
2669 Ty::Base(_)
2670 | Ty::Named {
2671 kind: NamedKind::Refined(_),
2672 ..
2673 }
2674 );
2675 let name_refined = matches!(
2676 cx.program.types.get(name).map(|d| &d.body),
2677 Some(TypeBody::Refined { .. })
2678 );
2679 value_baseish && name_refined
2680}
2681
2682fn refined_check_ir(
2688 recv: &IrExpr,
2689 name: &str,
2690 bool_ty: TyId,
2691 span: Span,
2692 cx: &LowerIrCtx,
2693) -> IrExpr {
2694 let Some(TypeBody::Refined {
2695 base, refinement, ..
2696 }) = cx.program.types.get(name).map(|d| &d.body)
2697 else {
2698 panic!(
2699 "bynk internal error (ADR 0334): `{name}` does not resolve to a declared refined \
2700 type, but is_refined_is_check_ir just confirmed it does"
2701 )
2702 };
2703 IrExpr {
2704 kind: IrExprKind::RefinedCheck {
2705 value: Box::new(recv.clone()),
2706 base: *base,
2707 refinement: refinement.clone(),
2708 },
2709 ty: bool_ty,
2710 span,
2711 }
2712}
2713
2714fn lower_pattern_test_ir(
2726 path: &IrExpr,
2727 path_ty: TyId,
2728 pat: &IrPat,
2729 bool_ty: TyId,
2730 program: &TypedCommons,
2731) -> Vec<IrExpr> {
2732 match pat {
2733 IrPat::Wild | IrPat::Bind { .. } => Vec::new(),
2734 IrPat::Const { value } => vec![IrExpr {
2735 kind: IrExprKind::BinOp {
2736 op: IrBinOp::Eq,
2737 lhs: Box::new(path.clone()),
2738 rhs: Box::new(IrExpr {
2739 kind: IrExprKind::Const(value.clone()),
2740 ty: path_ty,
2741 span: path.span,
2742 }),
2743 },
2744 ty: bool_ty,
2745 span: path.span,
2746 }],
2747 IrPat::Refined { inner, refinement } => {
2748 let mut tests = lower_pattern_test_ir(path, path_ty, inner, bool_ty, program);
2749 let base = literal_base_of_ty_ir(path_ty, program.tys()).unwrap_or_else(|| {
2750 panic!(
2751 "bynk internal error (ADR 0334): a refined pattern's scrutinee must be \
2752 literal-kind (Int/Float/String/Bool), but the checker already accepted \
2753 this pattern"
2754 )
2755 });
2756 tests.push(IrExpr {
2757 kind: IrExprKind::RefinedCheck {
2758 value: Box::new(path.clone()),
2759 base,
2760 refinement: Some(refinement.clone()),
2761 },
2762 ty: bool_ty,
2763 span: path.span,
2764 });
2765 tests
2766 }
2767 IrPat::Variant {
2768 scrutinee_ty,
2769 tag,
2770 fields,
2771 } => {
2772 let string_ty = program.tys().intern(Ty::Base(BaseType::String));
2773 let mut tests = vec![IrExpr {
2774 kind: IrExprKind::BinOp {
2775 op: IrBinOp::Eq,
2776 lhs: Box::new(IrExpr {
2777 kind: IrExprKind::Field {
2778 base: Box::new(path.clone()),
2779 field: "tag".to_string(),
2780 },
2781 ty: string_ty,
2782 span: path.span,
2783 }),
2784 rhs: Box::new(IrExpr {
2785 kind: IrExprKind::Const(ConstVal::Str(tag.clone())),
2786 ty: string_ty,
2787 span: path.span,
2788 }),
2789 },
2790 ty: bool_ty,
2791 span: path.span,
2792 }];
2793 let variant_info = variant_info_of(*scrutinee_ty, tag, program);
2794 for (field_name, sub_pat) in fields {
2795 if is_irrefutable_ir(sub_pat) {
2796 continue;
2797 }
2798 let field_ty = variant_info
2799 .payload
2800 .iter()
2801 .find(|(n, _)| n == field_name)
2802 .map(|(_, t)| *t)
2803 .unwrap_or(bool_ty);
2804 let field_path = IrExpr {
2805 kind: IrExprKind::Field {
2806 base: Box::new(path.clone()),
2807 field: field_name.clone(),
2808 },
2809 ty: field_ty,
2810 span: path.span,
2811 };
2812 tests.extend(lower_pattern_test_ir(
2813 &field_path,
2814 field_ty,
2815 sub_pat,
2816 bool_ty,
2817 program,
2818 ));
2819 }
2820 tests
2821 }
2822 IrPat::Or { alts } => {
2823 let terms: Vec<IrExpr> = alts
2824 .iter()
2825 .map(|alt| {
2826 let t = lower_pattern_test_ir(path, path_ty, alt, bool_ty, program);
2827 fold_and_ir(t, bool_ty, path.span)
2828 })
2829 .collect();
2830 vec![fold_or_ir(terms, bool_ty, path.span)]
2831 }
2832 }
2833}
2834
2835fn is_irrefutable_ir(pat: &IrPat) -> bool {
2839 match pat {
2840 IrPat::Wild | IrPat::Bind { .. } => true,
2841 IrPat::Or { alts } => alts.iter().any(is_irrefutable_ir),
2842 _ => false,
2843 }
2844}
2845
2846fn literal_base_of_ty_ir(ty: TyId, tys: &Types) -> Option<BaseType> {
2850 let base = match &*tys.get(ty) {
2851 Ty::Base(b) => *b,
2852 Ty::Named {
2853 kind: NamedKind::Refined(b),
2854 ..
2855 } => *b,
2856 _ => return None,
2857 };
2858 matches!(base, BaseType::Int | BaseType::String | BaseType::Bool).then_some(base)
2859}
2860
2861fn fold_and_ir(tests: Vec<IrExpr>, bool_ty: TyId, span: Span) -> IrExpr {
2865 let mut iter = tests.into_iter();
2866 let Some(first) = iter.next() else {
2867 return IrExpr {
2868 kind: IrExprKind::Const(ConstVal::Bool(true)),
2869 ty: bool_ty,
2870 span,
2871 };
2872 };
2873 iter.fold(first, |acc, next| IrExpr {
2874 kind: IrExprKind::And {
2875 lhs: Box::new(acc),
2876 rhs: Box::new(next),
2877 },
2878 ty: bool_ty,
2879 span,
2880 })
2881}
2882
2883fn fold_or_ir(terms: Vec<IrExpr>, bool_ty: TyId, span: Span) -> IrExpr {
2888 let mut iter = terms.into_iter();
2889 let Some(first) = iter.next() else {
2890 return IrExpr {
2891 kind: IrExprKind::Const(ConstVal::Bool(false)),
2892 ty: bool_ty,
2893 span,
2894 };
2895 };
2896 iter.fold(first, |acc, next| IrExpr {
2897 kind: IrExprKind::Or {
2898 lhs: Box::new(acc),
2899 rhs: Box::new(next),
2900 },
2901 ty: bool_ty,
2902 span,
2903 })
2904}
2905
2906pub fn lower_expr_ir(e: &Expr, cx: &mut LowerIrCtx) -> IrExpr {
2907 let ty = cx.expr_ty(e.id);
2908 let span = e.span;
2909 match &e.kind {
2910 ExprKind::IntLit { value, .. } => IrExpr {
2911 kind: IrExprKind::Const(ConstVal::Int(*value)),
2912 ty,
2913 span,
2914 },
2915 ExprKind::FloatLit { value, .. } => IrExpr {
2916 kind: IrExprKind::Const(ConstVal::Float(*value)),
2917 ty,
2918 span,
2919 },
2920 ExprKind::DurationLit { millis, .. } => IrExpr {
2921 kind: IrExprKind::Const(ConstVal::DurationMillis(*millis)),
2922 ty,
2923 span,
2924 },
2925 ExprKind::StrLit(s) => IrExpr {
2926 kind: IrExprKind::Const(ConstVal::Str(s.clone())),
2927 ty,
2928 span,
2929 },
2930 ExprKind::BoolLit(b) => IrExpr {
2931 kind: IrExprKind::Const(ConstVal::Bool(*b)),
2932 ty,
2933 span,
2934 },
2935 ExprKind::UnitLit => IrExpr {
2936 kind: IrExprKind::Const(ConstVal::Unit),
2937 ty,
2938 span,
2939 },
2940
2941 ExprKind::Ident(id) => IrExpr {
2942 kind: lower_ident_ir(&id.name, Some(e.id), cx),
2943 ty,
2944 span,
2945 },
2946
2947 ExprKind::RecordConstruction { fields, .. } => IrExpr {
2948 kind: IrExprKind::Record {
2949 fields: fields
2950 .iter()
2951 .map(|f| {
2952 let value = match &f.value {
2953 Some(v) => lower_expr_ir(v, cx),
2954 None => {
2960 let local_ty = cx.lookup(&f.name.name).unwrap_or_else(|| {
2961 panic!(
2962 "bynk internal error (ADR 0334): shorthand field `{}` has \
2963 no local binding in this pass's own scope — the checker \
2964 accepted it, so its own `ctx.lookup` must have found one",
2965 f.name.name
2966 )
2967 });
2968 IrExpr {
2969 kind: lower_ident_ir(&f.name.name, None, cx),
2970 ty: local_ty,
2971 span: f.name.span,
2972 }
2973 }
2974 };
2975 (f.name.name.clone(), value)
2976 })
2977 .collect(),
2978 },
2979 ty,
2980 span,
2981 },
2982 ExprKind::FieldAccess { receiver, field } => {
2983 if let ExprKind::Ident(id) = &receiver.kind
2997 && cx.lookup(&id.name).is_none()
2998 && let Some(decl) = cx.program.types.get(&id.name)
2999 && let TypeBody::Sum(s) = &decl.body
3000 && s.variants.iter().any(|v| v.name.name == field.name)
3001 {
3002 IrExpr {
3003 kind: IrExprKind::Variant {
3004 tag: field.name.clone(),
3005 payload: Vec::new(),
3006 },
3007 ty,
3008 span,
3009 }
3010 } else {
3011 IrExpr {
3012 kind: IrExprKind::Field {
3013 base: Box::new(lower_expr_ir(receiver, cx)),
3014 field: field.name.clone(),
3015 },
3016 ty,
3017 span,
3018 }
3019 }
3020 }
3021 ExprKind::ListLit(elems) => IrExpr {
3022 kind: IrExprKind::List {
3023 elems: elems.iter().map(|el| lower_expr_ir(el, cx)).collect(),
3024 },
3025 ty,
3026 span,
3027 },
3028 ExprKind::Block(b) => lower_block_ir(b, cx),
3029 ExprKind::If {
3030 cond,
3031 then_block,
3032 else_block,
3033 } => IrExpr {
3034 kind: IrExprKind::If {
3035 cond: Box::new(lower_expr_ir(cond, cx)),
3036 then_: Box::new(lower_block_ir(then_block, cx)),
3037 else_: Box::new(lower_block_ir(else_block, cx)),
3038 },
3039 ty,
3040 span,
3041 },
3042 ExprKind::BinOp(BinOp::And, lhs, rhs) => IrExpr {
3043 kind: IrExprKind::And {
3044 lhs: Box::new(lower_expr_ir(lhs, cx)),
3045 rhs: Box::new(lower_expr_ir(rhs, cx)),
3046 },
3047 ty,
3048 span,
3049 },
3050 ExprKind::BinOp(BinOp::Or, lhs, rhs) => IrExpr {
3051 kind: IrExprKind::Or {
3052 lhs: Box::new(lower_expr_ir(lhs, cx)),
3053 rhs: Box::new(lower_expr_ir(rhs, cx)),
3054 },
3055 ty,
3056 span,
3057 },
3058 ExprKind::UnaryOp(UnaryOp::Not, inner) => IrExpr {
3059 kind: IrExprKind::Not {
3060 operand: Box::new(lower_expr_ir(inner, cx)),
3061 },
3062 ty,
3063 span,
3064 },
3065 ExprKind::EffectPure(inner) => IrExpr {
3066 kind: IrExprKind::Pure {
3067 value: Box::new(lower_expr_ir(inner, cx)),
3068 },
3069 ty,
3070 span,
3071 },
3072 ExprKind::Paren(inner) => lower_expr_ir(inner, cx),
3076
3077 ExprKind::BinOp(BinOp::Implies, lhs, rhs) => IrExpr {
3096 kind: IrExprKind::Or {
3097 lhs: Box::new(IrExpr {
3101 kind: IrExprKind::Not {
3102 operand: Box::new(lower_expr_ir(lhs, cx)),
3103 },
3104 ty,
3105 span: lhs.span,
3106 }),
3107 rhs: Box::new(lower_expr_ir(rhs, cx)),
3108 },
3109 ty,
3110 span,
3111 },
3112
3113 ExprKind::BinOp(
3121 op @ (BinOp::Eq
3122 | BinOp::NotEq
3123 | BinOp::Lt
3124 | BinOp::LtEq
3125 | BinOp::Gt
3126 | BinOp::GtEq
3127 | BinOp::Add
3128 | BinOp::Sub
3129 | BinOp::Mul
3130 | BinOp::Div),
3131 lhs,
3132 rhs,
3133 ) => {
3134 let ir_op = match op {
3135 BinOp::Eq => IrBinOp::Eq,
3136 BinOp::NotEq => IrBinOp::NotEq,
3137 BinOp::Lt => IrBinOp::Lt,
3138 BinOp::LtEq => IrBinOp::LtEq,
3139 BinOp::Gt => IrBinOp::Gt,
3140 BinOp::GtEq => IrBinOp::GtEq,
3141 BinOp::Add => IrBinOp::Add,
3142 BinOp::Sub => IrBinOp::Sub,
3143 BinOp::Mul => IrBinOp::Mul,
3144 BinOp::Div => IrBinOp::Div,
3145 BinOp::And | BinOp::Or | BinOp::Implies => {
3146 unreachable!("And/Or/Implies are matched by their own arms above")
3147 }
3148 };
3149 IrExpr {
3150 kind: IrExprKind::BinOp {
3151 op: ir_op,
3152 lhs: Box::new(lower_expr_ir(lhs, cx)),
3153 rhs: Box::new(lower_expr_ir(rhs, cx)),
3154 },
3155 ty,
3156 span,
3157 }
3158 }
3159 ExprKind::UnaryOp(UnaryOp::Neg, inner) => IrExpr {
3162 kind: IrExprKind::Neg {
3163 operand: Box::new(lower_expr_ir(inner, cx)),
3164 },
3165 ty,
3166 span,
3167 },
3168 ExprKind::InterpStr(parts) => IrExpr {
3171 kind: IrExprKind::InterpStr {
3172 parts: parts.iter().map(|p| lower_interp_part_ir(p, cx)).collect(),
3173 },
3174 ty,
3175 span,
3176 },
3177 ExprKind::Call {
3178 type_args, args, ..
3179 } => lower_call_ir(e, None, type_args, args, cx),
3180 ExprKind::Lambda(lambda) => lower_lambda_ir(e, lambda, cx),
3181 ExprKind::Ok(inner) => IrExpr {
3189 kind: IrExprKind::Variant {
3190 tag: "Ok".to_string(),
3191 payload: vec![lower_expr_ir(inner, cx)],
3192 },
3193 ty,
3194 span,
3195 },
3196 ExprKind::Err(inner) => IrExpr {
3197 kind: IrExprKind::Variant {
3198 tag: "Err".to_string(),
3199 payload: vec![lower_expr_ir(inner, cx)],
3200 },
3201 ty,
3202 span,
3203 },
3204 ExprKind::Some(inner) => IrExpr {
3205 kind: IrExprKind::Variant {
3206 tag: "Some".to_string(),
3207 payload: vec![lower_expr_ir(inner, cx)],
3208 },
3209 ty,
3210 span,
3211 },
3212 ExprKind::None => IrExpr {
3213 kind: IrExprKind::Variant {
3214 tag: "None".to_string(),
3215 payload: Vec::new(),
3216 },
3217 ty,
3218 span,
3219 },
3220 ExprKind::Question(inner) => lower_question_ir(inner, ty, span, cx),
3221 ExprKind::ConstructorCall { args, .. } => lower_call_ir(e, None, &[], args, cx),
3222 ExprKind::MethodCall {
3223 receiver,
3224 type_args,
3225 args,
3226 ..
3227 } => lower_call_ir(e, Some(receiver), type_args, args, cx),
3228 ExprKind::Match { discriminant, arms } => {
3229 let scrutinee = Box::new(lower_expr_ir(discriminant, cx));
3230 let scrutinee_ty = scrutinee.ty;
3238 let ir_arms: Vec<IrArm> = arms
3239 .iter()
3240 .map(|a| lower_arm_ir(a, scrutinee_ty, cx))
3241 .collect();
3242 let exhaustive = lower_exhaustive_ir(arms);
3243 let form = if match_needs_if_chain(arms) {
3244 MatchForm::IfChain
3245 } else {
3246 MatchForm::Flat
3247 };
3248 IrExpr {
3259 kind: IrExprKind::Match {
3260 scrutinee,
3261 arms: ir_arms,
3262 exhaustive,
3263 form,
3264 },
3265 ty,
3266 span,
3267 }
3268 }
3269 ExprKind::Is { value, pattern } => lower_is_ir(value, pattern, ty, span, cx),
3270 ExprKind::RecordSpread {
3271 base, overrides, ..
3272 } => lower_record_spread_ir(ty, span, base, overrides, cx),
3273 ExprKind::Expect(_) => todo!(
3274 "`expect` expression — test-body-only, gated by ctx.in_test_body \
3275 (bynk-check/src/checker.rs's check_body), unreachable through this pass's own \
3276 single-file checked_program/find_fn/lower_fn harness without first building or \
3277 routing through bynk-check's heavier project-level test machinery (P6.3, #1145, \
3278 Decision C)"
3279 ),
3280 ExprKind::Val { .. } => todo!(
3281 "`Val[T]` — test-body-only, same unreachable-through-this-harness gap as `expect` \
3282 above (P6.3, #1145, Decision C)"
3283 ),
3284 ExprKind::Wire(_) => todo!(
3285 "`Wire(...)` — test-body-only, same unreachable-through-this-harness gap as `expect` \
3286 above (P6.3, #1145, Decision C)"
3287 ),
3288 ExprKind::Observation(_) => todo!(
3289 "capability-call observation — test-body-only, same unreachable-through-this-harness \
3290 gap as `expect` above (P6.3, #1145, Decision C)"
3291 ),
3292 ExprKind::Trace { .. } => todo!(
3293 "`trace(...)` — test-body-only, same unreachable-through-this-harness gap as \
3294 `expect` above (P6.3, #1145, Decision C)"
3295 ),
3296 }
3297}
3298
3299fn lower_interp_part_ir(part: &InterpPart, cx: &mut LowerIrCtx) -> IrInterpPart {
3303 match part {
3304 InterpPart::Chunk(s) => IrInterpPart::Chunk(s.clone()),
3305 InterpPart::Hole(e) => IrInterpPart::Hole(Box::new(lower_expr_ir(e, cx))),
3306 }
3307}
3308
3309fn lower_call_ir(
3337 e: &Expr,
3338 receiver: Option<&Expr>,
3339 type_args: &[bynk_syntax::ast::TypeRef],
3340 args: &[Expr],
3341 cx: &mut LowerIrCtx,
3342) -> IrExpr {
3343 let ty = cx.expr_ty(e.id);
3344 let span = e.span;
3345 let Some(callee) = cx.callee(e.id).cloned() else {
3346 todo!(
3347 "no Callee recorded for this call at {span:?} — one of the shapes Decision C (#1143) \
3348 left out on purpose (HttpResult/QueueResult bare-variant construction, Events.emit, \
3349 the production is_system_http_service address), or a genuine, newly-discovered gap"
3350 )
3351 };
3352 if let Callee::Ctor { tag, .. } = callee {
3353 return IrExpr {
3371 kind: IrExprKind::Variant {
3372 tag,
3373 payload: args.iter().map(|a| lower_expr_ir(a, cx)).collect(),
3374 },
3375 ty,
3376 span,
3377 };
3378 }
3379 let receiver_is_a_value = matches!(
3380 callee,
3381 Callee::Method(_) | Callee::Kernel { .. } | Callee::Agent { .. }
3382 );
3383 let mut ir_args: Vec<IrExpr> = Vec::with_capacity(args.len() + receiver_is_a_value as usize);
3384 if receiver_is_a_value && let Some(r) = receiver {
3385 ir_args.push(lower_expr_ir(r, cx));
3386 }
3387 ir_args.extend(args.iter().map(|a| lower_expr_ir(a, cx)));
3388 let targs = type_args
3389 .iter()
3390 .map(|t| {
3391 cx.resolve_type_ref(t).unwrap_or_else(|| {
3392 panic!(
3393 "bynk internal error (ADR 0334): an explicit call-site type argument does \
3394 not resolve in this pass's own rigid-variable scope, but the checker \
3395 already accepted this call"
3396 )
3397 })
3398 })
3399 .collect();
3400 IrExpr {
3401 kind: IrExprKind::Call {
3402 callee,
3403 targs,
3404 args: ir_args,
3405 },
3406 ty,
3407 span,
3408 }
3409}
3410
3411fn lower_lambda_ir(e: &Expr, lambda: &LambdaExpr, cx: &mut LowerIrCtx) -> IrExpr {
3418 let ty = cx.expr_ty(e.id);
3419 let param_tys: Vec<TyId> = match &*cx.program.ty_intern.get(ty) {
3420 Ty::Fn { params, .. } => params.clone(),
3421 _ => panic!(
3422 "bynk internal error (ADR 0334): a Lambda's own recorded type is not Ty::Fn — \
3423 check_lambda only ever types one as a function type"
3424 ),
3425 };
3426 assert_eq!(
3427 lambda.params.len(),
3428 param_tys.len(),
3429 "bynk internal error (ADR 0334): a Lambda's own recorded Ty::Fn has a different arity \
3430 than its own AST params — bynk_lower and bynk-check disagree about this \
3431 lambda's shape"
3432 );
3433 cx.push_scope();
3434 for (p, pty) in lambda.params.iter().zip(¶m_tys) {
3435 cx.bind(p.name.name.clone(), *pty);
3436 }
3437 let body = lower_expr_ir(&lambda.body, cx);
3438 cx.pop_scope();
3439 IrExpr {
3440 kind: IrExprKind::Lambda {
3441 params: lambda.params.iter().map(|p| p.name.name.clone()).collect(),
3442 body: Box::new(body),
3443 captures: Vec::new(),
3448 },
3449 ty,
3450 span: e.span,
3451 }
3452}
3453
3454fn lower_ident_ir(name: &str, expr_id: Option<ExprId>, cx: &LowerIrCtx) -> IrExprKind {
3478 if cx.lookup(name).is_some() {
3479 return IrExprKind::Local(name.to_string());
3480 }
3481 if let Some(callee @ Callee::Intrinsic { .. }) = expr_id.and_then(|id| cx.callee(id)) {
3496 return IrExprKind::Call {
3497 callee: callee.clone(),
3498 targs: Vec::new(),
3499 args: Vec::new(),
3500 };
3501 }
3502 if cx.store_queryable.contains(name) {
3518 return IrExprKind::StoreQuery(name.to_string());
3519 }
3520 if cx.program.fns.contains_key(name) {
3521 todo!(
3522 "bare ident `{name}` names a free function used as a value — Callee/Lambda-adjacent, \
3523 P6.2 territory, not a Global reference"
3524 )
3525 }
3526 if nullary_variant_owner(name, cx).is_some() {
3527 return IrExprKind::Global(GlobalRef {
3528 tag: name.to_string(),
3529 });
3530 }
3531 todo!(
3532 "bare ident `{name}` is neither a locally-bound name, a free function, nor a bare \
3533 nullary sum-variant reference — one of lower_ident's other special cases (store field, \
3534 agent `self`, actor binder, transition `old`/`new`), structurally unreachable through \
3535 lower_fn_body_ir (see its own doc comment) but left unhandled here defensively"
3536 )
3537}
3538
3539fn nullary_variant_owner(name: &str, cx: &LowerIrCtx) -> Option<Arc<bynk_syntax::ast::TypeDecl>> {
3555 let mut owners = cx.program.types.values().filter(|t| {
3556 matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name && v.payload.is_empty()))
3557 });
3558 let owner = owners.next()?;
3559 if owners.next().is_some() {
3560 return None;
3561 }
3562 Some(Arc::clone(owner))
3563}
3564
3565fn named_decl(ty: TyId, cx: &LowerIrCtx) -> Arc<bynk_syntax::ast::TypeDecl> {
3569 let Ty::Named { name, .. } = &*cx.program.ty_intern.get(ty) else {
3570 panic!(
3571 "bynk internal error (ADR 0334): a RecordConstruction's own resolved type is not \
3572 Ty::Named — the checker only ever types one as its declaring record type"
3573 )
3574 };
3575 Arc::clone(cx.program.types.get(name).unwrap_or_else(|| {
3576 panic!(
3577 "bynk internal error (ADR 0334): `{name}` has no TypedCommons::types entry, but a \
3578 RecordConstruction just resolved to it"
3579 )
3580 }))
3581}
3582
3583fn lower_record_spread_ir(
3607 ty: TyId,
3608 span: Span,
3609 base: &Expr,
3610 overrides: &[FieldInit],
3611 cx: &mut LowerIrCtx,
3612) -> IrExpr {
3613 let def = named_decl(ty, cx);
3619 let base_args = match &*cx.program.ty_intern.get(ty) {
3620 Ty::Named { args, .. } => args.clone(),
3621 _ => unreachable!("named_decl above already panics on a non-Ty::Named `ty`"),
3622 };
3623 let TypeBody::Record(record_body) = &def.body else {
3624 panic!(
3625 "bynk internal error (ADR 0334): `{}` is a RecordSpread's own resolved record type, \
3626 but its declaration is not TypeBody::Record",
3627 def.name.name
3628 )
3629 };
3630 let declared: HashSet<&str> = record_body
3631 .fields
3632 .iter()
3633 .map(|f| f.name.name.as_str())
3634 .collect();
3635
3636 let base_ir = lower_expr_ir(base, cx);
3637 let base_ty = base_ir.ty;
3638 let base_span = base_ir.span;
3639 let tmp = cx.fresh_spread_tmp();
3640 let mut stmts = vec![IrStmt::Let {
3641 local: tmp.clone(),
3642 value: base_ir,
3643 }];
3644
3645 let overridden: Vec<(String, IrExpr)> = overrides
3653 .iter()
3654 .map(|f| {
3655 if !declared.contains(f.name.name.as_str()) {
3656 panic!(
3663 "bynk internal error (ADR 0334): record spread override `{}` names a field \
3664 `{}` does not declare, but the checker already accepted this spread",
3665 f.name.name, def.name.name
3666 )
3667 }
3668 let value = match &f.value {
3669 Some(v) => lower_expr_ir(v, cx),
3673 None => {
3674 let local_ty = cx.lookup(&f.name.name).unwrap_or_else(|| {
3675 panic!(
3676 "bynk internal error (ADR 0334): shorthand spread override `{}` has \
3677 no local binding in this pass's own scope — the checker accepted \
3678 it, so its own `ctx.lookup` must have found one",
3679 f.name.name
3680 )
3681 });
3682 IrExpr {
3683 kind: lower_ident_ir(&f.name.name, None, cx),
3684 ty: local_ty,
3685 span: f.name.span,
3686 }
3687 }
3688 };
3689 (f.name.name.clone(), value)
3690 })
3691 .collect();
3692
3693 let mut last_index: HashMap<String, usize> = HashMap::new();
3700 for (i, (name, _)) in overridden.iter().enumerate() {
3701 last_index.insert(name.clone(), i);
3702 }
3703 let mut fields: Vec<(String, IrExpr)> = Vec::with_capacity(record_body.fields.len());
3704 let mut overridden_names: HashSet<String> = HashSet::new();
3705 for (i, (name, value)) in overridden.into_iter().enumerate() {
3706 if last_index[&name] == i {
3707 overridden_names.insert(name.clone());
3708 fields.push((name, value));
3709 } else {
3710 stmts.push(IrStmt::Expr { value });
3711 }
3712 }
3713
3714 for decl_field in &record_body.fields {
3722 if overridden_names.contains(decl_field.name.name.as_str()) {
3723 continue;
3724 }
3725 let field_ty = checker::instantiate_field_ty(
3726 &def,
3727 &base_args,
3728 &decl_field.type_ref,
3729 &cx.program.types,
3730 &cx.program.ty_intern,
3731 )
3732 .unwrap_or_else(|| {
3733 panic!(
3734 "bynk internal error (ADR 0334): declared field `{}` of `{}` does not resolve \
3735 against this spread's own base type arguments, but the checker already \
3736 accepted this record spread",
3737 decl_field.name.name, def.name.name
3738 )
3739 });
3740 fields.push((
3741 decl_field.name.name.clone(),
3742 IrExpr {
3743 kind: IrExprKind::Field {
3744 base: Box::new(IrExpr {
3745 kind: IrExprKind::Local(tmp.clone()),
3746 ty: base_ty,
3747 span: base_span,
3748 }),
3749 field: decl_field.name.name.clone(),
3750 },
3751 ty: field_ty,
3752 span: decl_field.span,
3753 },
3754 ));
3755 }
3756
3757 IrExpr {
3758 kind: IrExprKind::Block {
3759 stmts,
3760 tail: Box::new(IrExpr {
3761 kind: IrExprKind::Record { fields },
3762 ty,
3763 span,
3764 }),
3765 },
3766 ty,
3767 span,
3768 }
3769}
3770
3771fn lower_pattern_ir(pattern: &Pattern, scrutinee_ty: TyId, program: &TypedCommons) -> IrPat {
3790 match pattern {
3791 Pattern::Wildcard(_) => IrPat::Wild,
3792 Pattern::Binding(id) => IrPat::Bind {
3793 local: id.name.clone(),
3794 },
3795 Pattern::Literal { value, .. } => IrPat::Const {
3796 value: match value {
3797 LiteralValue::Int(n) => ConstVal::Int(*n),
3798 LiteralValue::Str(s) => ConstVal::Str(s.clone()),
3799 LiteralValue::Bool(b) => ConstVal::Bool(*b),
3800 },
3801 },
3802 Pattern::Variant {
3803 variant, bindings, ..
3804 } => {
3805 let variant_info = variant_info_of(scrutinee_ty, &variant.name, program);
3806 let fields = bindings
3807 .iter()
3808 .enumerate()
3809 .map(|(idx, b)| match &b.kind {
3810 PatternBindingKind::Named { field, pattern } => {
3811 let field_ty = variant_info
3812 .payload
3813 .iter()
3814 .find(|(name, _)| name == &field.name)
3815 .map(|(_, ty)| *ty)
3816 .unwrap_or_else(|| {
3817 panic!(
3818 "bynk internal error (ADR 0334): named pattern field `{}` \
3819 does not resolve against variant `{}`'s own payload, but \
3820 the checker already accepted this pattern",
3821 field.name, variant.name
3822 )
3823 });
3824 (
3825 field.name.clone(),
3826 Box::new(lower_pattern_ir(pattern, field_ty, program)),
3827 )
3828 }
3829 PatternBindingKind::Positional { pattern } => {
3830 let (name, field_ty) =
3831 variant_info.payload.get(idx).cloned().unwrap_or_else(|| {
3832 panic!(
3833 "bynk internal error (ADR 0334): positional pattern binding \
3834 {idx} has no matching payload field on variant `{}`, but \
3835 the checker already accepted this pattern's arity",
3836 variant.name
3837 )
3838 });
3839 (name, Box::new(lower_pattern_ir(pattern, field_ty, program)))
3840 }
3841 })
3842 .collect();
3843 IrPat::Variant {
3844 scrutinee_ty,
3845 tag: variant.name.clone(),
3846 fields,
3847 }
3848 }
3849 Pattern::Refined {
3850 inner, predicate, ..
3851 } => IrPat::Refined {
3852 inner: Box::new(lower_pattern_ir(inner, scrutinee_ty, program)),
3853 refinement: predicate.clone(),
3854 },
3855 Pattern::Or(alts, _) => IrPat::Or {
3856 alts: alts
3857 .iter()
3858 .map(|p| lower_pattern_ir(p, scrutinee_ty, program))
3859 .collect(),
3860 },
3861 }
3862}
3863
3864fn variant_info_of(scrutinee_ty: TyId, tag: &str, program: &TypedCommons) -> checker::VariantInfo {
3872 checker::variants_of(scrutinee_ty, &program.types, program.tys())
3873 .unwrap_or_else(|| {
3874 panic!(
3875 "bynk internal error (ADR 0334): variant pattern `{tag}` matches against a \
3876 non-variant-kind scrutinee, but the checker already accepted this pattern"
3877 )
3878 })
3879 .into_iter()
3880 .find(|v| v.name == tag)
3881 .unwrap_or_else(|| {
3882 panic!(
3883 "bynk internal error (ADR 0334): scrutinee has no variant `{tag}`, but the \
3884 checker already accepted this pattern"
3885 )
3886 })
3887}
3888
3889fn collect_pattern_binding_tys(
3900 pattern: &Pattern,
3901 ty: TyId,
3902 program: &TypedCommons,
3903 out: &mut Vec<(String, TyId)>,
3904) {
3905 match pattern {
3906 Pattern::Wildcard(_) | Pattern::Literal { .. } => {}
3907 Pattern::Binding(id) => out.push((id.name.clone(), ty)),
3908 Pattern::Variant {
3909 variant, bindings, ..
3910 } => {
3911 let variant_info = variant_info_of(ty, &variant.name, program);
3912 for (idx, b) in bindings.iter().enumerate() {
3917 match &b.kind {
3918 PatternBindingKind::Named { field, pattern } => {
3919 let field_ty = variant_info
3920 .payload
3921 .iter()
3922 .find(|(name, _)| name == &field.name)
3923 .map(|(_, ty)| *ty)
3924 .unwrap_or_else(|| {
3925 panic!(
3926 "bynk internal error (ADR 0334): named pattern field `{}` \
3927 does not resolve against variant `{}`'s own payload, but \
3928 the checker already accepted this pattern",
3929 field.name, variant.name
3930 )
3931 });
3932 collect_pattern_binding_tys(pattern, field_ty, program, out);
3933 }
3934 PatternBindingKind::Positional { pattern } => {
3935 let (_, field_ty) =
3936 variant_info.payload.get(idx).cloned().unwrap_or_else(|| {
3937 panic!(
3938 "bynk internal error (ADR 0334): positional pattern binding \
3939 {idx} has no matching payload field on variant `{}`, but \
3940 the checker already accepted this pattern's arity",
3941 variant.name
3942 )
3943 });
3944 collect_pattern_binding_tys(pattern, field_ty, program, out);
3945 }
3946 }
3947 }
3948 }
3949 Pattern::Refined { inner, .. } => collect_pattern_binding_tys(inner, ty, program, out),
3950 Pattern::Or(alts, _) => {
3955 if let Some(first) = alts.first() {
3956 collect_pattern_binding_tys(first, ty, program, out);
3957 }
3958 }
3959 }
3960}
3961
3962fn ir_pat_contains_or(pat: &IrPat) -> bool {
3966 match pat {
3967 IrPat::Wild | IrPat::Bind { .. } | IrPat::Const { .. } => false,
3968 IrPat::Variant { fields, .. } => fields.iter().any(|(_, p)| ir_pat_contains_or(p)),
3969 IrPat::Refined { inner, .. } => ir_pat_contains_or(inner),
3970 IrPat::Or { .. } => true,
3971 }
3972}
3973
3974fn lower_arm_ir(arm: &MatchArm, scrutinee_ty: TyId, cx: &mut LowerIrCtx) -> IrArm {
3985 let pat = lower_pattern_ir(&arm.pattern, scrutinee_ty, cx.program);
3986 let binds: Vec<String> = arm
3987 .pattern
3988 .bound_names()
3989 .into_iter()
3990 .map(|id| id.name.clone())
3991 .collect();
3992
3993 let mut bind_tys = Vec::new();
3994 collect_pattern_binding_tys(&arm.pattern, scrutinee_ty, cx.program, &mut bind_tys);
3995
3996 cx.push_scope();
3997 for (name, ty) in bind_tys {
3998 cx.bind(name, ty);
3999 }
4000 let guard = arm.guard.as_ref().map(|g| lower_expr_ir(g, cx));
4001 let body = match &arm.body {
4002 MatchBody::Expr(e) => lower_expr_ir(e, cx),
4003 MatchBody::Block(b) => lower_block_ir(b, cx),
4004 };
4005 cx.pop_scope();
4006
4007 let binding_mode = if ir_pat_contains_or(&pat) {
4008 BindingMode::OrDispatch
4009 } else {
4010 BindingMode::Direct
4011 };
4012
4013 IrArm {
4014 pat,
4015 guard,
4016 body,
4017 binds,
4018 binding_mode,
4019 }
4020}
4021
4022fn lower_exhaustive_ir(arms: &[MatchArm]) -> Exhaustive {
4037 if arms.iter().any(|a| a.guard.is_none()) {
4038 Exhaustive::Total
4039 } else {
4040 unreachable!(
4041 "bynk internal error (ADR 0334, extended by Decision B / #1157): a certified \
4042 program's match is never empty (the parser itself already requires at least one \
4043 arm) and always has at least one unguarded arm — bynk-check's own missing_patterns \
4044 gate (bynk.types.non_exhaustive_match, error-severity, rejected by certify per \
4045 R3.10) guarantees it"
4046 )
4047 }
4048}
4049
4050#[cfg(test)]
4056mod tests {
4057 use super::*;
4058 use bynk_check::builtin_names::types::QUEUE_RESULT;
4059 use bynk_check::checker::CheckedProgram;
4060 use bynk_check::hints::HintSink;
4061 use bynk_check::index::RefSink;
4062 use bynk_check::locals::LocalsSink;
4063 use bynk_check::requirements::RequirementSink;
4064 use bynk_check::{checker, context_checks, resolver, symbols};
4065 use bynk_project::UnitKind;
4066 use bynk_syntax::ast::PredKind;
4067 use bynk_syntax::ast::{Commons, CommonsItem, FnDecl, SourceUnit};
4068 use bynk_syntax::{lexer, parser};
4069
4070 fn checked_program(source: &str) -> CheckedProgram {
4071 let tokens = lexer::tokenize(source).expect("lex");
4072 let (commons, warnings) = parser::parse_with_warnings(&tokens, source).expect("parse");
4073 let resolved = resolver::resolve(commons).expect("resolve");
4074 let typed = checker::check(resolved).expect("check");
4075 checker::certify(typed, warnings).expect("certify")
4076 }
4077
4078 fn find_fn<'a>(program: &'a CheckedProgram, name: &str) -> &'a FnDecl {
4079 program
4080 .program()
4081 .commons
4082 .items
4083 .iter()
4084 .find_map(|item| match item {
4085 CommonsItem::Fn(f) if f.name.display() == name => Some(f),
4086 _ => None,
4087 })
4088 .unwrap_or_else(|| panic!("no fn named `{name}` in this fixture"))
4089 }
4090
4091 fn lower_fn(program: &CheckedProgram, name: &str) -> IrExpr {
4092 let f = find_fn(program, name);
4093 lower_fn_body_ir(f, program)
4094 }
4095
4096 fn find_type<'a>(program: &'a CheckedProgram, name: &str) -> &'a Arc<TypeDecl> {
4097 program
4098 .program()
4099 .types
4100 .get(name)
4101 .unwrap_or_else(|| panic!("no type named `{name}` in this fixture"))
4102 }
4103
4104 fn find_fn_arc<'a>(program: &'a CheckedProgram, name: &str) -> &'a Arc<FnDecl> {
4109 if let Some((type_name, method_name)) = name.split_once('.') {
4110 let table = program.program().methods.get(type_name).unwrap_or_else(|| {
4111 panic!("no method table for type `{type_name}` in this fixture")
4112 });
4113 return table
4114 .instance
4115 .get(method_name)
4116 .or_else(|| table.statics.get(method_name))
4117 .unwrap_or_else(|| panic!("no method named `{name}` in this fixture"));
4118 }
4119 program
4120 .program()
4121 .fns
4122 .get(name)
4123 .unwrap_or_else(|| panic!("no fn named `{name}` in this fixture"))
4124 }
4125
4126 fn fn_tail(ir: &IrExpr) -> &IrExpr {
4129 let IrExprKind::Block { stmts, tail } = &ir.kind else {
4130 panic!(
4131 "lower_fn_body_ir always returns IrExprKind::Block, got {:?}",
4132 ir.kind
4133 )
4134 };
4135 assert!(
4136 stmts.is_empty(),
4137 "this helper is for single-tail bodies only"
4138 );
4139 let IrExprKind::Return { value } = &tail.kind else {
4140 panic!(
4141 "a fn body's own tail is always wrapped in Return, got {:?}",
4142 tail.kind
4143 )
4144 };
4145 value
4146 }
4147
4148 #[test]
4149 fn const_covers_every_bynk_literal_form() {
4150 let program = checked_program(
4151 r#"
4152commons demo {
4153 fn int_lit() -> Int { 1 }
4154 fn float_lit() -> Float { 1.5 }
4155 fn duration_lit() -> Duration { 5.minutes }
4156 fn str_lit() -> String { "hi" }
4157 fn bool_lit() -> Bool { true }
4158 fn unit_lit() -> () { () }
4159}
4160"#,
4161 );
4162 let cases: &[(&str, ConstVal)] = &[
4163 ("int_lit", ConstVal::Int(1)),
4164 ("float_lit", ConstVal::Float(1.5)),
4165 ("duration_lit", ConstVal::DurationMillis(5 * 60 * 1000)),
4166 ("str_lit", ConstVal::Str("hi".to_string())),
4167 ("bool_lit", ConstVal::Bool(true)),
4168 ("unit_lit", ConstVal::Unit),
4169 ];
4170 for (fn_name, expected) in cases {
4171 let ir = lower_fn(&program, fn_name);
4172 let tail = fn_tail(&ir);
4173 let IrExprKind::Const(actual) = &tail.kind else {
4174 panic!("{fn_name}: expected Const, got {:?}", tail.kind)
4175 };
4176 assert_eq!(actual, expected, "{fn_name}");
4177 }
4178 }
4179
4180 #[test]
4181 fn local_reads_a_bound_param() {
4182 let program = checked_program(
4183 r#"
4184commons demo {
4185 fn identity(n: Int) -> Int { n }
4186}
4187"#,
4188 );
4189 let ir = lower_fn(&program, "identity");
4190 let tail = fn_tail(&ir);
4191 assert!(matches!(&tail.kind, IrExprKind::Local(name) if name == "n"));
4192 }
4193
4194 #[test]
4195 fn generic_fn_type_parameters_are_rigid_variables_not_unresolvable_declared_types() {
4196 let program = checked_program(
4203 r#"
4204commons demo {
4205 fn identity[T](x: T) -> T { x }
4206}
4207"#,
4208 );
4209 let ir = lower_fn(&program, "identity");
4210 let tail = fn_tail(&ir);
4211 assert!(matches!(&tail.kind, IrExprKind::Local(name) if name == "x"));
4212 }
4213
4214 #[test]
4215 fn global_covers_a_bare_nullary_sum_variant() {
4216 let program = checked_program(
4217 r#"
4218commons demo {
4219 type Outcome =
4220 | Hit(score: Int)
4221 | Miss
4222
4223 fn make() -> Outcome { Miss }
4224}
4225"#,
4226 );
4227 let ir = lower_fn(&program, "make");
4228 let tail = fn_tail(&ir);
4229 let IrExprKind::Global(g) = &tail.kind else {
4230 panic!("expected Global, got {:?}", tail.kind)
4231 };
4232 assert_eq!(g.tag, "Miss");
4233 }
4234
4235 #[test]
4236 #[should_panic(expected = "Callee/Lambda-adjacent")]
4237 fn bare_free_function_reference_is_excluded_from_the_global_probe() {
4238 let program = checked_program(
4244 r#"
4245commons demo {
4246 fn double(n: Int) -> Int { n * 2 }
4247
4248 fn get_double() -> (Int) -> Int { double }
4249}
4250"#,
4251 );
4252 let _ = lower_fn(&program, "get_double");
4253 }
4254
4255 #[test]
4256 fn record_construction_covers_explicit_and_shorthand_fields() {
4257 let program = checked_program(
4258 r#"
4259commons demo {
4260 type Point = { x: Int, y: Int }
4261
4262 fn explicit() -> Point { Point { x: 1, y: 2 } }
4263 fn shorthand(x: Int, y: Int) -> Point { Point { x, y } }
4264}
4265"#,
4266 );
4267 let explicit_ir = lower_fn(&program, "explicit");
4268 let explicit_tail = fn_tail(&explicit_ir);
4269 let IrExprKind::Record {
4270 fields: explicit_fields,
4271 } = &explicit_tail.kind
4272 else {
4273 panic!("explicit: expected Record, got {:?}", explicit_tail.kind)
4274 };
4275 assert_eq!(explicit_fields.len(), 2);
4276 assert_eq!(explicit_fields[0].0, "x");
4277 assert!(matches!(
4278 &explicit_fields[0].1.kind,
4279 IrExprKind::Const(ConstVal::Int(1))
4280 ));
4281 assert_eq!(explicit_fields[1].0, "y");
4282 assert!(matches!(
4283 &explicit_fields[1].1.kind,
4284 IrExprKind::Const(ConstVal::Int(2))
4285 ));
4286
4287 let shorthand_ir = lower_fn(&program, "shorthand");
4288 let shorthand_tail = fn_tail(&shorthand_ir);
4289 let IrExprKind::Record {
4290 fields: shorthand_fields,
4291 } = &shorthand_tail.kind
4292 else {
4293 panic!("shorthand: expected Record, got {:?}", shorthand_tail.kind)
4294 };
4295 assert_eq!(shorthand_fields.len(), 2);
4296 assert_eq!(shorthand_fields[0].0, "x");
4297 assert!(matches!(&shorthand_fields[0].1.kind, IrExprKind::Local(n) if n == "x"));
4298 assert_eq!(shorthand_fields[1].0, "y");
4299 assert!(matches!(&shorthand_fields[1].1.kind, IrExprKind::Local(n) if n == "y"));
4300 assert!(matches!(
4305 &*program.program().ty_intern.get(shorthand_fields[0].1.ty),
4306 Ty::Base(bynk_syntax::ast::BaseType::Int)
4307 ));
4308 assert!(matches!(
4309 &*program.program().ty_intern.get(shorthand_fields[1].1.ty),
4310 Ty::Base(bynk_syntax::ast::BaseType::Int)
4311 ));
4312 }
4313
4314 #[test]
4315 fn field_access_reads_a_record_field() {
4316 let program = checked_program(
4317 r#"
4318commons demo {
4319 type Point = { x: Int, y: Int }
4320
4321 fn get_x(p: Point) -> Int { p.x }
4322}
4323"#,
4324 );
4325 let ir = lower_fn(&program, "get_x");
4326 let tail = fn_tail(&ir);
4327 let IrExprKind::Field { base, field } = &tail.kind else {
4328 panic!("expected Field, got {:?}", tail.kind)
4329 };
4330 assert_eq!(field, "x");
4331 assert!(matches!(&base.kind, IrExprKind::Local(name) if name == "p"));
4332 }
4333
4334 #[test]
4335 fn list_literal_lowers_every_element() {
4336 let program = checked_program(
4337 r#"
4338commons demo {
4339 fn make() -> List[Int] { [1, 2, 3] }
4340}
4341"#,
4342 );
4343 let ir = lower_fn(&program, "make");
4344 let tail = fn_tail(&ir);
4345 let IrExprKind::List { elems } = &tail.kind else {
4346 panic!("expected List, got {:?}", tail.kind)
4347 };
4348 assert_eq!(elems.len(), 3);
4349 assert!(matches!(
4350 &elems[0].kind,
4351 IrExprKind::Const(ConstVal::Int(1))
4352 ));
4353 }
4354
4355 #[test]
4356 fn if_lowers_both_branches_as_blocks_not_return_wrapped() {
4357 let program = checked_program(
4368 r#"
4369commons demo {
4370 fn choose(b: Bool) -> Int {
4371 if b { 1 } else { 2 }
4372 }
4373}
4374"#,
4375 );
4376 let ir = lower_fn(&program, "choose");
4377 let tail = fn_tail(&ir);
4378 let IrExprKind::If { cond, then_, else_ } = &tail.kind else {
4379 panic!("expected If, got {:?}", tail.kind)
4380 };
4381 assert!(matches!(&cond.kind, IrExprKind::Local(name) if name == "b"));
4382 let IrExprKind::Block {
4383 tail: then_tail, ..
4384 } = &then_.kind
4385 else {
4386 panic!("expected then_ to be a Block, got {:?}", then_.kind)
4387 };
4388 assert!(matches!(
4389 &then_tail.kind,
4390 IrExprKind::Const(ConstVal::Int(1))
4391 ));
4392 let IrExprKind::Block {
4393 tail: else_tail, ..
4394 } = &else_.kind
4395 else {
4396 panic!("expected else_ to be a Block, got {:?}", else_.kind)
4397 };
4398 assert!(matches!(
4399 &else_tail.kind,
4400 IrExprKind::Const(ConstVal::Int(2))
4401 ));
4402 }
4403
4404 #[test]
4405 fn and_or_not_are_real_tree_nodes() {
4406 let program = checked_program(
4407 r#"
4408commons demo {
4409 fn conj(a: Bool, b: Bool) -> Bool { a && b }
4410 fn disj(a: Bool, b: Bool) -> Bool { a || b }
4411 fn negate(a: Bool) -> Bool { !a }
4412}
4413"#,
4414 );
4415 let and_ir = lower_fn(&program, "conj");
4416 assert!(matches!(fn_tail(&and_ir).kind, IrExprKind::And { .. }));
4417 let or_ir = lower_fn(&program, "disj");
4418 assert!(matches!(fn_tail(&or_ir).kind, IrExprKind::Or { .. }));
4419 let not_ir = lower_fn(&program, "negate");
4420 assert!(matches!(fn_tail(¬_ir).kind, IrExprKind::Not { .. }));
4421 }
4422
4423 #[test]
4424 fn pure_wraps_a_synchronous_value_as_effect() {
4425 let program = checked_program(
4426 r#"
4427commons demo {
4428 fn make() -> Effect[Int] { Effect.pure(1) }
4429}
4430"#,
4431 );
4432 let ir = lower_fn(&program, "make");
4433 let tail = fn_tail(&ir);
4434 let IrExprKind::Pure { value } = &tail.kind else {
4435 panic!("expected Pure, got {:?}", tail.kind)
4436 };
4437 assert!(matches!(&value.kind, IrExprKind::Const(ConstVal::Int(1))));
4438 }
4439
4440 #[test]
4441 fn await_peels_effect_from_an_effect_let_binding() {
4442 let program = checked_program(
4443 r#"
4444commons demo {
4445 fn use_it() -> Effect[Int] {
4446 let x <- Effect.pure(1)
4447 Effect.pure(x)
4448 }
4449}
4450"#,
4451 );
4452 let f = find_fn(&program, "use_it");
4453 let ir = lower_fn_body_ir(f, &program);
4454 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4455 panic!("expected Block")
4456 };
4457 assert_eq!(stmts.len(), 1);
4458 let IrStmt::Let { local, value } = &stmts[0] else {
4459 panic!("expected Let, got {:?}", stmts[0])
4460 };
4461 assert_eq!(local, "x");
4462 assert!(matches!(&value.kind, IrExprKind::Await { .. }));
4463 }
4464
4465 #[test]
4466 fn let_annotation_widens_the_bound_scope_type_not_just_the_rhs_expression() {
4467 let program = checked_program(
4475 r#"
4476commons demo {
4477 type Reps = Int where InRange(1, 100)
4478 type Wrapper = { n: Int }
4479
4480 fn make(p: Reps) -> Wrapper {
4481 let n: Int = p
4482 Wrapper { n }
4483 }
4484}
4485"#,
4486 );
4487 let f = find_fn(&program, "make");
4488 let ir = lower_fn_body_ir(f, &program);
4489 let IrExprKind::Block { stmts, tail } = &ir.kind else {
4490 panic!("expected Block")
4491 };
4492 let IrStmt::Let { local, value } = &stmts[0] else {
4493 panic!("expected Let, got {:?}", stmts[0])
4494 };
4495 assert_eq!(local, "n");
4496 assert!(matches!(
4497 &*program.program().ty_intern.get(value.ty),
4498 Ty::Named {
4499 kind: bynk_check::checker::NamedKind::Refined(_),
4500 ..
4501 }
4502 ));
4503 let IrExprKind::Return { value: wrapper } = &tail.kind else {
4504 panic!("expected Return, got {:?}", tail.kind)
4505 };
4506 let IrExprKind::Record { fields, .. } = &wrapper.kind else {
4507 panic!("expected Record, got {:?}", wrapper.kind)
4508 };
4509 assert_eq!(fields[0].0, "n");
4510 assert!(matches!(&fields[0].1.kind, IrExprKind::Local(name) if name == "n"));
4511 assert!(matches!(
4512 &*program.program().ty_intern.get(fields[0].1.ty),
4513 Ty::Base(bynk_syntax::ast::BaseType::Int)
4514 ));
4515 }
4516
4517 #[test]
4518 fn do_statement_lowers_to_a_discarded_await() {
4519 let program = checked_program(
4520 r#"
4521commons demo {
4522 fn use_it() -> Effect[()] {
4523 do Effect.pure(())
4524 Effect.pure(())
4525 }
4526}
4527"#,
4528 );
4529 let f = find_fn(&program, "use_it");
4530 let ir = lower_fn_body_ir(f, &program);
4531 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4532 panic!("expected Block")
4533 };
4534 assert_eq!(stmts.len(), 1);
4535 let IrStmt::Expr { value } = &stmts[0] else {
4536 panic!("expected Expr, got {:?}", stmts[0])
4537 };
4538 assert!(matches!(&value.kind, IrExprKind::Await { .. }));
4539 }
4540
4541 #[test]
4542 fn send_statement_lowers_to_a_fire_and_forget_send_typed_unit() {
4543 let program = checked_program(
4544 r#"
4545commons demo {
4546 fn use_it() -> Effect[()] {
4547 ~> Effect.pure(())
4548 Effect.pure(())
4549 }
4550}
4551"#,
4552 );
4553 let f = find_fn(&program, "use_it");
4554 let ir = lower_fn_body_ir(f, &program);
4555 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4556 panic!("expected Block")
4557 };
4558 assert_eq!(stmts.len(), 1);
4559 let IrStmt::Expr { value } = &stmts[0] else {
4560 panic!("expected Expr, got {:?}", stmts[0])
4561 };
4562 assert!(matches!(&value.kind, IrExprKind::Send { .. }));
4563 assert!(matches!(
4564 &*program.program().ty_intern.get(value.ty),
4565 bynk_check::checker::Ty::Unit
4566 ));
4567 }
4568
4569 #[test]
4582 fn call_driven_by_callee_fn_lowers_a_free_function_call() {
4583 let program = checked_program(
4584 r#"
4585commons demo {
4586 fn double(n: Int) -> Int { n }
4587
4588 fn use_it() -> Int { double(1) }
4589}
4590"#,
4591 );
4592 let ir = lower_fn(&program, "use_it");
4593 let tail = fn_tail(&ir);
4594 let IrExprKind::Call {
4595 callee,
4596 targs,
4597 args,
4598 } = &tail.kind
4599 else {
4600 panic!("expected Call, got {:?}", tail.kind)
4601 };
4602 assert!(matches!(callee, Callee::Fn(f) if f.name.display() == "double"));
4603 assert!(targs.is_empty());
4604 assert_eq!(args.len(), 1);
4605 assert!(matches!(&args[0].kind, IrExprKind::Const(ConstVal::Int(1))));
4606 }
4607
4608 #[test]
4609 fn call_with_an_explicit_type_argument_resolves_targs() {
4610 let program = checked_program(
4611 r#"
4612commons demo {
4613 fn identity[T](x: T) -> T { x }
4614
4615 fn use_it() -> Int { identity[Int](1) }
4616}
4617"#,
4618 );
4619 let ir = lower_fn(&program, "use_it");
4620 let tail = fn_tail(&ir);
4621 let IrExprKind::Call { callee, targs, .. } = &tail.kind else {
4622 panic!("expected Call, got {:?}", tail.kind)
4623 };
4624 assert!(matches!(callee, Callee::Fn(f) if f.name.display() == "identity"));
4625 assert_eq!(targs.len(), 1);
4626 assert!(matches!(
4627 &*program.program().ty_intern.get(targs[0]),
4628 Ty::Base(bynk_syntax::ast::BaseType::Int)
4629 ));
4630 }
4631
4632 #[test]
4633 fn call_with_an_explicit_type_argument_naming_the_enclosing_fns_own_rigid_var() {
4634 let program = checked_program(
4640 r#"
4641commons demo {
4642 fn identity[T](x: T) -> T { x }
4643
4644 fn wrap[U](x: U) -> U { identity[U](x) }
4645}
4646"#,
4647 );
4648 let ir = lower_fn(&program, "wrap");
4649 let tail = fn_tail(&ir);
4650 let IrExprKind::Call { callee, targs, .. } = &tail.kind else {
4651 panic!("expected Call, got {:?}", tail.kind)
4652 };
4653 assert!(matches!(callee, Callee::Fn(f) if f.name.display() == "identity"));
4654 assert_eq!(targs.len(), 1);
4655 assert!(matches!(
4656 &*program.program().ty_intern.get(targs[0]),
4657 Ty::Var(name) if name == "U"
4658 ));
4659 }
4660
4661 #[test]
4662 fn call_driven_by_callee_value_applies_a_function_typed_local() {
4663 let program = checked_program(
4664 r#"
4665commons demo {
4666 fn apply(f: (Int) -> Int, x: Int) -> Int { f(x) }
4667}
4668"#,
4669 );
4670 let ir = lower_fn(&program, "apply");
4671 let tail = fn_tail(&ir);
4672 let IrExprKind::Call { callee, args, .. } = &tail.kind else {
4673 panic!("expected Call, got {:?}", tail.kind)
4674 };
4675 assert!(matches!(callee, Callee::Value(name) if name == "f"));
4676 assert_eq!(args.len(), 1);
4677 assert!(matches!(&args[0].kind, IrExprKind::Local(n) if n == "x"));
4678 }
4679
4680 #[test]
4681 fn bare_and_qualified_variant_construction_both_lower_to_variant() {
4682 let program = checked_program(
4683 r#"
4684commons demo {
4685 type Shape =
4686 | Circle(radius: Int)
4687 | Square(side: Int)
4688
4689 fn bare(n: Int) -> Shape { Circle(n) }
4690 fn qualified(n: Int) -> Shape { Shape.Circle(n) }
4691}
4692"#,
4693 );
4694 for fn_name in ["bare", "qualified"] {
4695 let ir = lower_fn(&program, fn_name);
4696 let tail = fn_tail(&ir);
4697 let IrExprKind::Variant { tag, payload } = &tail.kind else {
4698 panic!("{fn_name}: expected Variant, got {:?}", tail.kind)
4699 };
4700 assert!(
4703 matches!(
4704 &*program.program().ty_intern.get(tail.ty),
4705 Ty::Named { name, .. } if name == "Shape"
4706 ),
4707 "{fn_name}: expected tail.ty to resolve to the Shape sum, got {:?}",
4708 program.program().ty_intern.get(tail.ty)
4709 );
4710 assert_eq!(tag, "Circle");
4711 assert_eq!(payload.len(), 1);
4712 assert!(matches!(&payload[0].kind, IrExprKind::Local(n) if n == "n"));
4713 }
4714 }
4715
4716 #[test]
4737 fn ok_err_some_none_all_lower_to_variant_by_tag_and_carry_their_sum_identity_on_ty() {
4738 let program = checked_program(
4739 r#"
4740commons demo {
4741 fn ok_case() -> Result[Int, String] { Ok(1) }
4742 fn err_case() -> Result[Int, String] { Err("bad") }
4743 fn some_case() -> Option[Int] { Some(1) }
4744 fn none_case() -> Option[Int] { None }
4745 fn ok_http_case() -> HttpResult[Int] { Ok(1) }
4746}
4747"#,
4748 );
4749 for (fn_name, expect_tag, expect_payload_len) in [
4750 ("ok_case", "Ok", 1),
4751 ("err_case", "Err", 1),
4752 ("some_case", "Some", 1),
4753 ("none_case", "None", 0),
4754 ("ok_http_case", "Ok", 1),
4755 ] {
4756 let ir = lower_fn(&program, fn_name);
4757 let tail = fn_tail(&ir);
4758 let IrExprKind::Variant { tag, payload } = &tail.kind else {
4759 panic!("{fn_name}: expected Variant, got {:?}", tail.kind)
4760 };
4761 assert_eq!(tag, expect_tag, "{fn_name}");
4762 assert_eq!(payload.len(), expect_payload_len, "{fn_name}");
4763 let resolved = &*program.program().ty_intern.get(tail.ty);
4764 let sum_matches = match fn_name {
4765 "ok_case" | "err_case" => matches!(resolved, Ty::Result(..)),
4766 "some_case" | "none_case" => matches!(resolved, Ty::Option(_)),
4767 "ok_http_case" => matches!(resolved, Ty::HttpResult(_)),
4768 _ => unreachable!(),
4769 };
4770 assert!(
4771 sum_matches,
4772 "{fn_name}: expected tail.ty to resolve to the constructed sum, got {resolved:?}"
4773 );
4774 }
4775 }
4776
4777 #[test]
4778 fn question_on_option_lowers_to_a_some_none_match_and_none_returns_http_result_not_found() {
4779 let program = checked_program(
4780 r#"
4781commons demo {
4782 fn maybe() -> Option[Int] { Some(1) }
4783 fn lift() -> HttpResult[Int] {
4784 let v = maybe()?
4785 Ok(v)
4786 }
4787}
4788"#,
4789 );
4790 let ir = lower_fn(&program, "lift");
4791 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4792 panic!("expected Block, got {:?}", ir.kind)
4793 };
4794 let IrStmt::Let { value, .. } = &stmts[0] else {
4795 panic!(
4796 "expected the first statement to be a Let, got {:?}",
4797 stmts[0]
4798 )
4799 };
4800 let IrExprKind::Match {
4801 scrutinee,
4802 arms,
4803 exhaustive,
4804 form,
4805 } = &value.kind
4806 else {
4807 panic!("expected Match, got {:?}", value.kind)
4808 };
4809 assert!(matches!(&scrutinee.kind, IrExprKind::Call { .. }));
4810 assert!(matches!(
4811 &*program.program().ty_intern.get(scrutinee.ty),
4812 Ty::Option(_)
4813 ));
4814 assert_eq!(arms.len(), 2);
4815 let IrPat::Variant {
4816 tag: some_tag,
4817 fields: some_fields,
4818 ..
4819 } = &arms[0].pat
4820 else {
4821 panic!("expected arm 0's pat to be Variant, got {:?}", arms[0].pat)
4822 };
4823 assert_eq!(some_tag, "Some");
4824 assert_eq!(some_fields.len(), 1);
4825 assert!(matches!(&arms[0].body.kind, IrExprKind::Local(_)));
4826 let IrPat::Variant {
4827 tag: none_tag,
4828 fields: none_fields,
4829 ..
4830 } = &arms[1].pat
4831 else {
4832 panic!("expected arm 1's pat to be Variant, got {:?}", arms[1].pat)
4833 };
4834 assert_eq!(none_tag, "None");
4835 assert!(none_fields.is_empty());
4836 let IrExprKind::Return { value: returned } = &arms[1].body.kind else {
4837 panic!(
4838 "expected arm 1's body to be Return, got {:?}",
4839 arms[1].body.kind
4840 )
4841 };
4842 assert!(matches!(returned.kind, IrExprKind::HttpResultNotFound));
4843 assert!(matches!(exhaustive, Exhaustive::Total));
4844 assert_eq!(*form, MatchForm::Flat);
4845 }
4846
4847 #[test]
4848 fn question_on_result_with_a_compatible_error_type_propagates_the_scrutinee_unchanged() {
4849 let program = checked_program(
4850 r#"
4851commons demo {
4852 fn g() -> Result[Int, String] { Ok(1) }
4853 fn bare_case() -> Result[Int, String] {
4854 let v = g()?
4855 Ok(v)
4856 }
4857}
4858"#,
4859 );
4860 let ir = lower_fn(&program, "bare_case");
4861 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4862 panic!("expected Block, got {:?}", ir.kind)
4863 };
4864 let IrStmt::Let { value, .. } = &stmts[0] else {
4865 panic!(
4866 "expected the first statement to be a Let, got {:?}",
4867 stmts[0]
4868 )
4869 };
4870 let IrExprKind::Match { arms, .. } = &value.kind else {
4871 panic!("expected Match, got {:?}", value.kind)
4872 };
4873 let IrPat::Variant { tag: ok_tag, .. } = &arms[0].pat else {
4874 panic!("expected arm 0's pat to be Variant, got {:?}", arms[0].pat)
4875 };
4876 assert_eq!(ok_tag, "Ok");
4877 let IrPat::Variant {
4878 tag: err_tag,
4879 fields: err_fields,
4880 ..
4881 } = &arms[1].pat
4882 else {
4883 panic!("expected arm 1's pat to be Variant, got {:?}", arms[1].pat)
4884 };
4885 assert_eq!(err_tag, "Err");
4886 assert_eq!(err_fields.len(), 1);
4887 let IrExprKind::Return { value: returned } = &arms[1].body.kind else {
4888 panic!(
4889 "expected arm 1's body to be Return, got {:?}",
4890 arms[1].body.kind
4891 )
4892 };
4893 let IrExprKind::Variant {
4894 tag: returned_tag,
4895 payload: returned_payload,
4896 } = &returned.kind
4897 else {
4898 panic!("expected a re-constructed Err, got {:?}", returned.kind)
4899 };
4900 assert_eq!(returned_tag, "Err");
4901 assert_eq!(returned_payload.len(), 1);
4902 let IrPat::Bind { local: bound_name } = &*err_fields[0].1 else {
4903 panic!(
4904 "expected arm 1's own payload pattern to be a Bind, got {:?}",
4905 err_fields[0].1
4906 )
4907 };
4908 assert!(matches!(&returned_payload[0].kind, IrExprKind::Local(n) if n == bound_name));
4911 }
4912
4913 #[test]
4914 fn question_on_result_with_a_declared_embeds_conversion_wraps_the_propagated_error() {
4915 let program = checked_program(
4916 r#"
4917commons demo {
4918 type PaymentError = enum { Declined, InsufficientFunds }
4919
4920 type OrderError =
4921 | OutOfStock(sku: String, qty: Int)
4922 | Payment(reason: PaymentError)
4923 embeds PaymentError as Payment
4924
4925 fn charge() -> Result[Int, PaymentError] { Ok(1) }
4926 fn embed_case() -> Result[Int, OrderError] {
4927 let v = charge()?
4928 Ok(v)
4929 }
4930}
4931"#,
4932 );
4933 let ir = lower_fn(&program, "embed_case");
4934 let IrExprKind::Block { stmts, .. } = &ir.kind else {
4935 panic!("expected Block, got {:?}", ir.kind)
4936 };
4937 let IrStmt::Let { value, .. } = &stmts[0] else {
4938 panic!(
4939 "expected the first statement to be a Let, got {:?}",
4940 stmts[0]
4941 )
4942 };
4943 let IrExprKind::Match { arms, .. } = &value.kind else {
4944 panic!("expected Match, got {:?}", value.kind)
4945 };
4946 let IrExprKind::Return { value: returned } = &arms[1].body.kind else {
4947 panic!(
4948 "expected arm 1's body to be Return, got {:?}",
4949 arms[1].body.kind
4950 )
4951 };
4952 let IrExprKind::Variant {
4953 tag: returned_tag,
4954 payload: returned_payload,
4955 } = &returned.kind
4956 else {
4957 panic!("expected a re-constructed Err, got {:?}", returned.kind)
4958 };
4959 assert_eq!(returned_tag, "Err");
4960 assert_eq!(returned_payload.len(), 1);
4961 let IrExprKind::Variant {
4962 tag: embed_tag,
4963 payload: embed_payload,
4964 } = &returned_payload[0].kind
4965 else {
4966 panic!(
4967 "expected the propagated error to be wrapped in the declared embed \
4968 construction, got {:?}",
4969 returned_payload[0].kind
4970 )
4971 };
4972 assert_eq!(embed_tag, "Payment");
4973 assert_eq!(embed_payload.len(), 1);
4974 assert!(matches!(&embed_payload[0].kind, IrExprKind::Local(_)));
4975 assert!(matches!(
4976 &*program.program().ty_intern.get(returned_payload[0].ty),
4977 Ty::Named { name, .. } if name == "OrderError"
4978 ));
4979 }
4980
4981 #[test]
4982 fn question_embeds_conversion_still_resolves_when_the_enclosing_return_is_effect_wrapped() {
4983 let program = checked_program(
4987 r#"
4988commons demo {
4989 type PaymentError = enum { Declined, InsufficientFunds }
4990
4991 type OrderError =
4992 | OutOfStock(sku: String, qty: Int)
4993 | Payment(reason: PaymentError)
4994 embeds PaymentError as Payment
4995
4996 fn charge() -> Result[Int, PaymentError] { Ok(1) }
4997 fn embed_case() -> Effect[Result[Int, OrderError]] {
4998 let v = charge()?
4999 Ok(v)
5000 }
5001}
5002"#,
5003 );
5004 let ir = lower_fn(&program, "embed_case");
5005 let IrExprKind::Block { stmts, .. } = &ir.kind else {
5006 panic!("expected Block, got {:?}", ir.kind)
5007 };
5008 let IrStmt::Let { value, .. } = &stmts[0] else {
5009 panic!(
5010 "expected the first statement to be a Let, got {:?}",
5011 stmts[0]
5012 )
5013 };
5014 let IrExprKind::Match { arms, .. } = &value.kind else {
5015 panic!("expected Match, got {:?}", value.kind)
5016 };
5017 let IrExprKind::Return { value: returned } = &arms[1].body.kind else {
5018 panic!(
5019 "expected arm 1's body to be Return, got {:?}",
5020 arms[1].body.kind
5021 )
5022 };
5023 let IrExprKind::Variant {
5024 tag: returned_tag,
5025 payload: returned_payload,
5026 } = &returned.kind
5027 else {
5028 panic!("expected a re-constructed Err, got {:?}", returned.kind)
5029 };
5030 assert_eq!(returned_tag, "Err");
5031 let IrExprKind::Variant { tag: embed_tag, .. } = &returned_payload[0].kind else {
5032 panic!(
5033 "expected the propagated error wrapped in the declared embed construction \
5034 even through the Effect wrapper, got {:?}",
5035 returned_payload[0].kind
5036 )
5037 };
5038 assert_eq!(
5039 embed_tag, "Payment",
5040 "peel_effect_ty must peel through Effect[..] to find the Result[_, OrderError] \
5041 underneath — an unpeeled Effect[Result[..]] would make target_err_ty resolve to \
5042 something other than OrderError and this embed lookup would silently miss"
5043 );
5044 assert!(matches!(
5045 &*program.program().ty_intern.get(returned_payload[0].ty),
5046 Ty::Named { name, .. } if name == "OrderError"
5047 ));
5048 }
5049
5050 #[test]
5051 fn question_reached_from_an_agent_handler_body_sets_return_ty_without_panicking() {
5052 let program = checked_context_program(
5061 r#"
5062context demo
5063
5064agent Counter {
5065 key id: String
5066 store n: Cell[Int] = 0
5067
5068 on call bump() -> Effect[Result[Int, String]] {
5069 Ok(1)
5070 }
5071}
5072"#,
5073 );
5074 let agent = program
5075 .program()
5076 .commons
5077 .items
5078 .iter()
5079 .find_map(|item| match item {
5080 CommonsItem::Agent(a) if a.name.name == "Counter" => Some(a),
5081 _ => None,
5082 })
5083 .unwrap_or_else(|| panic!("no agent named Counter in this fixture"));
5084 let handler = &agent.handlers[0];
5085 let _ = lower_handler_ir(
5088 handler,
5089 &HashMap::new(),
5090 &HashSet::new(),
5091 program.program().ty_intern.intern(Ty::Unit),
5092 &[],
5093 &[],
5094 &program,
5095 );
5096 }
5097
5098 #[test]
5099 fn question_reached_from_a_provider_op_body_sets_return_ty_without_panicking() {
5100 let program = checked_context_program(
5101 r#"
5102context demo
5103
5104capability Charge {
5105 fn run() -> Effect[Result[Int, String]]
5106}
5107
5108provides Charge = FakeCharge {
5109 fn run() -> Effect[Result[Int, String]] {
5110 Ok(1)
5111 }
5112}
5113"#,
5114 );
5115 let provider = find_provider(&program, "FakeCharge");
5116 let _ = lower_provider_item_ir(provider, &program);
5119 }
5120
5121 #[test]
5122 fn is_on_a_declared_refined_type_forces_a_receiver_temp_and_lowers_to_refined_check() {
5123 let program = checked_program(
5124 r#"
5125commons demo {
5126 type Quantity = Int where InRange(1, 100)
5127
5128 fn valid(n: Int) -> Bool {
5129 n is Quantity
5130 }
5131}
5132"#,
5133 );
5134 let ir = lower_fn(&program, "valid");
5135 let tail = fn_tail(&ir);
5136 let IrExprKind::Block { stmts, tail: inner } = &tail.kind else {
5137 panic!("expected Block, got {:?}", tail.kind)
5138 };
5139 let IrStmt::Let { local, value } = &stmts[0] else {
5140 panic!(
5141 "expected the first statement to be a Let, got {:?}",
5142 stmts[0]
5143 )
5144 };
5145 assert_eq!(local, "__is_receiver_0");
5146 assert!(matches!(&value.kind, IrExprKind::Local(n) if n == "n"));
5147 let IrExprKind::RefinedCheck {
5148 value: checked,
5149 base,
5150 refinement,
5151 } = &inner.kind
5152 else {
5153 panic!("expected RefinedCheck, got {:?}", inner.kind)
5154 };
5155 assert!(matches!(&checked.kind, IrExprKind::Local(n) if n == "__is_receiver_0"));
5156 assert_eq!(*base, BaseType::Int);
5157 let refinement = refinement
5158 .as_ref()
5159 .unwrap_or_else(|| panic!("expected a real refinement, got None"));
5160 assert_eq!(refinement.predicates.len(), 1);
5161 assert!(matches!(
5162 refinement.predicates[0].kind,
5163 PredKind::InRange(..)
5164 ));
5165 }
5166
5167 #[test]
5168 fn is_on_a_bare_variant_lowers_to_a_single_tag_test() {
5169 let program = checked_program(
5170 r#"
5171commons demo {
5172 fn check(r: Result[Int, String]) -> Bool {
5173 r is Ok(_)
5174 }
5175}
5176"#,
5177 );
5178 let ir = lower_fn(&program, "check");
5179 let tail = fn_tail(&ir);
5180 let IrExprKind::Block { tail: inner, .. } = &tail.kind else {
5181 panic!("expected Block, got {:?}", tail.kind)
5182 };
5183 let IrExprKind::BinOp { op, lhs, rhs } = &inner.kind else {
5184 panic!("expected a single BinOp tag test, got {:?}", inner.kind)
5185 };
5186 assert_eq!(*op, IrBinOp::Eq);
5187 assert!(matches!(
5188 &lhs.kind,
5189 IrExprKind::Field { field, .. } if field == "tag"
5190 ));
5191 assert!(matches!(&rhs.kind, IrExprKind::Const(ConstVal::Str(s)) if s == "Ok"));
5192 }
5193
5194 #[test]
5195 fn is_on_a_nested_variant_ands_the_outer_and_inner_tag_tests() {
5196 let program = checked_program(
5197 r#"
5198commons demo {
5199 type Fault =
5200 | NotFound
5201 | Denied(reason: String)
5202
5203 fn describe(r: Result[Int, Fault]) -> Bool {
5204 r is Err(Denied(_))
5205 }
5206}
5207"#,
5208 );
5209 let ir = lower_fn(&program, "describe");
5210 let tail = fn_tail(&ir);
5211 let IrExprKind::Block { tail: inner, .. } = &tail.kind else {
5212 panic!("expected Block, got {:?}", tail.kind)
5213 };
5214 let IrExprKind::And { lhs, rhs } = &inner.kind else {
5215 panic!(
5216 "expected the outer+inner tag tests And-joined, got {:?}",
5217 inner.kind
5218 )
5219 };
5220 let IrExprKind::BinOp {
5221 lhs: outer_field,
5222 rhs: outer_tag,
5223 ..
5224 } = &lhs.kind
5225 else {
5226 panic!("expected the outer test to be a BinOp, got {:?}", lhs.kind)
5227 };
5228 assert!(
5229 matches!(&outer_field.kind, IrExprKind::Field { base, field } if field == "tag" && matches!(&base.kind, IrExprKind::Local(n) if n == "__is_receiver_0"))
5230 );
5231 assert!(matches!(&outer_tag.kind, IrExprKind::Const(ConstVal::Str(s)) if s == "Err"));
5232 let IrExprKind::BinOp {
5233 lhs: inner_field,
5234 rhs: inner_tag,
5235 ..
5236 } = &rhs.kind
5237 else {
5238 panic!("expected the inner test to be a BinOp, got {:?}", rhs.kind)
5239 };
5240 assert!(matches!(
5241 &inner_field.kind,
5242 IrExprKind::Field { field, .. } if field == "tag"
5243 ));
5244 assert!(matches!(&inner_tag.kind, IrExprKind::Const(ConstVal::Str(s)) if s == "Denied"));
5245 let IrExprKind::Field {
5249 base: nested_base,
5250 field: nested_field,
5251 } = &inner_field.kind
5252 else {
5253 panic!("expected inner_field to be a Field access")
5254 };
5255 assert_eq!(nested_field, "tag");
5256 assert!(matches!(
5257 &nested_base.kind,
5258 IrExprKind::Field { field, .. } if field == "error"
5259 ));
5260 }
5261
5262 #[test]
5263 fn is_on_an_or_pattern_or_folds_each_alternatives_and_joined_tests() {
5264 let program = checked_program(
5265 r#"
5266commons demo {
5267 fn check(r: Result[Int, String]) -> Bool {
5268 r is Ok(_) | Err(_)
5269 }
5270}
5271"#,
5272 );
5273 let ir = lower_fn(&program, "check");
5274 let tail = fn_tail(&ir);
5275 let IrExprKind::Block { tail: inner, .. } = &tail.kind else {
5276 panic!("expected Block, got {:?}", tail.kind)
5277 };
5278 let IrExprKind::Or { lhs, rhs } = &inner.kind else {
5279 panic!(
5280 "expected the two alternatives Or-joined, got {:?}",
5281 inner.kind
5282 )
5283 };
5284 assert!(matches!(&lhs.kind, IrExprKind::BinOp { .. }));
5285 assert!(matches!(&rhs.kind, IrExprKind::BinOp { .. }));
5286 }
5287
5288 #[test]
5289 fn method_call_driven_by_callee_method_prepends_the_receiver() {
5290 let program = checked_program(
5291 r#"
5292commons demo {
5293 type Point = { x: Int, y: Int }
5294
5295 fn Point.shiftX(self, dx: Int) -> Point {
5296 Point { x: self.x, y: self.y }
5297 }
5298
5299 fn use_it(p: Point, dx: Int) -> Point { p.shiftX(dx) }
5300}
5301"#,
5302 );
5303 let ir = lower_fn(&program, "use_it");
5304 let tail = fn_tail(&ir);
5305 let IrExprKind::Call { callee, args, .. } = &tail.kind else {
5306 panic!("expected Call, got {:?}", tail.kind)
5307 };
5308 assert!(matches!(callee, Callee::Method(f) if f.name.display().ends_with("shiftX")));
5309 assert_eq!(args.len(), 2);
5310 assert!(matches!(&args[0].kind, IrExprKind::Local(n) if n == "p"));
5311 assert!(matches!(&args[1].kind, IrExprKind::Local(n) if n == "dx"));
5312 }
5313
5314 #[test]
5315 fn static_call_driven_by_callee_static_has_no_prepended_receiver() {
5316 let program = checked_program(
5317 r#"
5318commons demo {
5319 type Point = { x: Int, y: Int }
5320
5321 fn Point.origin() -> Point { Point { x: 0, y: 0 } }
5322
5323 fn use_it() -> Point { Point.origin() }
5324}
5325"#,
5326 );
5327 let ir = lower_fn(&program, "use_it");
5328 let tail = fn_tail(&ir);
5329 let IrExprKind::Call { callee, args, .. } = &tail.kind else {
5330 panic!("expected Call, got {:?}", tail.kind)
5331 };
5332 assert!(matches!(callee, Callee::Static(f) if f.name.display().ends_with("origin")));
5333 assert!(args.is_empty());
5336 }
5337
5338 #[test]
5339 fn kernel_method_call_prepends_the_receiver() {
5340 let program = checked_program(
5341 r#"
5342commons demo {
5343 fn identity_all(xs: List[Int]) -> List[Int] { xs.map((y) => y) }
5344}
5345"#,
5346 );
5347 let ir = lower_fn(&program, "identity_all");
5348 let tail = fn_tail(&ir);
5349 let IrExprKind::Call { callee, args, .. } = &tail.kind else {
5350 panic!("expected Call, got {:?}", tail.kind)
5351 };
5352 assert!(matches!(callee, Callee::Kernel { op, .. } if op == "map"));
5353 assert_eq!(args.len(), 2);
5354 assert!(matches!(&args[0].kind, IrExprKind::Local(n) if n == "xs"));
5355 let IrExprKind::Lambda {
5356 params,
5357 body,
5358 captures,
5359 } = &args[1].kind
5360 else {
5361 panic!("expected Lambda, got {:?}", args[1].kind)
5362 };
5363 assert_eq!(params, &["y".to_string()]);
5364 assert!(captures.is_empty());
5365 assert!(matches!(&body.kind, IrExprKind::Local(n) if n == "y"));
5366 }
5367
5368 #[test]
5374 fn implies_desugars_to_or_not() {
5375 let program = checked_program(
5376 r#"
5377commons demo {
5378 fn imp(p: Bool, q: Bool) -> Bool { p implies q }
5379}
5380"#,
5381 );
5382 let ir = lower_fn(&program, "imp");
5383 let tail = fn_tail(&ir);
5384 let IrExprKind::Or { lhs, rhs } = &tail.kind else {
5385 panic!("expected Or, got {:?}", tail.kind)
5386 };
5387 let IrExprKind::Not { operand } = &lhs.kind else {
5388 panic!("expected Or's lhs to be Not, got {:?}", lhs.kind)
5389 };
5390 assert!(matches!(&operand.kind, IrExprKind::Local(n) if n == "p"));
5391 assert!(matches!(&rhs.kind, IrExprKind::Local(n) if n == "q"));
5392 assert!(matches!(
5393 &*program.program().ty_intern.get(lhs.ty),
5394 Ty::Base(bynk_syntax::ast::BaseType::Bool)
5395 ));
5396 }
5397
5398 #[test]
5402 fn comparison_and_arithmetic_binops_lower_to_a_shared_dedicated_node() {
5403 let program = checked_program(
5404 r#"
5405commons demo {
5406 fn nonneg(balance: Int) -> Bool { balance >= 0 }
5407 fn total(a: Int, b: Int) -> Int { a + b }
5408}
5409"#,
5410 );
5411 let cmp_ir = lower_fn(&program, "nonneg");
5412 let IrExprKind::BinOp { op, lhs, rhs } = &fn_tail(&cmp_ir).kind else {
5413 panic!("expected BinOp, got {:?}", fn_tail(&cmp_ir).kind)
5414 };
5415 assert_eq!(*op, IrBinOp::GtEq);
5416 assert!(matches!(&lhs.kind, IrExprKind::Local(n) if n == "balance"));
5417 assert!(matches!(&rhs.kind, IrExprKind::Const(ConstVal::Int(0))));
5418 assert!(matches!(
5419 &*program.program().ty_intern.get(fn_tail(&cmp_ir).ty),
5420 Ty::Base(bynk_syntax::ast::BaseType::Bool)
5421 ));
5422
5423 let arith_ir = lower_fn(&program, "total");
5424 let IrExprKind::BinOp { op, lhs, rhs } = &fn_tail(&arith_ir).kind else {
5425 panic!("expected BinOp, got {:?}", fn_tail(&arith_ir).kind)
5426 };
5427 assert_eq!(*op, IrBinOp::Add);
5428 assert!(matches!(&lhs.kind, IrExprKind::Local(n) if n == "a"));
5429 assert!(matches!(&rhs.kind, IrExprKind::Local(n) if n == "b"));
5430 assert!(matches!(
5431 &*program.program().ty_intern.get(fn_tail(&arith_ir).ty),
5432 Ty::Base(bynk_syntax::ast::BaseType::Int)
5433 ));
5434 }
5435
5436 #[test]
5437 fn unary_neg_lowers_to_its_own_dedicated_node() {
5438 let program = checked_program(
5439 r#"
5440commons demo {
5441 fn negate(x: Int) -> Int { -x }
5442}
5443"#,
5444 );
5445 let ir = lower_fn(&program, "negate");
5446 let IrExprKind::Neg { operand } = &fn_tail(&ir).kind else {
5447 panic!("expected Neg, got {:?}", fn_tail(&ir).kind)
5448 };
5449 assert!(matches!(&operand.kind, IrExprKind::Local(n) if n == "x"));
5450 assert!(matches!(
5451 &*program.program().ty_intern.get(fn_tail(&ir).ty),
5452 Ty::Base(bynk_syntax::ast::BaseType::Int)
5453 ));
5454 }
5455
5456 #[test]
5457 fn every_binop_tag_maps_to_its_own_ir_binop_variant() {
5458 let program = checked_program(
5464 r#"
5465commons demo {
5466 fn eq(a: Int, b: Int) -> Bool { a == b }
5467 fn neq(a: Int, b: Int) -> Bool { a != b }
5468 fn lt(a: Int, b: Int) -> Bool { a < b }
5469 fn lteq(a: Int, b: Int) -> Bool { a <= b }
5470 fn gt(a: Int, b: Int) -> Bool { a > b }
5471 fn gteq(a: Int, b: Int) -> Bool { a >= b }
5472 fn add(a: Int, b: Int) -> Int { a + b }
5473 fn sub(a: Int, b: Int) -> Int { a - b }
5474 fn mul(a: Int, b: Int) -> Int { a * b }
5475 fn div(a: Int, b: Int) -> Int { a / b }
5476}
5477"#,
5478 );
5479 let cases = [
5480 ("eq", IrBinOp::Eq),
5481 ("neq", IrBinOp::NotEq),
5482 ("lt", IrBinOp::Lt),
5483 ("lteq", IrBinOp::LtEq),
5484 ("gt", IrBinOp::Gt),
5485 ("gteq", IrBinOp::GtEq),
5486 ("add", IrBinOp::Add),
5487 ("sub", IrBinOp::Sub),
5488 ("mul", IrBinOp::Mul),
5489 ("div", IrBinOp::Div),
5490 ];
5491 for (fn_name, expected_op) in cases {
5492 let ir = lower_fn(&program, fn_name);
5493 let IrExprKind::BinOp { op, .. } = &fn_tail(&ir).kind else {
5494 panic!("{fn_name}: expected BinOp, got {:?}", fn_tail(&ir).kind)
5495 };
5496 assert_eq!(*op, expected_op, "{fn_name}: wrong IrBinOp tag");
5497 }
5498 }
5499
5500 #[test]
5501 fn interp_str_lowers_chunks_verbatim_and_holes_as_ordinary_expressions() {
5502 let program = checked_program(
5503 r#"
5504commons demo {
5505 fn greet(name: String) -> String { "hi \(name)!" }
5506}
5507"#,
5508 );
5509 let ir = lower_fn(&program, "greet");
5510 let IrExprKind::InterpStr { parts } = &fn_tail(&ir).kind else {
5511 panic!("expected InterpStr, got {:?}", fn_tail(&ir).kind)
5512 };
5513 assert_eq!(parts.len(), 3);
5514 assert!(matches!(&parts[0], IrInterpPart::Chunk(s) if s == "hi "));
5515 let IrInterpPart::Hole(hole) = &parts[1] else {
5516 panic!("expected Hole, got {:?}", parts[1])
5517 };
5518 assert!(matches!(&hole.kind, IrExprKind::Local(n) if n == "name"));
5519 assert!(matches!(&parts[2], IrInterpPart::Chunk(s) if s == "!"));
5520 assert!(matches!(
5521 &*program.program().ty_intern.get(fn_tail(&ir).ty),
5522 Ty::Base(bynk_syntax::ast::BaseType::String)
5523 ));
5524 }
5525
5526 #[test]
5527 fn interp_str_covers_leading_hole_hole_only_and_adjacent_holes() {
5528 let program = checked_program(
5533 r#"
5534commons demo {
5535 fn lead(name: String) -> String { "\(name) hi" }
5536 fn only(name: String) -> String { "\(name)" }
5537 fn adjacent(a: String, b: String) -> String { "\(a)\(b)" }
5538}
5539"#,
5540 );
5541
5542 let lead = lower_fn(&program, "lead");
5543 let IrExprKind::InterpStr { parts } = &fn_tail(&lead).kind else {
5544 panic!("expected InterpStr, got {:?}", fn_tail(&lead).kind)
5545 };
5546 assert_eq!(parts.len(), 2, "no leading empty Chunk before the hole");
5547 assert!(matches!(&parts[0], IrInterpPart::Hole(h)
5548 if matches!(&h.kind, IrExprKind::Local(n) if n == "name")));
5549 assert!(matches!(&parts[1], IrInterpPart::Chunk(s) if s == " hi"));
5550
5551 let only = lower_fn(&program, "only");
5552 let IrExprKind::InterpStr { parts } = &fn_tail(&only).kind else {
5553 panic!("expected InterpStr, got {:?}", fn_tail(&only).kind)
5554 };
5555 assert_eq!(
5556 parts.len(),
5557 1,
5558 "a hole-only string is just the one Hole segment"
5559 );
5560 assert!(matches!(&parts[0], IrInterpPart::Hole(_)));
5561
5562 let adjacent = lower_fn(&program, "adjacent");
5563 let IrExprKind::InterpStr { parts } = &fn_tail(&adjacent).kind else {
5564 panic!("expected InterpStr, got {:?}", fn_tail(&adjacent).kind)
5565 };
5566 assert_eq!(parts.len(), 2, "no empty Chunk between adjacent holes");
5567 assert!(matches!(&parts[0], IrInterpPart::Hole(h)
5568 if matches!(&h.kind, IrExprKind::Local(n) if n == "a")));
5569 assert!(matches!(&parts[1], IrInterpPart::Hole(h)
5570 if matches!(&h.kind, IrExprKind::Local(n) if n == "b")));
5571 }
5572
5573 #[test]
5574 fn record_spread_resolves_every_declared_field_override_and_spread_through() {
5575 let program = checked_program(
5576 r#"
5577commons demo {
5578 type Point = { x: Int, y: Int, z: Int }
5579
5580 fn shift(p: Point, y: Int) -> Point { Point { ...p, x: 0, y } }
5581}
5582"#,
5583 );
5584 let ir = lower_fn(&program, "shift");
5585 let tail = fn_tail(&ir);
5586 let IrExprKind::Block {
5587 stmts,
5588 tail: block_tail,
5589 } = &tail.kind
5590 else {
5591 panic!("expected Block, got {:?}", tail.kind)
5592 };
5593 assert_eq!(stmts.len(), 1);
5594 let IrStmt::Let { local, value } = &stmts[0] else {
5595 panic!("expected Let, got {:?}", stmts[0])
5596 };
5597 assert_eq!(local, "__spread_base_0");
5598 assert!(matches!(&value.kind, IrExprKind::Local(n) if n == "p"));
5599
5600 let IrExprKind::Record { fields } = &block_tail.kind else {
5601 panic!("expected Record, got {:?}", block_tail.kind)
5602 };
5603 assert_eq!(fields.len(), 3);
5604
5605 assert_eq!(fields[0].0, "x");
5607 assert!(matches!(
5608 &fields[0].1.kind,
5609 IrExprKind::Const(ConstVal::Int(0))
5610 ));
5611
5612 assert_eq!(fields[1].0, "y");
5614 assert!(matches!(&fields[1].1.kind, IrExprKind::Local(n) if n == "y"));
5615
5616 assert_eq!(fields[2].0, "z");
5618 let IrExprKind::Field { base, field } = &fields[2].1.kind else {
5619 panic!("expected Field, got {:?}", fields[2].1.kind)
5620 };
5621 assert_eq!(field, "z");
5622 assert!(matches!(&base.kind, IrExprKind::Local(n) if n == "__spread_base_0"));
5623 assert!(matches!(
5624 &*program.program().ty_intern.get(fields[2].1.ty),
5625 Ty::Base(bynk_syntax::ast::BaseType::Int)
5626 ));
5627 }
5628
5629 #[test]
5630 fn record_spread_orders_fields_by_evaluation_order_not_declaration_order() {
5631 let program = checked_program(
5637 r#"
5638commons demo {
5639 type Point = { x: Int, y: Int, z: Int }
5640
5641 fn shift(p: Point) -> Point { Point { ...p, y: 1, x: 2 } }
5642}
5643"#,
5644 );
5645 let ir = lower_fn(&program, "shift");
5646 let tail = fn_tail(&ir);
5647 let IrExprKind::Block {
5648 tail: block_tail, ..
5649 } = &tail.kind
5650 else {
5651 panic!("expected Block, got {:?}", tail.kind)
5652 };
5653 let IrExprKind::Record { fields, .. } = &block_tail.kind else {
5654 panic!("expected Record, got {:?}", block_tail.kind)
5655 };
5656 let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
5657 assert_eq!(names, vec!["y", "x", "z"]);
5658 }
5659
5660 #[test]
5661 fn record_spread_duplicate_override_keeps_the_last_value_and_still_runs_the_earlier_one() {
5662 let program = checked_program(
5667 r#"
5668commons demo {
5669 type Point = { x: Int, y: Int }
5670
5671 fn shift(p: Point) -> Point { Point { ...p, x: 1, x: 2 } }
5672}
5673"#,
5674 );
5675 let ir = lower_fn(&program, "shift");
5676 let tail = fn_tail(&ir);
5677 let IrExprKind::Block {
5678 stmts,
5679 tail: block_tail,
5680 } = &tail.kind
5681 else {
5682 panic!("expected Block, got {:?}", tail.kind)
5683 };
5684 assert_eq!(stmts.len(), 2, "the base Let plus one discarded duplicate");
5685 let IrStmt::Expr { value: discarded } = &stmts[1] else {
5686 panic!(
5687 "expected the second stmt to be a discarded Expr, got {:?}",
5688 stmts[1]
5689 )
5690 };
5691 assert!(matches!(
5692 &discarded.kind,
5693 IrExprKind::Const(ConstVal::Int(1))
5694 ));
5695
5696 let IrExprKind::Record { fields, .. } = &block_tail.kind else {
5697 panic!("expected Record, got {:?}", block_tail.kind)
5698 };
5699 assert_eq!(fields.len(), 2);
5700 assert_eq!(fields[0].0, "x");
5701 assert!(matches!(
5702 &fields[0].1.kind,
5703 IrExprKind::Const(ConstVal::Int(2))
5704 ));
5705 }
5706
5707 #[test]
5708 fn record_spread_instantiates_a_generic_records_spread_through_field_type() {
5709 let program = checked_program(
5715 r#"
5716commons demo {
5717 type Boxed[T] = { item: T, tag: String }
5718
5719 fn retag(b: Boxed[Int]) -> Boxed[Int] { Boxed { ...b, tag: "x" } }
5720}
5721"#,
5722 );
5723 let ir = lower_fn(&program, "retag");
5724 let tail = fn_tail(&ir);
5725 let IrExprKind::Block {
5726 tail: block_tail, ..
5727 } = &tail.kind
5728 else {
5729 panic!("expected Block, got {:?}", tail.kind)
5730 };
5731 let IrExprKind::Record { fields, .. } = &block_tail.kind else {
5732 panic!("expected Record, got {:?}", block_tail.kind)
5733 };
5734 let (_, item) = fields
5735 .iter()
5736 .find(|(n, _)| n == "item")
5737 .expect("item field present");
5738 let IrExprKind::Field { .. } = &item.kind else {
5739 panic!(
5740 "expected item to spread through as Field, got {:?}",
5741 item.kind
5742 )
5743 };
5744 assert!(matches!(
5745 &*program.program().ty_intern.get(item.ty),
5746 Ty::Base(bynk_syntax::ast::BaseType::Int)
5747 ));
5748 }
5749
5750 fn find_match_arms(f: &FnDecl) -> (&Expr, &[MatchArm]) {
5763 let ExprKind::Match { discriminant, arms } = &f.body.tail.kind else {
5764 panic!(
5765 "fixture's fn body tail is not a Match, got {:?}",
5766 f.body.tail.kind
5767 )
5768 };
5769 (discriminant, arms)
5770 }
5771
5772 fn lower_match_fixture(program: &CheckedProgram, fn_name: &str) -> (Vec<IrArm>, Exhaustive) {
5777 let f = find_fn(program, fn_name);
5778 let (discriminant, arms) = find_match_arms(f);
5779 let disc_ty = program
5780 .program()
5781 .expr_types
5782 .get(&discriminant.id)
5783 .unwrap_or_else(|| panic!("{fn_name}: discriminant has no recorded type"))
5784 .ty;
5785 let mut cx = LowerIrCtx::new(program, HashSet::new());
5786 for p in &f.params {
5787 let ty = cx.resolve_type_ref(&p.type_ref).unwrap_or_else(|| {
5788 panic!("{fn_name}: param `{}`'s type does not resolve", p.name.name)
5789 });
5790 cx.bind(p.name.name.clone(), ty);
5791 }
5792 let ir_arms: Vec<IrArm> = arms
5793 .iter()
5794 .map(|a| lower_arm_ir(a, disc_ty, &mut cx))
5795 .collect();
5796 let exhaustive = lower_exhaustive_ir(arms);
5797 (ir_arms, exhaustive)
5798 }
5799
5800 #[test]
5801 fn pattern_ir_covers_wildcard_binding_and_literal_leaves() {
5802 let program = checked_program(
5803 r#"
5804commons demo {
5805 fn wildcard_case(n: Int) -> Int {
5806 match n {
5807 _ => 0
5808 }
5809 }
5810
5811 fn binding_case(n: Int) -> Int {
5812 match n {
5813 m => m
5814 }
5815 }
5816
5817 fn literal_case(n: Int) -> String {
5818 match n {
5819 0 => "zero"
5820 other => "other"
5821 }
5822 }
5823}
5824"#,
5825 );
5826
5827 let (wild_arms, wild_exhaustive) = lower_match_fixture(&program, "wildcard_case");
5828 assert_eq!(wild_arms.len(), 1);
5829 assert!(matches!(&wild_arms[0].pat, IrPat::Wild));
5830 assert!(wild_arms[0].binds.is_empty());
5831 assert_eq!(wild_arms[0].binding_mode, BindingMode::Direct);
5832 assert!(matches!(wild_exhaustive, Exhaustive::Total));
5833
5834 let (binding_arms, _) = lower_match_fixture(&program, "binding_case");
5835 assert!(matches!(&binding_arms[0].pat, IrPat::Bind { local } if local == "m"));
5836 assert_eq!(binding_arms[0].binds, vec!["m".to_string()]);
5837 assert!(matches!(&binding_arms[0].body.kind, IrExprKind::Local(n) if n == "m"));
5838
5839 let (literal_arms, literal_exhaustive) = lower_match_fixture(&program, "literal_case");
5840 assert!(matches!(
5841 &literal_arms[0].pat,
5842 IrPat::Const {
5843 value: ConstVal::Int(0)
5844 }
5845 ));
5846 assert!(matches!(&literal_arms[1].pat, IrPat::Bind { local } if local == "other"));
5847 assert!(matches!(literal_exhaustive, Exhaustive::Total));
5848 }
5849
5850 #[test]
5851 fn pattern_ir_variant_over_a_user_sum_resolves_tag_and_payload_fields() {
5852 let program = checked_program(
5853 r#"
5854commons demo {
5855 type Outcome =
5856 | Hit(score: Int)
5857 | Miss
5858
5859 fn variant_user_sum(o: Outcome) -> Int {
5860 match o {
5861 Hit(score) => score
5862 Miss => 0
5863 }
5864 }
5865}
5866"#,
5867 );
5868
5869 let (arms, exhaustive) = lower_match_fixture(&program, "variant_user_sum");
5870 assert_eq!(arms.len(), 2);
5871
5872 let IrPat::Variant { tag, fields, .. } = &arms[0].pat else {
5873 panic!("expected Variant, got {:?}", arms[0].pat)
5874 };
5875 assert_eq!(tag, "Hit");
5876 assert_eq!(fields.len(), 1);
5877 assert_eq!(fields[0].0, "score");
5878 assert!(matches!(&*fields[0].1, IrPat::Bind { local } if local == "score"));
5879 assert_eq!(arms[0].binds, vec!["score".to_string()]);
5880 assert!(matches!(&arms[0].body.kind, IrExprKind::Local(n) if n == "score"));
5881
5882 let IrPat::Variant { tag, fields, .. } = &arms[1].pat else {
5883 panic!("expected Variant, got {:?}", arms[1].pat)
5884 };
5885 assert_eq!(tag, "Miss");
5886 assert!(fields.is_empty());
5887 assert!(arms[1].binds.is_empty());
5888
5889 assert!(matches!(exhaustive, Exhaustive::Total));
5890 }
5891
5892 #[test]
5893 fn pattern_ir_variant_over_result_and_option_resolves_via_variants_of() {
5894 let program = checked_program(
5895 r#"
5896commons demo {
5897 fn variant_result(r: Result[Int, String]) -> Int {
5898 match r {
5899 Ok(v) => v
5900 Err(_) => 0
5901 }
5902 }
5903
5904 fn variant_option(o: Option[Int]) -> Int {
5905 match o {
5906 Some(v) => v
5907 None => 0
5908 }
5909 }
5910}
5911"#,
5912 );
5913
5914 let (result_arms, _) = lower_match_fixture(&program, "variant_result");
5915 let IrPat::Variant { tag, fields, .. } = &result_arms[0].pat else {
5916 panic!("expected Variant, got {:?}", result_arms[0].pat)
5917 };
5918 assert_eq!(tag, "Ok");
5919 assert_eq!(fields[0].0, "value");
5920 assert!(matches!(&*fields[0].1, IrPat::Bind { local } if local == "v"));
5921 let IrPat::Variant { tag, fields, .. } = &result_arms[1].pat else {
5922 panic!("expected Variant, got {:?}", result_arms[1].pat)
5923 };
5924 assert_eq!(tag, "Err");
5925 assert_eq!(fields[0].0, "error");
5926 assert!(matches!(&*fields[0].1, IrPat::Wild));
5927
5928 let (option_arms, _) = lower_match_fixture(&program, "variant_option");
5929 let IrPat::Variant { tag, fields, .. } = &option_arms[0].pat else {
5930 panic!("expected Variant, got {:?}", option_arms[0].pat)
5931 };
5932 assert_eq!(tag, "Some");
5933 assert_eq!(fields[0].0, "value");
5934 let IrPat::Variant { tag, fields, .. } = &option_arms[1].pat else {
5935 panic!("expected Variant, got {:?}", option_arms[1].pat)
5936 };
5937 assert_eq!(tag, "None");
5938 assert!(fields.is_empty());
5939 }
5940
5941 #[test]
5942 fn pattern_ir_refined_wraps_the_inner_pattern_and_stays_direct_mode() {
5943 let program = checked_program(
5944 r#"
5945commons demo {
5946 fn refined_case(n: Int) -> Int {
5947 match n {
5948 _ where Positive => 1
5949 _ => 0
5950 }
5951 }
5952}
5953"#,
5954 );
5955
5956 let (arms, exhaustive) = lower_match_fixture(&program, "refined_case");
5957 let IrPat::Refined { inner, refinement } = &arms[0].pat else {
5958 panic!("expected Refined, got {:?}", arms[0].pat)
5959 };
5960 assert!(matches!(&**inner, IrPat::Wild));
5961 assert_eq!(refinement.predicates.len(), 1);
5962 assert_eq!(refinement.predicates[0].kind.name(), "Positive");
5963 assert_eq!(arms[0].binding_mode, BindingMode::Direct);
5964 assert!(matches!(exhaustive, Exhaustive::Total));
5965 }
5966
5967 #[test]
5968 fn pattern_ir_or_pattern_records_or_dispatch_binding_mode_and_shared_binds() {
5969 let program = checked_program(
5970 r#"
5971commons demo {
5972 type Outcome =
5973 | Hit(score: Int)
5974 | Boost(score: Int)
5975 | Miss
5976
5977 fn or_case(o: Outcome) -> Int {
5978 match o {
5979 Hit(score) | Boost(score) => score
5980 Miss => 0
5981 }
5982 }
5983}
5984"#,
5985 );
5986
5987 let (arms, exhaustive) = lower_match_fixture(&program, "or_case");
5988 let IrPat::Or { alts } = &arms[0].pat else {
5989 panic!("expected Or, got {:?}", arms[0].pat)
5990 };
5991 assert_eq!(alts.len(), 2);
5992 assert!(matches!(&alts[0], IrPat::Variant { tag, .. } if tag == "Hit"));
5993 assert!(matches!(&alts[1], IrPat::Variant { tag, .. } if tag == "Boost"));
5994 assert_eq!(arms[0].binds, vec!["score".to_string()]);
5998 assert_eq!(arms[0].binding_mode, BindingMode::OrDispatch);
5999 assert!(matches!(&arms[0].body.kind, IrExprKind::Local(n) if n == "score"));
6000
6001 assert_eq!(arms[1].binding_mode, BindingMode::Direct);
6004
6005 assert!(matches!(exhaustive, Exhaustive::Total));
6006 }
6007
6008 #[test]
6009 fn pattern_ir_arm_guard_lowers_and_sees_the_patterns_own_bindings() {
6010 let program = checked_program(
6017 r#"
6018commons demo {
6019 type Outcome =
6020 | Hit(flag: Bool)
6021 | Miss
6022
6023 fn guarded(o: Outcome) -> Int {
6024 match o {
6025 Hit(flag) if flag => 1
6026 _ => 0
6027 }
6028 }
6029}
6030"#,
6031 );
6032
6033 let (arms, exhaustive) = lower_match_fixture(&program, "guarded");
6034 let guard = arms[0]
6035 .guard
6036 .as_ref()
6037 .unwrap_or_else(|| panic!("expected a lowered guard, got {:?}", arms[0].guard));
6038 assert!(matches!(&guard.kind, IrExprKind::Local(n) if n == "flag"));
6039 assert!(matches!(exhaustive, Exhaustive::Total));
6040 }
6041
6042 #[test]
6043 fn pattern_ir_named_bindings_resolve_by_field_name_not_position() {
6044 let program = checked_program(
6045 r#"
6046commons demo {
6047 type Pair =
6048 | Pair(a: Int, b: String)
6049 | Empty
6050
6051 fn named_reordered(p: Pair) -> String {
6052 match p {
6053 Pair(b: s, a: n) => s
6054 Empty => "none"
6055 }
6056 }
6057
6058 fn named_subset(p: Pair) -> Int {
6059 match p {
6060 Pair(a: n) => n
6061 Empty => 0
6062 }
6063 }
6064}
6065"#,
6066 );
6067
6068 let (reordered_arms, _) = lower_match_fixture(&program, "named_reordered");
6072 let IrPat::Variant { fields, .. } = &reordered_arms[0].pat else {
6073 panic!("expected Variant, got {:?}", reordered_arms[0].pat)
6074 };
6075 assert_eq!(fields.len(), 2);
6076 assert_eq!(fields[0].0, "b");
6077 assert!(matches!(&*fields[0].1, IrPat::Bind { local } if local == "s"));
6078 assert_eq!(fields[1].0, "a");
6079 assert!(matches!(&*fields[1].1, IrPat::Bind { local } if local == "n"));
6080
6081 let (subset_arms, _) = lower_match_fixture(&program, "named_subset");
6085 let IrPat::Variant { fields, .. } = &subset_arms[0].pat else {
6086 panic!("expected Variant, got {:?}", subset_arms[0].pat)
6087 };
6088 assert_eq!(fields.len(), 1);
6089 assert_eq!(fields[0].0, "a");
6090 assert!(matches!(&*fields[0].1, IrPat::Bind { local } if local == "n"));
6091 }
6092
6093 #[test]
6094 fn pattern_ir_nested_variant_pattern_threads_a_derived_field_ty() {
6095 let program = checked_program(
6100 r#"
6101commons demo {
6102 fn nested_variant(x: Result[Option[Int], String]) -> Int {
6103 match x {
6104 Ok(Some(v)) => v
6105 Ok(None) => 0
6106 Err(_) => 0
6107 }
6108 }
6109}
6110"#,
6111 );
6112
6113 let (arms, exhaustive) = lower_match_fixture(&program, "nested_variant");
6114 let IrPat::Variant {
6115 tag: outer_tag,
6116 fields: outer_fields,
6117 ..
6118 } = &arms[0].pat
6119 else {
6120 panic!("expected Variant, got {:?}", arms[0].pat)
6121 };
6122 assert_eq!(outer_tag, "Ok");
6123 assert_eq!(outer_fields[0].0, "value");
6124 let IrPat::Variant {
6125 tag: inner_tag,
6126 fields: inner_fields,
6127 ..
6128 } = &*outer_fields[0].1
6129 else {
6130 panic!("expected a nested Variant, got {:?}", outer_fields[0].1)
6131 };
6132 assert_eq!(inner_tag, "Some");
6133 assert!(matches!(&*inner_fields[0].1, IrPat::Bind { local } if local == "v"));
6134 assert!(matches!(&arms[0].body.kind, IrExprKind::Local(n) if n == "v"));
6135
6136 assert!(matches!(exhaustive, Exhaustive::Total));
6137 }
6138
6139 #[test]
6140 #[should_panic(expected = "always has at least one unguarded arm")]
6141 fn exhaustive_ir_panics_if_every_arm_is_guarded() {
6142 let dummy_expr = Expr {
6150 id: ExprId(0),
6151 kind: ExprKind::BoolLit(true),
6152 span: Span::default(),
6153 };
6154 let arm = MatchArm {
6155 pattern: Pattern::Wildcard(Span::default()),
6156 guard: Some(dummy_expr.clone()),
6157 body: MatchBody::Expr(dummy_expr),
6158 span: Span::default(),
6159 };
6160 let _ = lower_exhaustive_ir(std::slice::from_ref(&arm));
6161 }
6162
6163 #[test]
6171 fn match_through_lower_expr_ir_builds_a_real_match_node_with_flat_form() {
6172 let program = checked_program(
6177 r#"
6178commons demo {
6179 type Outcome =
6180 | Hit(score: Int)
6181 | Miss
6182
6183 fn describe(o: Outcome) -> Int {
6184 match o {
6185 Hit(score) => score
6186 Miss => 0
6187 }
6188 }
6189}
6190"#,
6191 );
6192 let ir = lower_fn(&program, "describe");
6193 let tail = fn_tail(&ir);
6194 let IrExprKind::Match {
6195 scrutinee,
6196 arms,
6197 exhaustive,
6198 form,
6199 } = &tail.kind
6200 else {
6201 panic!("expected Match, got {:?}", tail.kind)
6202 };
6203 assert!(matches!(&scrutinee.kind, IrExprKind::Local(n) if n == "o"));
6204 assert_eq!(arms.len(), 2);
6205 assert!(matches!(&arms[0].pat, IrPat::Variant { tag, .. } if tag == "Hit"));
6206 assert!(matches!(&arms[0].body.kind, IrExprKind::Local(n) if n == "score"));
6207 assert!(matches!(&arms[1].pat, IrPat::Variant { tag, .. } if tag == "Miss"));
6208 assert!(matches!(exhaustive, Exhaustive::Total));
6209 assert_eq!(*form, MatchForm::Flat);
6210 }
6211
6212 #[test]
6213 fn match_with_a_guarded_arm_gets_if_chain_form() {
6214 let program = checked_program(
6215 r#"
6216commons demo {
6217 type Outcome =
6218 | Hit(flag: Bool)
6219 | Miss
6220
6221 fn describe(o: Outcome) -> Int {
6222 match o {
6223 Hit(flag) if flag => 1
6224 _ => 0
6225 }
6226 }
6227}
6228"#,
6229 );
6230 let ir = lower_fn(&program, "describe");
6231 let tail = fn_tail(&ir);
6232 let IrExprKind::Match { arms, form, .. } = &tail.kind else {
6233 panic!("expected Match, got {:?}", tail.kind)
6234 };
6235 assert_eq!(*form, MatchForm::IfChain);
6236 let guard = arms[0]
6244 .guard
6245 .as_ref()
6246 .unwrap_or_else(|| panic!("expected a lowered guard, got {:?}", arms[0].guard));
6247 assert!(matches!(&guard.kind, IrExprKind::Local(n) if n == "flag"));
6248 }
6249
6250 #[test]
6251 fn match_with_a_nested_refutable_payload_pattern_gets_if_chain_form() {
6252 let program = checked_program(
6253 r#"
6254commons demo {
6255 fn describe(x: Result[Option[Int], String]) -> Int {
6256 match x {
6257 Ok(Some(v)) => v
6258 Ok(None) => 0
6259 Err(_) => 0
6260 }
6261 }
6262}
6263"#,
6264 );
6265 let ir = lower_fn(&program, "describe");
6266 let tail = fn_tail(&ir);
6267 let IrExprKind::Match { form, .. } = &tail.kind else {
6268 panic!("expected Match, got {:?}", tail.kind)
6269 };
6270 assert_eq!(*form, MatchForm::IfChain);
6271 }
6272
6273 #[test]
6274 fn match_with_a_refined_arm_gets_if_chain_form() {
6275 let program = checked_program(
6281 r#"
6282commons demo {
6283 fn describe(n: Int) -> Int {
6284 match n {
6285 _ where Positive => 1
6286 _ => 0
6287 }
6288 }
6289}
6290"#,
6291 );
6292 let ir = lower_fn(&program, "describe");
6293 let tail = fn_tail(&ir);
6294 let IrExprKind::Match { arms, form, .. } = &tail.kind else {
6295 panic!("expected Match, got {:?}", tail.kind)
6296 };
6297 assert!(matches!(&arms[0].pat, IrPat::Refined { .. }));
6298 assert_eq!(*form, MatchForm::IfChain);
6299 }
6300
6301 #[test]
6302 fn match_with_a_bindingless_or_pattern_stays_flat() {
6303 let program = checked_program(
6314 r#"
6315commons demo {
6316 type Outcome =
6317 | Hit
6318 | Boost
6319 | Miss
6320
6321 fn describe(o: Outcome) -> Int {
6322 match o {
6323 Hit | Boost => 1
6324 Miss => 0
6325 }
6326 }
6327}
6328"#,
6329 );
6330 let ir = lower_fn(&program, "describe");
6331 let tail = fn_tail(&ir);
6332 let IrExprKind::Match { arms, form, .. } = &tail.kind else {
6333 panic!("expected Match, got {:?}", tail.kind)
6334 };
6335 assert!(matches!(&arms[0].pat, IrPat::Or { .. }));
6336 assert_eq!(*form, MatchForm::Flat);
6337 }
6338
6339 #[test]
6340 fn match_construction_is_position_agnostic_tail_vs_nested() {
6341 let program = checked_program(
6349 r#"
6350commons demo {
6351 type Outcome =
6352 | Hit(score: Int)
6353 | Miss
6354
6355 fn tail_position(o: Outcome) -> Int {
6356 match o {
6357 Hit(score) => score
6358 Miss => 0
6359 }
6360 }
6361
6362 fn nested_position(o: Outcome) -> Int {
6363 let result = match o {
6364 Hit(score) => score
6365 Miss => 0
6366 }
6367 result
6368 }
6369}
6370"#,
6371 );
6372
6373 let tail_ir = lower_fn(&program, "tail_position");
6374 let tail = fn_tail(&tail_ir);
6375 let IrExprKind::Match {
6376 arms: tail_arms,
6377 exhaustive: tail_exhaustive,
6378 form: tail_form,
6379 ..
6380 } = &tail.kind
6381 else {
6382 panic!("tail_position: expected Match, got {:?}", tail.kind)
6383 };
6384
6385 let nested_ir = lower_fn(&program, "nested_position");
6386 let IrExprKind::Block { stmts, .. } = &nested_ir.kind else {
6387 panic!(
6388 "lower_fn_body_ir always returns IrExprKind::Block, got {:?}",
6389 nested_ir.kind
6390 )
6391 };
6392 let IrStmt::Let {
6393 value: nested_match,
6394 ..
6395 } = &stmts[0]
6396 else {
6397 panic!("nested_position: expected a Let statement, got {stmts:?}")
6398 };
6399 let IrExprKind::Match {
6400 arms: nested_arms,
6401 exhaustive: nested_exhaustive,
6402 form: nested_form,
6403 ..
6404 } = &nested_match.kind
6405 else {
6406 panic!(
6407 "nested_position: expected Match, got {:?}",
6408 nested_match.kind
6409 )
6410 };
6411
6412 fn tags(arms: &[IrArm]) -> Vec<&str> {
6413 arms.iter()
6414 .map(|a| match &a.pat {
6415 IrPat::Variant { tag, .. } => tag.as_str(),
6416 other => panic!("expected a Variant pattern, got {other:?}"),
6417 })
6418 .collect()
6419 }
6420 assert_eq!(tags(tail_arms), tags(nested_arms));
6421 assert_eq!(*tail_form, *nested_form);
6422 assert!(matches!(tail_exhaustive, Exhaustive::Total));
6423 assert!(matches!(nested_exhaustive, Exhaustive::Total));
6424 }
6425
6426 #[test]
6427 fn type_item_record_resolves_fields_and_generic_rigid_vars() {
6428 let program = checked_program(
6429 r#"
6430commons demo {
6431 type Box[T] = { value: T }
6432}
6433"#,
6434 );
6435 let decl = find_type(&program, "Box");
6436 let item = lower_type_item_ir(decl, &program);
6437 let IrItem::Type { shape } = &item else {
6438 panic!("expected IrItem::Type, got {item:?}")
6439 };
6440 let TypeShape::Record { fields } = shape else {
6441 panic!("expected TypeShape::Record, got {shape:?}")
6442 };
6443 assert_eq!(fields.len(), 1);
6444 assert_eq!(fields[0].0, "value");
6445 assert!(matches!(
6446 &*program.program().ty_intern.get(fields[0].1),
6447 Ty::Var(name) if name == "T"
6448 ));
6449 }
6450
6451 #[test]
6452 fn type_item_sum_resolves_variant_payloads_and_embeds() {
6453 let program = checked_program(
6454 r#"
6455commons demo {
6456 type PaymentError = enum { Declined, InsufficientFunds }
6457
6458 type OrderError =
6459 | OutOfStock(sku: String, qty: Int)
6460 | Payment(reason: PaymentError)
6461 embeds PaymentError as Payment
6462}
6463"#,
6464 );
6465 let decl = find_type(&program, "OrderError");
6466 let item = lower_type_item_ir(decl, &program);
6467 let IrItem::Type { shape, .. } = &item else {
6468 panic!("expected IrItem::Type, got {item:?}")
6469 };
6470 let TypeShape::Sum { variants, embeds } = shape else {
6471 panic!("expected TypeShape::Sum, got {shape:?}")
6472 };
6473 assert_eq!(variants.len(), 2);
6474 assert_eq!(variants[0].0, "OutOfStock");
6475 assert_eq!(variants[0].1.len(), 2);
6476 assert_eq!(variants[0].1[0].0, "sku");
6477 assert!(matches!(
6478 &*program.program().ty_intern.get(variants[0].1[0].1),
6479 Ty::Base(bynk_syntax::ast::BaseType::String)
6480 ));
6481 assert_eq!(variants[0].1[1].0, "qty");
6482 assert!(matches!(
6483 &*program.program().ty_intern.get(variants[0].1[1].1),
6484 Ty::Base(bynk_syntax::ast::BaseType::Int)
6485 ));
6486 assert_eq!(variants[1].0, "Payment");
6487 assert_eq!(variants[1].1.len(), 1);
6488 assert_eq!(variants[1].1[0].0, "reason");
6489
6490 assert_eq!(embeds.len(), 1);
6491 let (source, tag) = &embeds[0];
6492 assert_eq!(tag, "Payment");
6493 let Ty::Named { name, .. } = &*program.program().ty_intern.get(*source) else {
6494 panic!("expected embeds source to resolve to a named type")
6495 };
6496 assert_eq!(name, "PaymentError");
6497 }
6498
6499 #[test]
6500 fn type_item_refined_and_opaque_cover_bare_and_predicated_and_opaque_forms() {
6501 let program = checked_program(
6502 r#"
6503commons demo {
6504 type Age = Int where Positive
6505 type UserId = opaque Int
6506 type Bare = Int
6507}
6508"#,
6509 );
6510 let age = find_type(&program, "Age");
6511 let IrItem::Type {
6512 shape: age_shape, ..
6513 } = lower_type_item_ir(age, &program)
6514 else {
6515 unreachable!()
6516 };
6517 let TypeShape::Refined {
6518 base,
6519 refinement,
6520 opaque,
6521 } = age_shape
6522 else {
6523 panic!("expected TypeShape::Refined for Age")
6524 };
6525 assert_eq!(base, bynk_syntax::ast::BaseType::Int);
6526 assert!(refinement.is_some());
6527 assert!(!opaque);
6528
6529 let user_id = find_type(&program, "UserId");
6530 let IrItem::Type {
6531 shape: user_id_shape,
6532 ..
6533 } = lower_type_item_ir(user_id, &program)
6534 else {
6535 unreachable!()
6536 };
6537 let TypeShape::Refined {
6538 refinement, opaque, ..
6539 } = user_id_shape
6540 else {
6541 panic!("expected TypeShape::Refined for UserId")
6542 };
6543 assert!(refinement.is_none());
6544 assert!(opaque);
6545
6546 let bare = find_type(&program, "Bare");
6547 let IrItem::Type {
6548 shape: bare_shape, ..
6549 } = lower_type_item_ir(bare, &program)
6550 else {
6551 unreachable!()
6552 };
6553 let TypeShape::Refined {
6554 refinement, opaque, ..
6555 } = bare_shape
6556 else {
6557 panic!("expected TypeShape::Refined for Bare")
6558 };
6559 assert!(refinement.is_none());
6560 assert!(!opaque);
6561 }
6562
6563 #[test]
6564 fn fn_item_covers_effectful_and_pure_and_generic_and_method() {
6565 let program = checked_program(
6566 r#"
6567commons demo {
6568 type Box[A] = { value: A }
6569
6570 fn Box.get(self) -> A { self.value }
6571
6572 fn Box.fetch(self) -> Effect[A] { Effect.pure(self.value) }
6573
6574 fn two_params(a: Int, b: Int) -> Int { a }
6575
6576 fn identity[T](x: T) -> T { x }
6577
6578 fn fetch() -> Effect[Int] { Effect.pure(1) }
6579}
6580"#,
6581 );
6582
6583 let two_params = find_fn_arc(&program, "two_params");
6584 let IrItem::Fn {
6585 receiver,
6586 params,
6587 ret,
6588 effectful,
6589 ..
6590 } = lower_fn_item_ir(two_params, &program)
6591 else {
6592 unreachable!()
6593 };
6594 assert!(receiver.is_none(), "a free function has no receiver");
6595 assert_eq!(params.len(), 2);
6596 assert_eq!(params[0].0, "a");
6597 assert_eq!(params[1].0, "b");
6598 assert!(matches!(
6599 &*program.program().ty_intern.get(ret),
6600 Ty::Base(bynk_syntax::ast::BaseType::Int)
6601 ));
6602 assert!(!effectful);
6603
6604 let fetch = find_fn_arc(&program, "fetch");
6605 let IrItem::Fn { effectful, .. } = lower_fn_item_ir(fetch, &program) else {
6606 unreachable!()
6607 };
6608 assert!(effectful);
6609
6610 let identity = find_fn_arc(&program, "identity");
6611 let IrItem::Fn { params, ret, .. } = lower_fn_item_ir(identity, &program) else {
6612 unreachable!()
6613 };
6614 assert!(matches!(
6615 &*program.program().ty_intern.get(params[0].1),
6616 Ty::Var(n) if n == "T"
6617 ));
6618 assert!(matches!(
6619 &*program.program().ty_intern.get(ret),
6620 Ty::Var(n) if n == "T"
6621 ));
6622
6623 let method = find_fn_arc(&program, "Box.get");
6624 let IrItem::Fn {
6625 receiver,
6626 params,
6627 ret,
6628 effectful,
6629 ..
6630 } = lower_fn_item_ir(method, &program)
6631 else {
6632 unreachable!()
6633 };
6634 assert!(params.is_empty(), "self is not a param — see receiver");
6635 let Some(receiver_ty) = receiver else {
6636 panic!("expected Box.get to carry a receiver")
6637 };
6638 let Ty::Named { name, args, .. } = &*program.program().ty_intern.get(receiver_ty) else {
6639 panic!("expected the receiver to resolve to a named type")
6640 };
6641 assert_eq!(name, "Box");
6642 assert_eq!(args.len(), 1);
6643 assert!(matches!(
6644 &*program.program().ty_intern.get(args[0]),
6645 Ty::Var(n) if n == "A"
6646 ));
6647 assert!(matches!(
6648 &*program.program().ty_intern.get(ret),
6649 Ty::Var(n) if n == "A"
6650 ));
6651 assert!(!effectful);
6652
6653 let effectful_method = find_fn_arc(&program, "Box.fetch");
6654 let IrItem::Fn {
6655 receiver,
6656 effectful,
6657 ..
6658 } = lower_fn_item_ir(effectful_method, &program)
6659 else {
6660 unreachable!()
6661 };
6662 assert!(
6663 receiver.is_some(),
6664 "Box.fetch is also a method with a receiver"
6665 );
6666 assert!(effectful, "Effect[A] return makes Box.fetch effectful");
6667 }
6668
6669 #[test]
6670 fn type_item_sum_covers_a_payload_less_variant() {
6671 let program = checked_program(
6672 r#"
6673commons demo {
6674 type PaymentError = enum { Declined, InsufficientFunds }
6675}
6676"#,
6677 );
6678 let decl = find_type(&program, "PaymentError");
6679 let item = lower_type_item_ir(decl, &program);
6680 let IrItem::Type { shape, .. } = &item else {
6681 panic!("expected IrItem::Type, got {item:?}")
6682 };
6683 let TypeShape::Sum { variants, embeds } = shape else {
6684 panic!("expected TypeShape::Sum, got {shape:?}")
6685 };
6686 assert_eq!(variants.len(), 2);
6687 assert_eq!(variants[0].0, "Declined");
6688 assert!(
6689 variants[0].1.is_empty(),
6690 "a bare variant carries no payload"
6691 );
6692 assert_eq!(variants[1].0, "InsufficientFunds");
6693 assert!(variants[1].1.is_empty());
6694 assert!(embeds.is_empty());
6695 }
6696
6697 #[test]
6698 fn type_item_record_drops_a_fields_own_inline_refinement() {
6699 let program = checked_program(
6705 r#"
6706commons demo {
6707 type Account = { balance: Int where NonNegative }
6708}
6709"#,
6710 );
6711 let decl = find_type(&program, "Account");
6712 let item = lower_type_item_ir(decl, &program);
6713 let IrItem::Type { shape, .. } = &item else {
6714 panic!("expected IrItem::Type, got {item:?}")
6715 };
6716 let TypeShape::Record { fields } = shape else {
6717 panic!("expected TypeShape::Record, got {shape:?}")
6718 };
6719 assert_eq!(fields.len(), 1);
6720 assert_eq!(fields[0].0, "balance");
6721 assert!(matches!(
6722 &*program.program().ty_intern.get(fields[0].1),
6723 Ty::Base(bynk_syntax::ast::BaseType::Int)
6724 ));
6725 }
6726
6727 #[test]
6728 fn type_item_record_resolves_a_generic_type_application_field() {
6729 let program = checked_program(
6734 r#"
6735commons demo {
6736 type Box[T] = { value: T }
6737 type Wrapper = { boxed: Box[Int] }
6738}
6739"#,
6740 );
6741 let decl = find_type(&program, "Wrapper");
6742 let item = lower_type_item_ir(decl, &program);
6743 let IrItem::Type { shape, .. } = &item else {
6744 panic!("expected IrItem::Type, got {item:?}")
6745 };
6746 let TypeShape::Record { fields } = shape else {
6747 panic!("expected TypeShape::Record, got {shape:?}")
6748 };
6749 assert_eq!(fields.len(), 1);
6750 assert_eq!(fields[0].0, "boxed");
6751 let Ty::Named { name, args, .. } = &*program.program().ty_intern.get(fields[0].1) else {
6752 panic!("expected `boxed` to resolve to a named type")
6753 };
6754 assert_eq!(name, "Box");
6755 assert_eq!(args.len(), 1);
6756 assert!(matches!(
6757 &*program.program().ty_intern.get(args[0]),
6758 Ty::Base(bynk_syntax::ast::BaseType::Int)
6759 ));
6760 }
6761
6762 fn checked_context_program(source: &str) -> CheckedProgram {
6799 let tokens = lexer::tokenize(source).expect("lex");
6800 let unit = parser::parse_unit(&tokens, source).expect("parse");
6801 let SourceUnit::Context(mut ctx) = unit else {
6802 panic!("expected a context unit, got {unit:?}")
6803 };
6804 for item in &mut ctx.items {
6805 if let CommonsItem::Service(svc) = item {
6806 bynk_check::project_model::inject_service_defaults(svc);
6807 }
6808 }
6809 let commons = Commons {
6810 name: ctx.name,
6811 items: ctx.items,
6812 uses: ctx.uses,
6813 documentation: ctx.documentation,
6814 form: ctx.form,
6815 span: ctx.span,
6816 trivia: ctx.trivia,
6817 trailing_comments: ctx.trailing_comments,
6818 };
6819 let resolved = resolver::resolve(commons).expect("resolve");
6820 let mut typed = checker::check(resolved).expect("check");
6821 let agents: HashMap<String, AgentDecl> = typed
6822 .commons
6823 .items
6824 .iter()
6825 .filter_map(|item| match item {
6826 CommonsItem::Agent(a) => Some((a.name.name.clone(), a.clone())),
6827 _ => None,
6828 })
6829 .collect();
6830 let services: HashMap<String, ServiceDecl> = typed
6831 .commons
6832 .items
6833 .iter()
6834 .filter_map(|item| match item {
6835 CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
6836 _ => None,
6837 })
6838 .collect();
6839 let actors: HashMap<String, bynk_syntax::ast::ActorDecl> = typed
6840 .commons
6841 .items
6842 .iter()
6843 .filter_map(|item| match item {
6844 CommonsItem::Actor(a) => Some((a.name.name.clone(), a.clone())),
6845 _ => None,
6846 })
6847 .collect();
6848 let capabilities: HashMap<String, bynk_syntax::ast::CapabilityDecl> = typed
6856 .commons
6857 .items
6858 .iter()
6859 .filter_map(|item| match item {
6860 CommonsItem::Capability(c) => Some((c.name.name.clone(), c.clone())),
6861 _ => None,
6862 })
6863 .collect();
6864 let providers: HashMap<String, ProviderDecl> = typed
6874 .commons
6875 .items
6876 .iter()
6877 .filter_map(|item| match item {
6878 CommonsItem::Provider(p) => Some((p.capability.name.clone(), p.clone())),
6879 _ => None,
6880 })
6881 .collect();
6882 let table = symbols::UnitTable {
6883 kind: Some(UnitKind::Context),
6884 types: typed.types.clone(),
6885 agents,
6886 services,
6887 actors,
6888 capabilities,
6889 providers,
6890 ..symbols::UnitTable::default()
6891 };
6892 let tys = typed.ty_intern.clone();
6893 let errors = context_checks::check_context_declarations(
6894 &mut typed,
6895 &table,
6896 &resolver::CrossContextInfo::default(),
6897 true,
6898 &HashSet::new(),
6899 &HashMap::new(),
6900 &mut RefSink::new(),
6901 &mut HintSink::new(),
6902 &mut LocalsSink::new(),
6903 &mut RequirementSink::new(),
6904 &tys,
6905 );
6906 checker::certify(typed, errors).expect("certify")
6907 }
6908
6909 fn find_agent<'a>(program: &'a CheckedProgram, name: &str) -> &'a AgentDecl {
6910 program
6911 .program()
6912 .commons
6913 .items
6914 .iter()
6915 .find_map(|item| match item {
6916 CommonsItem::Agent(a) if a.name.name == name => Some(a),
6917 _ => None,
6918 })
6919 .unwrap_or_else(|| panic!("no agent named `{name}` in this fixture"))
6920 }
6921
6922 fn actors_map(program: &CheckedProgram) -> HashMap<String, ActorDecl> {
6928 program
6929 .program()
6930 .commons
6931 .items
6932 .iter()
6933 .filter_map(|item| match item {
6934 CommonsItem::Actor(a) => Some((a.name.name.clone(), a.clone())),
6935 _ => None,
6936 })
6937 .collect()
6938 }
6939
6940 fn find_service<'a>(program: &'a CheckedProgram, name: &str) -> &'a ServiceDecl {
6941 program
6942 .program()
6943 .commons
6944 .items
6945 .iter()
6946 .find_map(|item| match item {
6947 CommonsItem::Service(s) if s.name.name == name => Some(s),
6948 _ => None,
6949 })
6950 .unwrap_or_else(|| panic!("no service named `{name}` in this fixture"))
6951 }
6952
6953 fn find_capability<'a>(program: &'a CheckedProgram, name: &str) -> &'a CapabilityDecl {
6954 program
6955 .program()
6956 .commons
6957 .items
6958 .iter()
6959 .find_map(|item| match item {
6960 CommonsItem::Capability(c) if c.name.name == name => Some(c),
6961 _ => None,
6962 })
6963 .unwrap_or_else(|| panic!("no capability named `{name}` in this fixture"))
6964 }
6965
6966 fn find_provider<'a>(program: &'a CheckedProgram, name: &str) -> &'a ProviderDecl {
6967 program
6968 .program()
6969 .commons
6970 .items
6971 .iter()
6972 .find_map(|item| match item {
6973 CommonsItem::Provider(p) if p.provider_name.name == name => Some(p),
6974 _ => None,
6975 })
6976 .unwrap_or_else(|| panic!("no provider named `{name}` in this fixture"))
6977 }
6978
6979 fn find_service_handler<'a>(service: &'a ServiceDecl, kind: &HandlerKind) -> &'a Handler {
6987 service
6988 .handlers
6989 .iter()
6990 .find(|h| &h.kind == kind)
6991 .unwrap_or_else(|| {
6992 panic!(
6993 "no handler of kind {kind:?} on service `{}`",
6994 service.name.name
6995 )
6996 })
6997 }
6998
6999 fn find_store_field<'a>(agent: &'a AgentDecl, name: &str) -> &'a StoreField {
7000 agent
7001 .store_fields
7002 .iter()
7003 .find(|f| f.name.name == name)
7004 .unwrap_or_else(|| {
7005 panic!(
7006 "no store field named `{name}` on agent `{}`",
7007 agent.name.name
7008 )
7009 })
7010 }
7011
7012 #[test]
7013 fn store_field_cell_with_initialiser_and_without() {
7014 let program = checked_context_program(
7015 r#"
7016context demo
7017
7018agent Counter {
7019 key id: String
7020 store balance: Cell[Int] = 0
7021 store hint: Cell[String]
7022
7023 on call touch() -> Effect[()] {
7024 Effect.pure(())
7025 }
7026}
7027"#,
7028 );
7029 let agent = find_agent(&program, "Counter");
7030
7031 let balance = find_store_field(agent, "balance");
7032 let ir = lower_store_field_ir(balance, &program);
7033 assert_eq!(ir.field, "balance");
7034 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7035 assert!(
7036 ir.init.is_some(),
7037 "a Cell field with an initialiser lowers a real init expression"
7038 );
7039 assert!(ir.indexed.is_empty());
7040
7041 let hint = find_store_field(agent, "hint");
7042 let ir = lower_store_field_ir(hint, &program);
7043 assert_eq!(ir.field, "hint");
7044 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7045 assert!(
7046 ir.init.is_none(),
7047 "Decision D's own boundary: a Cell field with no initialiser lowers to None, \
7048 not merely reached by omission"
7049 );
7050 }
7051
7052 #[test]
7069 fn store_field_shape_ir_does_not_panic_on_none_or_is_initialisers() {
7070 let program = checked_context_program(
7071 r#"
7072context demo
7073
7074type AuthId = String where NonEmpty
7075
7076agent Order {
7077 key id: String
7078 store paymentRef: Cell[Option[AuthId]] = None
7079
7080 on call touch() -> Effect[()] {
7081 Effect.pure(())
7082 }
7083}
7084"#,
7085 );
7086 let agent = find_agent(&program, "Order");
7087 let payment_ref = find_store_field(agent, "paymentRef");
7088 let ir = lower_store_field_shape_ir(payment_ref, &program);
7089 assert_eq!(ir.field, "paymentRef");
7090 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7091 assert!(
7092 ir.init.is_none(),
7093 "the shape-only reader never lowers init, regardless of the source field"
7094 );
7095
7096 let program = checked_context_program(
7097 r#"
7098context demo
7099
7100type PositiveInt = Int where Positive
7101
7102agent Meter {
7103 key id: String
7104 store active: Cell[Bool] = if true { 5 is PositiveInt } else { false }
7105
7106 on call touch() -> Effect[()] {
7107 Effect.pure(())
7108 }
7109}
7110"#,
7111 );
7112 let agent = find_agent(&program, "Meter");
7113 let active = find_store_field(agent, "active");
7114 let ir = lower_store_field_shape_ir(active, &program);
7115 assert_eq!(ir.field, "active");
7116 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7117 assert!(ir.init.is_none());
7118 }
7119
7120 #[test]
7121 fn store_field_falls_back_to_unit_on_an_unresolvable_type_like_the_checker_does() {
7122 let program = checked_context_program(
7138 r#"
7139context demo
7140
7141agent Widget {
7142 key id: String
7143 store x: Cell[Bogus] = "hello"
7144
7145 on call touch() -> Effect[()] {
7146 Effect.pure(())
7147 }
7148}
7149"#,
7150 );
7151 let agent = find_agent(&program, "Widget");
7152 let x = find_store_field(agent, "x");
7153 let shape = lower_store_field_shape_ir(x, &program);
7157 let StoreKindIr::Cell(ty) = shape.kind else {
7158 panic!("expected StoreKindIr::Cell, got {:?}", shape.kind)
7159 };
7160 assert!(matches!(&*program.program().ty_intern.get(ty), Ty::Unit));
7161 }
7162
7163 #[test]
7164 fn store_field_cell_init_qualified_constructor_call_lowers_without_panicking() {
7165 let program = checked_context_program(
7177 r#"
7178context demo
7179
7180type PositiveId = opaque Int where NonNegative
7181
7182agent Counter {
7183 key id: String
7184 store n: Cell[PositiveId] = PositiveId.unsafe(1)
7185
7186 on call touch() -> Effect[()] {
7187 Effect.pure(())
7188 }
7189}
7190"#,
7191 );
7192 let agent = find_agent(&program, "Counter");
7193 let n = find_store_field(agent, "n");
7194 let ir = lower_store_field_ir(n, &program);
7195 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7196 assert!(ir.init.is_some());
7197 }
7198
7199 #[test]
7210 fn store_field_cell_option_init_none_lowers_without_panicking() {
7211 let program = checked_context_program(
7212 r#"
7213context demo
7214
7215type AuthId = String where NonEmpty
7216
7217agent Order {
7218 key id: String
7219 store paymentRef: Cell[Option[AuthId]] = None
7220
7221 on call touch() -> Effect[()] {
7222 Effect.pure(())
7223 }
7224}
7225"#,
7226 );
7227 let agent = find_agent(&program, "Order");
7228 let payment_ref = find_store_field(agent, "paymentRef");
7229 let ir = lower_store_field_ir(payment_ref, &program);
7230 assert!(matches!(ir.kind, StoreKindIr::Cell(_)));
7231 let Some(init) = &ir.init else {
7232 panic!("expected a lowered `None` initialiser, got None")
7233 };
7234 assert!(matches!(
7235 &init.kind,
7236 IrExprKind::Variant { tag, payload } if tag == "None" && payload.is_empty()
7237 ));
7238 }
7239
7240 #[test]
7241 fn store_field_map_indexed_zero_one_and_multiple() {
7242 let program = checked_context_program(
7243 r#"
7244context demo
7245
7246type Reservation = {
7247 id: String,
7248 orderId: String,
7249 status: String,
7250}
7251
7252agent Inventory {
7253 key sku: String
7254 store noIndex: Map[String, Reservation]
7255 store oneIndex: Map[String, Reservation] @indexed(by: orderId)
7256 store twoIndex: Map[String, Reservation] @indexed(by: orderId, by: status)
7257 store dupIndex: Map[String, Reservation] @indexed(by: orderId, by: orderId)
7258
7259 on call touch() -> Effect[()] {
7260 Effect.pure(())
7261 }
7262}
7263"#,
7264 );
7265 let agent = find_agent(&program, "Inventory");
7266
7267 let no_index = find_store_field(agent, "noIndex");
7268 let ir = lower_store_field_ir(no_index, &program);
7269 assert!(matches!(ir.kind, StoreKindIr::Map(_, _)));
7270 assert!(ir.indexed.is_empty());
7271 assert!(
7272 ir.init.is_none(),
7273 "Decision D: init is None for a non-Cell kind"
7274 );
7275
7276 let one_index = find_store_field(agent, "oneIndex");
7277 let ir = lower_store_field_ir(one_index, &program);
7278 assert_eq!(ir.indexed, vec!["orderId".to_string()]);
7279
7280 let two_index = find_store_field(agent, "twoIndex");
7281 let ir = lower_store_field_ir(two_index, &program);
7282 assert_eq!(
7283 ir.indexed,
7284 vec!["orderId".to_string(), "status".to_string()],
7285 "Vec<IndexIr>'s own multi-entry case, in the annotation's own by: order"
7286 );
7287
7288 let dup_index = find_store_field(agent, "dupIndex");
7295 let ir = lower_store_field_ir(dup_index, &program);
7296 assert_eq!(
7297 ir.indexed,
7298 vec!["orderId".to_string()],
7299 "a duplicate by: key collapses to one sibling-table entry"
7300 );
7301 }
7302
7303 #[test]
7304 fn store_field_set_element_type() {
7305 let program = checked_context_program(
7306 r#"
7307context demo
7308
7309agent Tags {
7310 key id: String
7311 store tags: Set[String]
7312
7313 on call touch() -> Effect[()] {
7314 Effect.pure(())
7315 }
7316}
7317"#,
7318 );
7319 let agent = find_agent(&program, "Tags");
7320 let tags = find_store_field(agent, "tags");
7321 let ir = lower_store_field_ir(tags, &program);
7322 assert_eq!(ir.field, "tags");
7323 assert!(matches!(ir.kind, StoreKindIr::Set(_)));
7324 assert!(ir.init.is_none());
7325 assert!(ir.indexed.is_empty());
7326 }
7327
7328 #[test]
7329 fn store_field_cache_resolves_ttl_millis() {
7330 let program = checked_context_program(
7331 r#"
7332context demo
7333
7334agent Sessions {
7335 key id: String
7336 store live: Cache[String, Int] @ttl(5.minutes)
7337
7338 on call touch() -> Effect[()] {
7339 Effect.pure(())
7340 }
7341}
7342"#,
7343 );
7344 let agent = find_agent(&program, "Sessions");
7345 let live = find_store_field(agent, "live");
7346 let ir = lower_store_field_ir(live, &program);
7347 assert_eq!(ir.field, "live");
7348 let StoreKindIr::Cache(_, _, ttl) = ir.kind else {
7349 panic!("expected StoreKindIr::Cache, got {:?}", ir.kind)
7350 };
7351 assert_eq!(
7352 ttl,
7353 5 * 60 * 1000,
7354 "5.minutes' own DurationLit.millis, not re-derived arithmetic"
7355 );
7356 }
7357
7358 #[test]
7359 fn store_field_log_with_and_without_retain() {
7360 let program = checked_context_program(
7361 r#"
7362context demo
7363
7364agent Inventory {
7365 key id: String
7366 store historyWithRetain: Log[String] @retain(30.days)
7367 store historyNoRetain: Log[String]
7368
7369 on call touch() -> Effect[()] {
7370 Effect.pure(())
7371 }
7372}
7373"#,
7374 );
7375 let agent = find_agent(&program, "Inventory");
7376
7377 let with_retain = find_store_field(agent, "historyWithRetain");
7378 let ir = lower_store_field_ir(with_retain, &program);
7379 let StoreKindIr::Log(_, retain) = ir.kind else {
7380 panic!("expected StoreKindIr::Log, got {:?}", ir.kind)
7381 };
7382 assert_eq!(retain, Some(30 * 24 * 60 * 60 * 1000));
7383
7384 let no_retain = find_store_field(agent, "historyNoRetain");
7385 let ir = lower_store_field_ir(no_retain, &program);
7386 let StoreKindIr::Log(_, retain) = ir.kind else {
7387 panic!("expected StoreKindIr::Log, got {:?}", ir.kind)
7388 };
7389 assert_eq!(retain, None, "@retain is genuinely optional on Log");
7390 }
7391
7392 fn find_handler<'a>(agent: &'a AgentDecl, name: &str) -> &'a Handler {
7393 agent
7394 .handlers
7395 .iter()
7396 .find(|h| h.method_name.as_ref().is_some_and(|m| m.name == name))
7397 .unwrap_or_else(|| panic!("no handler named `{name}` on agent `{}`", agent.name.name))
7398 }
7399
7400 fn agent_state_ty(program: &CheckedProgram, agent_name: &str) -> TyId {
7408 program.program().ty_intern.intern(Ty::Named {
7409 name: format!("{agent_name}State"),
7410 kind: checker::NamedKind::Record,
7411 args: Vec::new(),
7412 })
7413 }
7414
7415 fn agent_store_cells(program: &CheckedProgram, agent: &AgentDecl) -> HashMap<String, TyId> {
7420 agent
7421 .store_fields
7422 .iter()
7423 .filter(|f| f.kind.head.name == "Cell")
7424 .map(|f| {
7425 let ir = lower_store_field_ir(f, program);
7426 let StoreKindIr::Cell(ty) = ir.kind else {
7427 unreachable!("filtered to Cell fields above")
7428 };
7429 (ir.field, ty)
7430 })
7431 .collect()
7432 }
7433
7434 fn agent_store_queryable(_program: &CheckedProgram, agent: &AgentDecl) -> HashSet<String> {
7439 agent
7440 .store_fields
7441 .iter()
7442 .filter(|f| f.kind.head.name == "Map")
7443 .map(|f| f.name.name.clone())
7444 .collect()
7445 }
7446
7447 #[test]
7448 fn invariant_ir_references_a_store_cell_by_bare_name() {
7449 let program = checked_context_program(
7450 r#"
7451context demo
7452
7453agent Counter {
7454 key id: String
7455 store active: Cell[Bool] = true
7456
7457 invariant staysKnown: active
7458
7459 on call touch() -> Effect[()] {
7460 Effect.pure(())
7461 }
7462}
7463"#,
7464 );
7465 let agent = find_agent(&program, "Counter");
7466 let store_cells = agent_store_cells(&program, agent);
7467 let inv = agent
7468 .invariants
7469 .iter()
7470 .find(|i| i.name.name == "staysKnown")
7471 .expect("invariant `staysKnown` in this fixture");
7472 let ir = lower_invariant_ir(inv, &store_cells, &program);
7473 assert_eq!(ir.name, "staysKnown");
7474 assert!(
7475 matches!(&ir.predicate.kind, IrExprKind::Local(n) if n == "active"),
7476 "a bare-name read of a store Cell field lowers to Local, got {:?}",
7477 ir.predicate.kind
7478 );
7479 }
7480
7481 #[test]
7482 fn transition_ir_references_both_old_and_new() {
7483 let program = checked_context_program(
7484 r#"
7485context demo
7486
7487agent Counter {
7488 key id: String
7489 store active: Cell[Bool] = true
7490
7491 transition activeImplication: old.active implies new.active
7492
7493 on call touch() -> Effect[()] {
7494 Effect.pure(())
7495 }
7496}
7497"#,
7498 );
7499 let agent = find_agent(&program, "Counter");
7500 let state_ty = agent_state_ty(&program, "Counter");
7501 let tr = agent
7502 .transitions
7503 .iter()
7504 .find(|t| t.name.name == "activeImplication")
7505 .expect("transition `activeImplication` in this fixture");
7506 let ir = lower_transition_ir(tr, state_ty, &program);
7507 assert_eq!(ir.name, "activeImplication");
7508 let IrExprKind::Or { lhs, rhs } = &ir.predicate.kind else {
7512 panic!(
7513 "expected `implies` to lower to Or, got {:?}",
7514 ir.predicate.kind
7515 )
7516 };
7517 let IrExprKind::Not { operand } = &lhs.kind else {
7518 panic!("expected Or's own lhs to be Not, got {:?}", lhs.kind)
7519 };
7520 let IrExprKind::Field { base, field } = &operand.kind else {
7521 panic!(
7522 "expected `old.active` to lower to Field, got {:?}",
7523 operand.kind
7524 )
7525 };
7526 assert!(matches!(&base.kind, IrExprKind::Local(n) if n == "old"));
7527 assert_eq!(field, "active");
7528 let IrExprKind::Field { base, field } = &rhs.kind else {
7529 panic!(
7530 "expected `new.active` to lower to Field, got {:?}",
7531 rhs.kind
7532 )
7533 };
7534 assert!(matches!(&base.kind, IrExprKind::Local(n) if n == "new"));
7535 assert_eq!(field, "active");
7536 }
7537
7538 fn commit_shape_fixture() -> CheckedProgram {
7542 checked_context_program(
7543 r#"
7544context demo
7545
7546type Box = { n: Int }
7547
7548fn Box.put(self, x: Int) -> Effect[()] {
7549 Effect.pure(())
7550}
7551
7552capability Clock {
7553 fn now() -> Effect[Int]
7554}
7555
7556provides Clock = FixedClock {
7557 fn now() -> Effect[Int] {
7558 42
7559 }
7560}
7561
7562agent Widget {
7563 key id: String
7564 store items: Map[String, Int]
7565 store active: Cell[Bool] = true
7566 store tags: Set[String]
7567 store history: Log[String]
7568
7569 on call readOnlyPlain() -> Effect[()] {
7570 Effect.pure(())
7571 }
7572
7573 on call readOnlyQuery() -> Effect[Int] {
7574 items.size()
7575 }
7576
7577 on call nestedMutation(xs: List[String], flag: Bool) -> Effect[()] {
7578 if flag {
7579 match flag {
7580 true => xs.forEach((x) => items.put(x, 1))
7581 false => Effect.pure(())
7582 }
7583 } else {
7584 Effect.pure(())
7585 }
7586 }
7587
7588 on call bareAssign(v: Bool) -> Effect[()] {
7589 active := v
7590 Effect.pure(())
7591 }
7592
7593 on call shadowedName(items: Box, x: Int) -> Effect[()] {
7594 let _ <- items.put(x)
7595 Effect.pure(())
7596 }
7597
7598 on call cellUpdate() -> Effect[()] {
7599 let _ <- active.update((b) => !b)
7600 Effect.pure(())
7601 }
7602
7603 on call setAdd(t: String) -> Effect[()] {
7604 let _ <- tags.add(t)
7605 Effect.pure(())
7606 }
7607
7608 on call logAppend(t: String) -> Effect[()] given Clock {
7609 let _ <- history.append(t)
7610 Effect.pure(())
7611 }
7612}
7613"#,
7614 )
7615 }
7616
7617 fn commit_shape_of(program: &CheckedProgram, handler_name: &str) -> CommitShape {
7618 let agent = find_agent(program, "Widget");
7619 let handler = find_handler(agent, handler_name);
7620 let emits = block_uses_emit(&handler.body, &program.program().callees);
7621 lower_commit_shape_ir(&handler.body, &[], &[], emits, program)
7622 }
7623
7624 #[test]
7625 fn commit_shape_read_only_for_a_plain_body() {
7626 let program = commit_shape_fixture();
7627 assert!(matches!(
7628 commit_shape_of(&program, "readOnlyPlain"),
7629 CommitShape::ReadOnly
7630 ));
7631 }
7632
7633 #[test]
7634 fn commit_shape_read_only_for_a_non_mutating_store_read() {
7635 let program = commit_shape_fixture();
7639 assert!(matches!(
7640 commit_shape_of(&program, "readOnlyQuery"),
7641 CommitShape::ReadOnly
7642 ));
7643 }
7644
7645 #[test]
7646 fn commit_shape_flush_events_for_an_emit_only_body() {
7647 let program = commit_shape_fixture();
7656 let agent = find_agent(&program, "Widget");
7657 let handler = find_handler(agent, "readOnlyPlain");
7658 let shape = lower_commit_shape_ir(&handler.body, &[], &[], true, &program);
7659 assert!(matches!(shape, CommitShape::FlushEvents));
7660 }
7661
7662 #[test]
7663 fn commit_shape_transactional_for_a_write_nested_in_if_match_lambda() {
7664 let program = commit_shape_fixture();
7669 assert!(matches!(
7670 commit_shape_of(&program, "nestedMutation"),
7671 CommitShape::Transactional { .. }
7672 ));
7673 }
7674
7675 #[test]
7676 fn commit_shape_transactional_for_a_bare_cell_assign() {
7677 let program = commit_shape_fixture();
7678 assert!(matches!(
7679 commit_shape_of(&program, "bareAssign"),
7680 CommitShape::Transactional { .. }
7681 ));
7682 }
7683
7684 #[test]
7685 fn commit_shape_read_only_for_a_locally_shadowed_store_field_name() {
7686 let program = commit_shape_fixture();
7695 assert!(matches!(
7696 commit_shape_of(&program, "shadowedName"),
7697 CommitShape::ReadOnly
7698 ));
7699 }
7700
7701 #[test]
7702 fn commit_shape_transactional_for_a_cell_update_method_call() {
7703 let program = commit_shape_fixture();
7708 assert!(matches!(
7709 commit_shape_of(&program, "cellUpdate"),
7710 CommitShape::Transactional { .. }
7711 ));
7712 }
7713
7714 #[test]
7715 fn commit_shape_transactional_for_a_set_add_method_call() {
7716 let program = commit_shape_fixture();
7719 assert!(matches!(
7720 commit_shape_of(&program, "setAdd"),
7721 CommitShape::Transactional { .. }
7722 ));
7723 }
7724
7725 #[test]
7726 fn commit_shape_transactional_for_a_log_append_method_call() {
7727 let program = commit_shape_fixture();
7729 assert!(matches!(
7730 commit_shape_of(&program, "logAppend"),
7731 CommitShape::Transactional { .. }
7732 ));
7733 }
7734
7735 #[test]
7736 fn commit_shape_transactional_carries_its_own_invariants_and_transitions_unswapped() {
7737 let program = commit_shape_fixture();
7742 let agent = find_agent(&program, "Widget");
7743 let handler = find_handler(agent, "bareAssign");
7744 let bool_ty = program
7745 .program()
7746 .ty_intern
7747 .intern(Ty::Base(bynk_syntax::ast::BaseType::Bool));
7748 let mk = |name: &str| IrPredicate {
7749 name: name.to_string(),
7750 predicate: IrExpr {
7751 kind: IrExprKind::Const(ConstVal::Bool(true)),
7752 ty: bool_ty,
7753 span: Span::new(0, 0),
7754 },
7755 };
7756 let invariants = vec![mk("invOnly")];
7757 let transitions = vec![mk("transitionOnly")];
7758 let shape =
7759 lower_commit_shape_ir(&handler.body, &invariants, &transitions, false, &program);
7760 let CommitShape::Transactional {
7761 invariants: got_inv,
7762 transitions: got_tr,
7763 } = shape
7764 else {
7765 panic!("expected Transactional, got a different CommitShape")
7766 };
7767 assert_eq!(got_inv.len(), 1);
7768 assert_eq!(got_inv[0].name, "invOnly");
7769 assert_eq!(got_tr.len(), 1);
7770 assert_eq!(got_tr[0].name, "transitionOnly");
7771 }
7772
7773 fn handler_ir_fixture() -> CheckedProgram {
7784 checked_context_program(
7785 r#"
7786context demo
7787
7788capability Clock {
7789 fn now() -> Effect[Int]
7790}
7791
7792provides Clock = FixedClock {
7793 fn now() -> Effect[Int] {
7794 42
7795 }
7796}
7797
7798fn passThroughQuery(q: Query[Int]) -> Query[Int] {
7799 q
7800}
7801
7802-- P6.20-pre (review of #1240): a free fn sharing a name with `Ledger`'s own
7803-- `entries` store field — pins that the store-field dispatch wins (checked
7804-- immediately after `cx.lookup`, ahead of the free-fn probe), not the other
7805-- way around.
7806fn entries(n: Int) -> Int {
7807 n
7808}
7809
7810agent Ledger {
7811 key id: String
7812 store balance: Cell[Int] = 0
7813 store entries: Map[String, Int]
7814
7815 on call peek() -> Effect[Int] {
7816 Effect.pure(balance)
7817 }
7818
7819 on call deposit(amount: Int) -> Effect[()] {
7820 balance := amount
7821 Effect.pure(())
7822 }
7823
7824 on call touchClock() -> Effect[Int] given Clock {
7825 let t <- Clock.now()
7826 Effect.pure(t)
7827 }
7828
7829 on call addEntry(k: String, v: Int) -> Effect[()] {
7830 let _ <- entries.put(k, v)
7831 Effect.pure(())
7832 }
7833
7834 -- P6.20-pre: `.keys` on a bare store `Map` field — the FieldAccess
7835 -- receiver-position shape (ADR 0184) that panicked as `Inventory`/`items`,
7836 -- `Ledger`/`balances` in the real fixture corpus.
7837 on call entryKeys() -> Effect[Query[String]] {
7838 Effect.pure(entries.keys)
7839 }
7840
7841 -- P6.20-pre: a bare store `Map` field passed as a plain argument — the
7842 -- argument-position shape (ADR 0120) that panicked as `Sales`/`orders` in
7843 -- the real fixture corpus (`lines.joinOn(orders, ...)`).
7844 on call passEntries() -> Effect[Query[Int]] {
7845 Effect.pure(passThroughQuery(entries))
7846 }
7847
7848 -- P6.20-pre (review of #1240): the store field `entries` and the free fn
7849 -- `entries` (declared above, colliding by name) both exist — the checker
7850 -- resolves the bare reference to the store field's own `Query[Int]`
7851 -- before `check_ident` (where a free-fn reference would resolve) ever
7852 -- sees it, so this must lower to StoreQuery, not panic on the free-fn arm.
7853 on call bareEntriesCollidesWithAFreeFn() -> Effect[Query[Int]] {
7854 Effect.pure(entries)
7855 }
7856
7857 -- P6.20-pre (review of #1240): a handler param named `entries` shadows
7858 -- the store field of the same name — must lower to Local, not StoreQuery.
7859 on call shadowedEntries(entries: Query[Int]) -> Effect[Query[Int]] {
7860 Effect.pure(entries)
7861 }
7862}
7863"#,
7864 )
7865 }
7866
7867 fn handler_ir_of(program: &CheckedProgram, handler_name: &str) -> IrHandler {
7868 handler_ir_of_with_predicates(program, handler_name, &[], &[])
7869 }
7870
7871 fn handler_ir_of_with_predicates(
7872 program: &CheckedProgram,
7873 handler_name: &str,
7874 invariants: &[IrPredicate],
7875 transitions: &[IrPredicate],
7876 ) -> IrHandler {
7877 let agent = find_agent(program, "Ledger");
7878 let handler = find_handler(agent, handler_name);
7879 let store_cells = agent_store_cells(program, agent);
7880 let store_queryable = agent_store_queryable(program, agent);
7881 let state_ty = agent_state_ty(program, "Ledger");
7882 lower_handler_ir(
7883 handler,
7884 &store_cells,
7885 &store_queryable,
7886 state_ty,
7887 invariants,
7888 transitions,
7889 program,
7890 )
7891 }
7892
7893 #[test]
7894 fn handler_ir_read_only_handler_has_no_binder_and_read_only_commit() {
7895 let program = handler_ir_fixture();
7896 let ir = handler_ir_of(&program, "peek");
7897 assert!(
7898 ir.binder.is_none(),
7899 "Decision D: an agent handler's own binder is always None, pinned explicitly rather \
7900 than left to omission"
7901 );
7902 assert_eq!(ir.kind, IrHandlerKind::Call);
7903 assert!(matches!(ir.commit, CommitShape::ReadOnly));
7904 assert_eq!(ir.method_name.as_deref(), Some("peek"));
7905 assert!(ir.effectful, "peek returns Effect[Int]");
7906 assert!(ir.params.is_empty());
7907 assert!(ir.given.is_empty());
7908 let IrExprKind::Block { stmts, tail } = &ir.body.kind else {
7909 panic!("expected a Block, got {:?}", ir.body.kind)
7910 };
7911 assert!(stmts.is_empty());
7912 let IrExprKind::Return { value } = &tail.kind else {
7913 panic!(
7914 "expected the tail to be wrapped in Return, got {:?}",
7915 tail.kind
7916 )
7917 };
7918 let IrExprKind::Pure { value } = &value.kind else {
7919 panic!(
7920 "expected `Effect.pure(balance)` to lower to Pure, got {:?}",
7921 value.kind
7922 )
7923 };
7924 assert!(
7925 matches!(&value.kind, IrExprKind::Local(n) if n == "balance"),
7926 "a bare-name read of a store Cell field lowers to Local, got {:?}",
7927 value.kind
7928 );
7929 }
7930
7931 #[test]
7932 fn handler_ir_store_writing_handler_lowers_assign_and_transactional_commit() {
7933 let program = handler_ir_fixture();
7934 let ir = handler_ir_of(&program, "deposit");
7935 assert!(ir.binder.is_none());
7936 let int_ty = program
7937 .program()
7938 .ty_intern
7939 .intern(Ty::Base(bynk_syntax::ast::BaseType::Int));
7940 assert_eq!(
7941 ir.params,
7942 vec![("amount".to_string(), int_ty)],
7943 "a declared param's type must resolve and bind into the body's own scope"
7944 );
7945 assert_eq!(ir.method_name.as_deref(), Some("deposit"));
7946 let CommitShape::Transactional {
7947 invariants,
7948 transitions,
7949 } = &ir.commit
7950 else {
7951 panic!("expected Transactional, got {:?}", ir.commit)
7952 };
7953 assert!(invariants.is_empty());
7959 assert!(transitions.is_empty());
7960 let IrExprKind::Block { stmts, .. } = &ir.body.kind else {
7961 panic!("expected a Block, got {:?}", ir.body.kind)
7962 };
7963 assert_eq!(
7964 stmts.len(),
7965 1,
7966 "the `:=` write is the body's only statement"
7967 );
7968 let IrStmt::Assign { field, value } = &stmts[0] else {
7969 panic!("expected IrStmt::Assign, got {:?}", stmts[0])
7970 };
7971 assert_eq!(field, "balance");
7972 assert!(
7973 matches!(&value.kind, IrExprKind::Local(n) if n == "amount"),
7974 "the assigned value reads the handler's own `amount` param, got {:?}",
7975 value.kind
7976 );
7977 }
7978
7979 #[test]
7980 fn handler_ir_given_capability_recorded_and_call_lowers_as_ordinary_callee() {
7981 let program = handler_ir_fixture();
7982 let ir = handler_ir_of(&program, "touchClock");
7983 assert!(ir.binder.is_none());
7984 assert_eq!(ir.given, vec!["Clock".to_string()]);
7985 let IrExprKind::Block { stmts, .. } = &ir.body.kind else {
7986 panic!("expected a Block, got {:?}", ir.body.kind)
7987 };
7988 assert_eq!(stmts.len(), 1);
7989 let IrStmt::Let { local, value } = &stmts[0] else {
7990 panic!("expected IrStmt::Let, got {:?}", stmts[0])
7991 };
7992 assert_eq!(local, "t");
7993 let IrExprKind::Await { effect } = &value.kind else {
7994 panic!(
7995 "expected `let t <- Clock.now()` to lower to Await, got {:?}",
7996 value.kind
7997 )
7998 };
7999 let IrExprKind::Call { callee, args, .. } = &effect.kind else {
8000 panic!(
8001 "expected `Clock.now()` to lower as an ordinary Callee-classified Call \
8002 (Decision F — no new call-lowering logic needed), got {:?}",
8003 effect.kind
8004 )
8005 };
8006 assert!(
8007 matches!(callee, Callee::Capability { cap, op } if cap == "Clock" && op == "now"),
8008 "expected Callee::Capability {{ cap: \"Clock\", op: \"now\" }}, got {callee:?}"
8009 );
8010 assert!(args.is_empty());
8011 }
8012
8013 #[test]
8014 fn handler_ir_store_method_call_on_a_non_cell_field_lowers_as_ordinary_callee() {
8015 let program = handler_ir_fixture();
8022 let ir = handler_ir_of(&program, "addEntry");
8023 assert!(ir.binder.is_none());
8024 assert!(
8025 matches!(ir.commit, CommitShape::Transactional { .. }),
8026 "Map.put is a mutating Callee::Store op"
8027 );
8028 let IrExprKind::Block { stmts, .. } = &ir.body.kind else {
8029 panic!("expected a Block, got {:?}", ir.body.kind)
8030 };
8031 assert_eq!(stmts.len(), 1);
8032 let IrStmt::Let { local, value } = &stmts[0] else {
8033 panic!("expected IrStmt::Let, got {:?}", stmts[0])
8034 };
8035 assert_eq!(local, "_");
8036 let IrExprKind::Await { effect } = &value.kind else {
8037 panic!(
8038 "expected `let _ <- entries.put(k, v)` to lower to Await, got {:?}",
8039 value.kind
8040 )
8041 };
8042 let IrExprKind::Call { callee, args, .. } = &effect.kind else {
8043 panic!(
8044 "expected `entries.put(k, v)` to lower as an ordinary Callee-classified Call, \
8045 got {:?}",
8046 effect.kind
8047 )
8048 };
8049 assert!(
8050 matches!(callee, Callee::Store { field, op } if field == "entries" && op == "put"),
8051 "expected Callee::Store {{ field: \"entries\", op: \"put\" }}, got {callee:?}"
8052 );
8053 assert_eq!(
8054 args.len(),
8055 2,
8056 "the receiver is never prepended for Callee::Store"
8057 );
8058 assert!(matches!(&args[0].kind, IrExprKind::Local(n) if n == "k"));
8059 assert!(matches!(&args[1].kind, IrExprKind::Local(n) if n == "v"));
8060 }
8061
8062 #[test]
8063 fn handler_ir_bare_store_map_field_as_field_access_receiver_lowers_to_store_query() {
8064 let program = handler_ir_fixture();
8073 let ir = handler_ir_of(&program, "entryKeys");
8074 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8075 panic!("expected a Block with a tail, got {:?}", ir.body.kind)
8076 };
8077 let IrExprKind::Return { value } = &tail.kind else {
8078 panic!("expected the tail to wrap in Return, got {:?}", tail.kind)
8079 };
8080 let IrExprKind::Pure { value } = &value.kind else {
8081 panic!(
8082 "expected Effect.pure(...) to lower to Pure, got {:?}",
8083 value.kind
8084 )
8085 };
8086 let IrExprKind::Field { base, field } = &value.kind else {
8087 panic!(
8088 "expected entries.keys to lower to Field, got {:?}",
8089 value.kind
8090 )
8091 };
8092 assert_eq!(field, "keys");
8093 assert!(
8094 matches!(&base.kind, IrExprKind::StoreQuery(name) if name == "entries"),
8095 "expected the receiver `entries` to lower to StoreQuery, got {:?}",
8096 base.kind
8097 );
8098 }
8099
8100 #[test]
8101 fn handler_ir_bare_store_map_field_as_plain_argument_lowers_to_store_query() {
8102 let program = handler_ir_fixture();
8106 let ir = handler_ir_of(&program, "passEntries");
8107 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8108 panic!("expected a Block with a tail, got {:?}", ir.body.kind)
8109 };
8110 let IrExprKind::Return { value } = &tail.kind else {
8111 panic!("expected the tail to wrap in Return, got {:?}", tail.kind)
8112 };
8113 let IrExprKind::Pure { value } = &value.kind else {
8114 panic!(
8115 "expected Effect.pure(...) to lower to Pure, got {:?}",
8116 value.kind
8117 )
8118 };
8119 let IrExprKind::Call { args, .. } = &value.kind else {
8120 panic!(
8121 "expected passThroughQuery(entries) to lower to a Call, got {:?}",
8122 value.kind
8123 )
8124 };
8125 assert_eq!(args.len(), 1);
8126 assert!(
8127 matches!(&args[0].kind, IrExprKind::StoreQuery(name) if name == "entries"),
8128 "expected the bare argument `entries` to lower to StoreQuery, got {:?}",
8129 args[0].kind
8130 );
8131 }
8132
8133 #[test]
8134 fn handler_ir_bare_store_map_field_wins_over_a_colliding_free_fn_of_the_same_name() {
8135 let program = handler_ir_fixture();
8143 let ir = handler_ir_of(&program, "bareEntriesCollidesWithAFreeFn");
8144 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8145 panic!("expected a Block with a tail, got {:?}", ir.body.kind)
8146 };
8147 let IrExprKind::Return { value } = &tail.kind else {
8148 panic!("expected the tail to wrap in Return, got {:?}", tail.kind)
8149 };
8150 let IrExprKind::Pure { value } = &value.kind else {
8151 panic!(
8152 "expected Effect.pure(...) to lower to Pure, got {:?}",
8153 value.kind
8154 )
8155 };
8156 assert!(
8157 matches!(&value.kind, IrExprKind::StoreQuery(name) if name == "entries"),
8158 "expected the store field to win over the colliding free fn, got {:?}",
8159 value.kind
8160 );
8161 }
8162
8163 #[test]
8164 fn handler_ir_a_local_param_shadowing_a_store_map_field_name_lowers_to_local() {
8165 let program = handler_ir_fixture();
8170 let ir = handler_ir_of(&program, "shadowedEntries");
8171 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8172 panic!("expected a Block with a tail, got {:?}", ir.body.kind)
8173 };
8174 let IrExprKind::Return { value } = &tail.kind else {
8175 panic!("expected the tail to wrap in Return, got {:?}", tail.kind)
8176 };
8177 let IrExprKind::Pure { value } = &value.kind else {
8178 panic!(
8179 "expected Effect.pure(...) to lower to Pure, got {:?}",
8180 value.kind
8181 )
8182 };
8183 assert!(
8184 matches!(&value.kind, IrExprKind::Local(name) if name == "entries"),
8185 "expected the shadowing param to win over the store field, got {:?}",
8186 value.kind
8187 );
8188 }
8189
8190 #[test]
8191 fn handler_ir_threads_invariants_and_transitions_into_commit_unswapped() {
8192 let program = handler_ir_fixture();
8199 let bool_ty = program
8200 .program()
8201 .ty_intern
8202 .intern(Ty::Base(bynk_syntax::ast::BaseType::Bool));
8203 let mk = |name: &str| IrPredicate {
8204 name: name.to_string(),
8205 predicate: IrExpr {
8206 kind: IrExprKind::Const(ConstVal::Bool(true)),
8207 ty: bool_ty,
8208 span: Span::new(0, 0),
8209 },
8210 };
8211 let invariants = vec![mk("invOnly")];
8212 let transitions = vec![mk("transitionOnly")];
8213 let ir = handler_ir_of_with_predicates(&program, "deposit", &invariants, &transitions);
8214 let CommitShape::Transactional {
8215 invariants: got_inv,
8216 transitions: got_tr,
8217 } = ir.commit
8218 else {
8219 panic!("expected Transactional, got {:?}", ir.commit)
8220 };
8221 assert_eq!(got_inv.len(), 1);
8222 assert_eq!(got_inv[0].name, "invOnly");
8223 assert_eq!(got_tr.len(), 1);
8224 assert_eq!(got_tr[0].name, "transitionOnly");
8225 }
8226
8227 fn agent_item_fixture() -> CheckedProgram {
8252 checked_context_program(
8253 r#"
8254context demo
8255
8256agent Ledger {
8257 key id: String
8258 store active: Cell[Bool] = true
8259 store balance: Cell[Int] = 0
8260 store entries: Map[String, Int]
8261
8262 invariant staysKnown: active
8263
8264 transition activeImplication: old.active implies new.active
8265
8266 on call peek() -> Effect[Int] {
8267 Effect.pure(balance)
8268 }
8269
8270 on call deposit(amount: Int) -> Effect[()] {
8271 balance := amount
8272 Effect.pure(())
8273 }
8274
8275 on call addEntry(k: String, v: Int) -> Effect[()] {
8276 let _ <- entries.put(k, v)
8277 Effect.pure(())
8278 }
8279}
8280"#,
8281 )
8282 }
8283
8284 #[test]
8285 fn agent_item_ir_assembles_key_state_and_threads_invariants_transitions_into_handlers() {
8286 let program = agent_item_fixture();
8287 let agent = find_agent(&program, "Ledger");
8288 let ir = lower_agent_item_ir(agent, &program);
8289 let IrItem::Agent {
8290 def,
8291 key,
8292 state,
8293 handlers,
8294 invariants,
8295 transitions,
8296 } = &ir
8297 else {
8298 panic!("expected IrItem::Agent, got {:?}", ir)
8299 };
8300
8301 assert_eq!(def, "Ledger");
8302 let string_ty = program
8303 .program()
8304 .ty_intern
8305 .intern(Ty::Base(bynk_syntax::ast::BaseType::String));
8306 assert_eq!(key, &("id".to_string(), string_ty));
8307
8308 assert_eq!(
8309 state.len(),
8310 3,
8311 "active, balance, entries — declaration order"
8312 );
8313 assert!(matches!(state[0].kind, StoreKindIr::Cell(_)));
8314 assert_eq!(state[0].field, "active");
8315 assert!(matches!(state[1].kind, StoreKindIr::Cell(_)));
8316 assert_eq!(state[1].field, "balance");
8317 assert!(matches!(state[2].kind, StoreKindIr::Map(_, _)));
8318 assert_eq!(state[2].field, "entries");
8319
8320 assert_eq!(invariants.len(), 1);
8321 assert_eq!(invariants[0].name, "staysKnown");
8322 assert_eq!(transitions.len(), 1);
8323 assert_eq!(transitions[0].name, "activeImplication");
8324
8325 assert_eq!(handlers.len(), 3, "peek, deposit, addEntry");
8326
8327 let peek = handlers
8328 .iter()
8329 .find(|h| h.method_name.as_deref() == Some("peek"))
8330 .expect("peek handler");
8331 assert!(matches!(peek.commit, CommitShape::ReadOnly));
8332
8333 let deposit = handlers
8338 .iter()
8339 .find(|h| h.method_name.as_deref() == Some("deposit"))
8340 .expect("deposit handler");
8341 let CommitShape::Transactional {
8342 invariants: got_inv,
8343 transitions: got_tr,
8344 } = &deposit.commit
8345 else {
8346 panic!(
8347 "expected deposit's own commit to be Transactional, got {:?}",
8348 deposit.commit
8349 )
8350 };
8351 assert_eq!(got_inv.len(), 1);
8352 assert_eq!(got_inv[0].name, "staysKnown");
8353 assert_eq!(got_tr.len(), 1);
8354 assert_eq!(got_tr[0].name, "activeImplication");
8355
8356 let add_entry = handlers
8361 .iter()
8362 .find(|h| h.method_name.as_deref() == Some("addEntry"))
8363 .expect("addEntry handler");
8364 let CommitShape::Transactional {
8365 invariants: got_inv,
8366 transitions: got_tr,
8367 } = &add_entry.commit
8368 else {
8369 panic!(
8370 "expected addEntry's own commit to be Transactional, got {:?}",
8371 add_entry.commit
8372 )
8373 };
8374 assert_eq!(got_inv.len(), 1);
8375 assert_eq!(got_tr.len(), 1);
8376
8377 assert!(handlers.iter().all(|h| h.binder.is_none()));
8382 }
8383
8384 #[test]
8393 fn agent_invariant_with_a_real_comparison_lowers_without_panicking() {
8394 let program = checked_context_program(
8395 r#"
8396context demo
8397
8398agent Ledger {
8399 key id: String
8400 store balance: Cell[Int] = 0
8401
8402 invariant nonneg: balance >= 0
8403
8404 on call deposit(amount: Int) -> Effect[()] {
8405 balance := amount
8406 Effect.pure(())
8407 }
8408}
8409"#,
8410 );
8411 let agent = find_agent(&program, "Ledger");
8412 let ir = lower_agent_item_ir(agent, &program);
8413 let IrItem::Agent { invariants, .. } = &ir else {
8414 panic!("expected IrItem::Agent, got {:?}", ir)
8415 };
8416 assert_eq!(invariants.len(), 1);
8417 let IrExprKind::BinOp { op, .. } = &invariants[0].predicate.kind else {
8418 panic!("expected BinOp, got {:?}", invariants[0].predicate.kind)
8419 };
8420 assert_eq!(*op, IrBinOp::GtEq);
8421 }
8422
8423 fn call_service_fixture() -> CheckedProgram {
8431 checked_context_program(
8432 r#"
8433context demo
8434
8435type UserId = String
8436
8437actor Buyer { auth = Internal, identity = UserId }
8438
8439capability Clock {
8440 fn now() -> Effect[Int]
8441}
8442
8443provides Clock = FixedClock {
8444 fn now() -> Effect[Int] {
8445 42
8446 }
8447}
8448
8449service Api {
8450 on call(ping: String) -> Effect[String] {
8451 Effect.pure(ping)
8452 }
8453 on call(ping: String) -> Effect[String] by u: Buyer {
8454 Effect.pure(ping)
8455 }
8456 on call() -> Effect[Int] given Clock {
8457 let t <- Clock.now()
8458 Effect.pure(t)
8459 }
8460}
8461"#,
8462 )
8463 }
8464
8465 #[test]
8466 fn service_item_ir_assembles_a_call_protocol_service() {
8467 let program = call_service_fixture();
8468 let service = find_service(&program, "Api");
8469 let ir = lower_service_item_ir(service, &program);
8470 let IrItem::Service {
8471 def,
8472 protocol,
8473 handlers,
8474 policy,
8475 } = &ir
8476 else {
8477 panic!("expected IrItem::Service, got {:?}", ir)
8478 };
8479 assert_eq!(def, "Api");
8480 assert!(matches!(protocol, ProtocolIr::Call));
8481 assert!(
8482 policy.is_none(),
8483 "policy is only ever Some for a from http service"
8484 );
8485 assert_eq!(handlers.len(), 3, "declaration order preserved");
8486
8487 assert!(handlers[0].binder.is_none());
8488 assert!(handlers[0].given.is_empty());
8489 assert!(
8490 handlers[0].actors.is_empty(),
8491 "no `by` clause at all on this handler"
8492 );
8493 assert!(handlers[1].binder.is_some());
8494 assert_eq!(
8495 handlers[1].actors,
8496 vec!["Buyer".to_string()],
8497 "the gate itself, read straight off the `by` clause — see IrHandler::actors' own \
8498 doc comment for why this can't be recovered from `binder` alone"
8499 );
8500 assert_eq!(handlers[2].given, vec!["Clock".to_string()]);
8501
8502 for h in handlers {
8503 assert!(
8504 h.method_name.is_none(),
8505 "a service's on call handler carries no method_name"
8506 );
8507 assert!(h.effectful, "every service handler returns Effect[T]");
8508 assert!(matches!(h.commit, CommitShape::ReadOnly));
8509 }
8510 }
8511
8512 #[test]
8513 fn service_handler_binder_is_recorded_and_bound_into_the_body_scope() {
8514 let program = checked_context_program(
8517 r#"
8518context demo
8519
8520service Api {
8521 on call(ping: String) -> Effect[String] by c: Caller {
8522 Effect.pure(c.identity)
8523 }
8524}
8525"#,
8526 );
8527 let service = find_service(&program, "Api");
8528 let handler = find_service_handler(service, &HandlerKind::Call);
8529 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
8530
8531 let binder = ir
8532 .binder
8533 .as_ref()
8534 .unwrap_or_else(|| panic!("expected a persisted actor binding for this handler"));
8535 assert_eq!(binder.binder, "c");
8536 let tys = &program.program().ty_intern;
8537 let Ty::Actor(identity_ty) = &*tys.get(binder.ty) else {
8538 panic!("expected Ty::Actor, got {:?}", tys.get(binder.ty))
8539 };
8540 assert_eq!(identity_ty.display(tys), "String");
8541 assert_eq!(ir.actors, vec!["Caller".to_string()]);
8542
8543 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8549 panic!("expected a Block, got {:?}", ir.body.kind)
8550 };
8551 let IrExprKind::Return { value } = &tail.kind else {
8552 panic!("expected Return, got {:?}", tail.kind)
8553 };
8554 let IrExprKind::Pure { value } = &value.kind else {
8555 panic!("expected Pure, got {:?}", value.kind)
8556 };
8557 let IrExprKind::Field { base, field } = &value.kind else {
8558 panic!(
8559 "expected `c.identity` to lower to Field, got {:?}",
8560 value.kind
8561 )
8562 };
8563 assert_eq!(field, "identity");
8564 assert!(
8565 matches!(&base.kind, IrExprKind::Local(n) if n == "c"),
8566 "expected the binder to be bound into the body's own scope as Local(\"c\"), got {:?}",
8567 base.kind
8568 );
8569 }
8570
8571 #[test]
8572 fn sum_actor_binder_lowers_an_actor_sum() {
8573 let program = checked_context_program(
8586 r#"
8587context demo
8588
8589type UserId = String
8590
8591actor User { auth = Bearer(secret = "AUTH_SECRET"), identity = UserId }
8592
8593fn ok(s: String) -> HttpResult[String] { Ok(s) }
8594
8595service Api from http {
8596 on GET("/whoami") () -> Effect[HttpResult[String]] by who: User | Visitor {
8597 match who {
8598 User(_) => Effect.pure(ok("user"))
8599 Visitor => Effect.pure(ok("visitor"))
8600 }
8601 }
8602}
8603"#,
8604 );
8605 let service = find_service(&program, "Api");
8606 let handler = &service.handlers[0];
8607 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
8608 let binder = ir
8609 .binder
8610 .as_ref()
8611 .unwrap_or_else(|| panic!("expected a persisted actor binding for this handler"));
8612 assert_eq!(binder.binder, "who");
8613 let tys = &program.program().ty_intern;
8614 let Ty::ActorSum(members) = &*tys.get(binder.ty) else {
8615 panic!("expected Ty::ActorSum, got {:?}", tys.get(binder.ty))
8616 };
8617 assert_eq!(members.len(), 2);
8618 assert_eq!(members[0].0, "User");
8619 assert_eq!(members[0].1.display(tys), "UserId");
8620 assert_eq!(members[1].0, "Visitor");
8621 assert_eq!(
8622 members[1].1.display(tys),
8623 "()",
8624 "Visitor is a unit-identity prelude actor"
8625 );
8626 assert_eq!(
8627 ir.actors,
8628 vec!["User".to_string(), "Visitor".to_string()],
8629 "actors is redundant with Ty::ActorSum's own member names here, but present \
8630 uniformly regardless of shape — see IrHandler::actors' own doc comment"
8631 );
8632
8633 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8637 panic!("expected a Block, got {:?}", ir.body.kind)
8638 };
8639 let IrExprKind::Return { value } = &tail.kind else {
8640 panic!("expected Return, got {:?}", tail.kind)
8641 };
8642 assert!(
8643 matches!(&value.kind, IrExprKind::Match { .. }),
8644 "expected `match who {{ … }}` to lower to a real Match node, got {:?}",
8645 value.kind
8646 );
8647 }
8648
8649 #[test]
8659 fn lower_actor_seam_ir_tries_sum_ahead_of_bearer_for_a_bearer_first_sum() {
8660 let program = checked_context_program(
8661 r#"
8662context demo
8663
8664type UserId = String
8665
8666actor User { auth = Bearer(secret = "AUTH_SECRET"), identity = UserId }
8667
8668service Api from http {
8669 on GET("/whoami") () -> Effect[HttpResult[String]] by who: User | Visitor {
8670 Effect.pure(Ok("ok"))
8671 }
8672}
8673"#,
8674 );
8675 let service = find_service(&program, "Api");
8676 let handler = &service.handlers[0];
8677 let actors = actors_map(&program);
8678 let seam = lower_actor_seam_ir(handler, &actors);
8679 let ActorSeamIr::Sum(members) = &seam else {
8680 panic!("expected ActorSeamIr::Sum for a Bearer-first sum `by` clause, got {seam:?}");
8681 };
8682 assert_eq!(members.len(), 2);
8683 assert_eq!(members[0].actor_name, "User");
8684 assert_eq!(members[1].actor_name, "Visitor");
8685 }
8686
8687 #[test]
8688 fn a_binderless_by_clause_still_records_the_actor_gate() {
8689 let program = checked_context_program(
8696 r#"
8697context demo
8698
8699type UserId = String
8700
8701actor Buyer { auth = Internal, identity = UserId }
8702
8703service Api {
8704 on call(ping: String) -> Effect[String] by Buyer {
8705 Effect.pure(ping)
8706 }
8707}
8708"#,
8709 );
8710 let service = find_service(&program, "Api");
8711 let handler = find_service_handler(service, &HandlerKind::Call);
8712 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
8713 assert!(
8714 ir.binder.is_none(),
8715 "a binder-less `by <Actor>` verifies-and-discards — no identity is bound"
8716 );
8717 assert_eq!(
8718 ir.actors,
8719 vec!["Buyer".to_string()],
8720 "the gate itself survives even though no binder does"
8721 );
8722 }
8723
8724 fn http_service_fixture() -> CheckedProgram {
8734 checked_context_program(
8735 r#"
8736context demo
8737
8738fn ok(s: String) -> HttpResult[String] { Ok(s) }
8739
8740service Api from http {
8741 cors { origins: ["https://app.example.com"], credentials: true, maxAge: 1.hours }
8742 security { hsts: 365.days }
8743 limits { maxBody: 1048576 }
8744 on GET("/ping") () -> Effect[HttpResult[String]] by v: Visitor {
8745 Effect.pure(ok("pong"))
8746 }
8747}
8748"#,
8749 )
8750 }
8751
8752 #[test]
8753 fn http_policy_lowers_every_accessor_to_interpreted_values() {
8754 let program = http_service_fixture();
8755 let service = find_service(&program, "Api");
8756 let ir = lower_service_item_ir(service, &program);
8757 let IrItem::Service {
8758 protocol,
8759 handlers,
8760 policy,
8761 ..
8762 } = &ir
8763 else {
8764 panic!("expected IrItem::Service, got {:?}", ir)
8765 };
8766 assert!(matches!(protocol, ProtocolIr::Http));
8767 assert_eq!(
8768 handlers[0].kind,
8769 IrHandlerKind::Http {
8770 method: IrHttpMethod::Get,
8771 path: "/ping".to_string(),
8772 },
8773 "the route binding lives per-handler — this is why ProtocolIr::Http itself \
8774 carries no payload"
8775 );
8776
8777 let policy = policy
8778 .as_ref()
8779 .unwrap_or_else(|| panic!("expected a real PolicyIr for a from http service"));
8780 let cors = policy
8781 .cors
8782 .as_ref()
8783 .unwrap_or_else(|| panic!("expected Some(CorsIr) — this fixture declares cors {{ }}"));
8784 assert_eq!(cors.origins, vec!["https://app.example.com".to_string()]);
8785 assert!(cors.credentials);
8786 assert_eq!(
8787 cors.allow_headers, None,
8788 "no `headers:` field written — the author-override distinction, not the \
8789 emitter's own smart default"
8790 );
8791 assert_eq!(cors.max_age_secs, Some(3600), "1.hours in whole seconds");
8792
8793 assert!(policy.security.nosniff);
8794 assert_eq!(
8795 policy.security.hsts_max_age_secs,
8796 Some(365 * 24 * 60 * 60),
8797 "365.days in whole seconds"
8798 );
8799 assert_eq!(policy.max_body_bytes, Some(1_048_576));
8800 }
8801
8802 #[test]
8803 fn an_http_service_with_no_security_block_still_lowers_the_safe_defaults() {
8804 let program = checked_context_program(
8809 r#"
8810context demo
8811
8812fn ok(s: String) -> HttpResult[String] { Ok(s) }
8813
8814service Api from http {
8815 on GET("/ping") () -> Effect[HttpResult[String]] by v: Visitor {
8816 Effect.pure(ok("pong"))
8817 }
8818}
8819"#,
8820 );
8821 let service = find_service(&program, "Api");
8822 let ir = lower_service_item_ir(service, &program);
8823 let IrItem::Service { policy, .. } = &ir else {
8824 panic!("expected IrItem::Service, got {:?}", ir)
8825 };
8826 let policy = policy
8827 .as_ref()
8828 .unwrap_or_else(|| panic!("expected Some(PolicyIr) for a from http service"));
8829 assert!(policy.cors.is_none(), "no cors {{ }} block was declared");
8830 assert!(
8831 policy.security.nosniff,
8832 "the safe default, even with no security {{ }} block"
8833 );
8834 assert_eq!(policy.security.hsts_max_age_secs, None);
8835 assert_eq!(
8836 policy.max_body_bytes, None,
8837 "no limits {{ }} block was declared"
8838 );
8839 }
8840
8841 fn queue_service_fixture() -> CheckedProgram {
8860 checked_context_program(
8861 r#"
8862context demo
8863
8864type EmailJob = { to: String }
8865
8866service Outbox from queue("orders") {
8867 on message(m: EmailJob) -> Effect[QueueResult] {
8868 Ack
8869 }
8870}
8871"#,
8872 )
8873 }
8874
8875 #[test]
8876 fn a_queue_services_protocol_and_handler_signature_lower_correctly() {
8877 let program = queue_service_fixture();
8884 let service = find_service(&program, "Outbox");
8885 assert!(matches!(
8886 lower_protocol_ir(&service.protocol, &program),
8887 ProtocolIr::Queue { name } if name == "orders"
8888 ));
8889 let handler = find_service_handler(service, &HandlerKind::Message);
8890 let cx = LowerIrCtx::new(&program, HashSet::new());
8891 let (params, given, _ret, effectful) = lower_handler_signature_ir(handler, &cx);
8892 assert_eq!(params.len(), 1);
8893 assert_eq!(params[0].0, "m");
8894 assert!(given.is_empty());
8895 assert!(effectful, "every service handler returns Effect[T]");
8896 }
8897
8898 #[test]
8918 fn service_handler_signature_lowers_without_touching_a_body_that_constructs_ok() {
8919 let program = checked_context_program(
8920 r#"
8921context demo
8922
8923service Api from http {
8924 on GET("/ping") () -> Effect[HttpResult[String]] by v: Visitor {
8925 Effect.pure(Ok("pong"))
8926 }
8927}
8928"#,
8929 );
8930 let service = find_service(&program, "Api");
8931 let handler = find_service_handler(
8932 service,
8933 &HandlerKind::Http {
8934 method: bynk_syntax::ast::HttpMethod::Get,
8935 path: "/ping".to_string(),
8936 },
8937 );
8938 let (params, _given, ret, effectful) =
8939 lower_service_handler_signature_ir(handler, &program);
8940 assert!(params.is_empty(), "`() -> ...` declares no parameters");
8941 assert!(effectful, "an `Effect[...]` return type");
8942 assert!(matches!(
8943 &*program.program().ty_intern.get(ret),
8944 Ty::Effect(_)
8945 ));
8946 }
8947
8948 #[test]
8949 fn a_queue_services_on_message_handler_reaches_ordinary_body_lowering_not_the_websocket_deferral()
8950 {
8951 let program = queue_service_fixture();
8967 let service = find_service(&program, "Outbox");
8968 let handler = find_service_handler(service, &HandlerKind::Message);
8969 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
8970 assert!(
8971 ir.connection.is_none(),
8972 "a queue on message handler is not a WebSocket lifecycle handler"
8973 );
8974 let IrExprKind::Block { tail, .. } = &ir.body.kind else {
8975 panic!("expected a Block, got {:?}", ir.body.kind)
8976 };
8977 let IrExprKind::Return { value } = &tail.kind else {
8978 panic!("expected the tail to wrap in Return, got {:?}", tail.kind)
8979 };
8980 let IrExprKind::Call { callee, args, .. } = &value.kind else {
8981 panic!("expected Ack to lower to a Call, got {:?}", value.kind)
8982 };
8983 assert!(args.is_empty());
8984 assert!(
8985 matches!(callee, Callee::Intrinsic { ns, op } if *ns == QUEUE_RESULT && op == "Ack"),
8986 "expected Callee::Intrinsic {{ ns: QUEUE_RESULT, op: \"Ack\" }}, got {callee:?}"
8987 );
8988 }
8989
8990 #[test]
8991 fn a_qualified_nullary_sum_variant_reference_lowers_to_variant_not_field_access() {
8992 let program = checked_context_program(
9004 r#"
9005context demo
9006
9007type Region = enum { Domestic, International }
9008
9009type Order = { id: String, region: Region }
9010
9011fn pack(id: String) -> Order {
9012 Order { id: id, region: Region.International }
9013}
9014"#,
9015 );
9016 let f = program.program().fns.get("pack").unwrap();
9017 let mut cx = LowerIrCtx::new(&program, HashSet::new());
9018 cx.bind(
9019 "id".to_string(),
9020 program
9021 .program()
9022 .ty_intern
9023 .intern(Ty::Base(BaseType::String)),
9024 );
9025 let body = lower_expr_ir(&f.body.tail, &mut cx);
9026 let IrExprKind::Record { fields, .. } = &body.kind else {
9027 panic!("expected a Record construction, got {:?}", body.kind)
9028 };
9029 let (_, region_value) = fields
9030 .iter()
9031 .find(|(name, _)| name == "region")
9032 .expect("Order has a `region` field");
9033 assert!(
9034 matches!(®ion_value.kind, IrExprKind::Variant { tag, payload } if tag == "International" && payload.is_empty()),
9035 "expected `Region.International` to lower to a nullary Variant, got {:?}",
9036 region_value.kind
9037 );
9038 }
9039
9040 #[test]
9041 fn a_service_handler_param_with_an_unresolvable_type_falls_back_to_unit_not_a_panic() {
9042 let program = checked_context_program(
9055 r#"
9056context demo
9057
9058service Api from http {
9059 on POST("/x") (body: Nope) -> Effect[HttpResult[String]] by v: Visitor {
9060 Effect.pure(Ok("hi"))
9061 }
9062}
9063"#,
9064 );
9065 let service = find_service(&program, "Api");
9066 let handler = &service.handlers[0];
9067 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
9068 assert_eq!(ir.params.len(), 1);
9069 assert_eq!(ir.params[0].0, "body");
9070 assert_eq!(
9071 ir.params[0].1,
9072 program.program().ty_intern.intern(Ty::Unit),
9073 "an unresolvable param type falls back to Unit, matching lower_protocol_ir's own posture"
9074 );
9075 }
9076
9077 #[test]
9078 fn a_cron_service_lowers_its_schedule_from_the_handler_not_the_protocol() {
9079 let program = checked_context_program(
9080 r#"
9081context demo
9082
9083fn done() -> Result[(), String] { Ok(()) }
9084
9085service Sweeper from cron {
9086 on schedule("*/5 * * * *") () -> Effect[Result[(), String]] {
9087 Effect.pure(done())
9088 }
9089}
9090"#,
9091 );
9092 let service = find_service(&program, "Sweeper");
9093 let ir = lower_service_item_ir(service, &program);
9094 let IrItem::Service {
9095 protocol, handlers, ..
9096 } = &ir
9097 else {
9098 panic!("expected IrItem::Service, got {:?}", ir)
9099 };
9100 assert!(matches!(protocol, ProtocolIr::Cron));
9101 assert_eq!(
9102 handlers[0].kind,
9103 IrHandlerKind::Cron {
9104 expr: "*/5 * * * *".to_string()
9105 }
9106 );
9107 }
9108
9109 fn websocket_service_fixture() -> CheckedProgram {
9117 checked_context_program(
9118 r#"
9119context demo
9120
9121type RoomId = String
9122type UserId = String
9123type ServerFrame = { text: String }
9124type ClientFrame = { text: String }
9125
9126actor Participant { auth = Bearer(secret = "AUTH_SECRET"), identity = UserId }
9127
9128service ChatGateway from websocket(in: ClientFrame, out: ServerFrame) {
9129 on open (roomId: RoomId) -> Effect[()] by user: Participant {
9130 let _ <- connection.send(ServerFrame { text: "welcome" })
9131 let _ <- Room(roomId).join(user.identity, connection)
9132 ()
9133 }
9134
9135 on message (roomId: RoomId, frame: ClientFrame) -> Effect[()] by user: Participant {
9136 let _ <- connection.send(ServerFrame { text: frame.text })
9137 let _ <- Room(roomId).post(user.identity, frame.text)
9138 ()
9139 }
9140
9141 on close (roomId: RoomId) -> Effect[()] by user: Participant {
9142 let _ <- Room(roomId).leave(user.identity)
9143 ()
9144 }
9145}
9146
9147agent Room {
9148 key id: RoomId
9149 store members: Set[UserId]
9150 store conns: Map[UserId, Connection[ServerFrame]]
9151
9152 on call join(u: UserId, conn: Connection[ServerFrame]) -> Effect[()] {
9153 let _ <- members.add(u)
9154 let _ <- conns.put(u, conn)
9155 ()
9156 }
9157
9158 on call leave(u: UserId) -> Effect[()] {
9159 let _ <- members.remove(u)
9160 let _ <- conns.remove(u)
9161 ()
9162 }
9163
9164 on call post(sender: UserId, text: String) -> Effect[()] {
9165 let _ <- conns.parTraverse((c: Connection[ServerFrame]) => c.send(ServerFrame { text: text }))
9166 ()
9167 }
9168}
9169"#,
9170 )
9171 }
9172
9173 #[test]
9174 fn websocket_protocol_descriptor_lowers_its_frame_types() {
9175 let program = websocket_service_fixture();
9176 let service = find_service(&program, "ChatGateway");
9177 let ir = lower_protocol_ir(&service.protocol, &program);
9178 let ProtocolIr::WebSocket { in_ty, out_ty } = ir else {
9179 panic!("expected ProtocolIr::WebSocket, got {:?}", ir)
9180 };
9181 let tys = &program.program().ty_intern;
9182 assert_eq!(in_ty.display(tys), "ClientFrame");
9183 assert_eq!(out_ty.display(tys), "ServerFrame");
9184 }
9185
9186 #[test]
9187 fn websocket_open_handler_lowers_an_owned_connection_binding() {
9188 let program = websocket_service_fixture();
9193 let service = find_service(&program, "ChatGateway");
9194 let handler = find_service_handler(service, &HandlerKind::Open);
9195 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
9196 let conn = ir
9197 .connection
9198 .as_ref()
9199 .expect("on open must carry a ConnectionBinder");
9200 assert!(
9201 !conn.borrowed,
9202 "on open's connection is a fresh owned socket, not borrowed"
9203 );
9204 let tys = &program.program().ty_intern;
9205 assert_eq!(conn.ty.display(tys), "Connection[ServerFrame]");
9206 assert!(ir.params.iter().all(|(name, _)| name != "connection"));
9209 let IrExprKind::Block { stmts, .. } = &ir.body.kind else {
9210 panic!("expected a Block body, got {:?}", ir.body.kind)
9211 };
9212 let has_connection_local = stmts
9213 .iter()
9214 .any(|stmt| format!("{stmt:?}").contains("Local(\"connection\")"));
9215 assert!(
9216 has_connection_local,
9217 "expected the lowered body to resolve `connection` as a Local, got {stmts:?}"
9218 );
9219 }
9220
9221 #[test]
9222 fn websocket_message_and_close_handlers_lower_a_borrowed_connection_binding() {
9223 let program = websocket_service_fixture();
9227 let service = find_service(&program, "ChatGateway");
9228 for kind in [HandlerKind::Message, HandlerKind::Close] {
9229 let handler = find_service_handler(service, &kind);
9230 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
9231 let conn = ir
9232 .connection
9233 .as_ref()
9234 .unwrap_or_else(|| panic!("{kind:?} must carry a ConnectionBinder"));
9235 assert!(
9236 conn.borrowed,
9237 "{kind:?}'s connection is the borrowed firing socket"
9238 );
9239 }
9240 let handler = find_service_handler(service, &HandlerKind::Message);
9249 let ir = lower_service_handler_ir(handler, &service.protocol, &program);
9250 let IrExprKind::Block { stmts, .. } = &ir.body.kind else {
9251 panic!("expected a Block body, got {:?}", ir.body.kind)
9252 };
9253 let has_connection_local = stmts
9254 .iter()
9255 .any(|stmt| format!("{stmt:?}").contains("Local(\"connection\")"));
9256 assert!(
9257 has_connection_local,
9258 "expected on message's lowered body to resolve `connection` as a Local, got {stmts:?}"
9259 );
9260 }
9261
9262 #[test]
9263 fn websocket_service_item_assembles_with_every_lifecycle_handler_lowered() {
9264 let program = websocket_service_fixture();
9268 let service = find_service(&program, "ChatGateway");
9269 let ir = lower_service_item_ir(service, &program);
9270 let IrItem::Service { handlers, .. } = &ir else {
9271 panic!("expected IrItem::Service, got {:?}", ir)
9272 };
9273 assert_eq!(handlers.len(), 3);
9274 assert!(handlers.iter().all(|h| h.connection.is_some()));
9275 }
9276
9277 #[test]
9278 fn service_level_by_and_given_defaults_are_already_injected_before_lowering() {
9279 let program = checked_context_program(
9286 r#"
9287context demo
9288
9289type UserId = String
9290
9291actor Buyer { auth = Internal, identity = UserId }
9292
9293capability Clock {
9294 fn now() -> Effect[Int]
9295}
9296
9297provides Clock = FixedClock {
9298 fn now() -> Effect[Int] {
9299 42
9300 }
9301}
9302
9303service Api by u: Buyer given Clock {
9304 on call(ping: String) -> Effect[String] {
9305 Effect.pure(ping)
9306 }
9307}
9308"#,
9309 );
9310 let service = find_service(&program, "Api");
9311 let ir = lower_service_item_ir(service, &program);
9312 let IrItem::Service { handlers, .. } = &ir else {
9313 panic!("expected IrItem::Service, got {:?}", ir)
9314 };
9315 assert_eq!(handlers.len(), 1);
9316 let binder = handlers[0]
9317 .binder
9318 .as_ref()
9319 .unwrap_or_else(|| panic!("expected the service-level `by u: Buyer` to be inherited"));
9320 assert_eq!(binder.binder, "u");
9321 assert_eq!(handlers[0].given, vec!["Clock".to_string()]);
9322 }
9323
9324 #[test]
9325 fn lower_event_pattern_ir_reshapes_literal_and_variant_fields() {
9326 let source = r#"
9338context demo
9339
9340type Status = | Active | Inactive
9341
9342event OrderPlaced = {
9343 status: Status,
9344 count: Int,
9345}
9346
9347service Subscriber from Events(OrderPlaced { status: Active, count: 3, .. }) {
9348 on event(o: OrderPlaced) -> Effect[()] {
9349 Effect.pure(())
9350 }
9351}
9352"#;
9353 let tokens = lexer::tokenize(source).expect("lex");
9354 let unit = parser::parse_unit(&tokens, source).expect("parse");
9355 let SourceUnit::Context(ctx) = unit else {
9356 panic!("expected a context unit, got {unit:?}")
9357 };
9358 let service = ctx
9359 .items
9360 .iter()
9361 .find_map(|item| match item {
9362 CommonsItem::Service(s) if s.name.name == "Subscriber" => Some(s),
9363 _ => None,
9364 })
9365 .expect("no service named `Subscriber` in this fixture");
9366 let ServiceProtocol::Events { pattern, .. } = &service.protocol else {
9367 panic!(
9368 "expected ServiceProtocol::Events, got {:?}",
9369 service.protocol
9370 )
9371 };
9372 let pattern = pattern
9373 .as_ref()
9374 .expect("expected a structural pattern on this Events subscription");
9375
9376 let ir = lower_event_pattern_ir(pattern);
9377 assert_eq!(ir.fields.len(), 2, "declaration order preserved");
9378 assert_eq!(ir.fields[0].0, "status");
9379 assert!(
9380 matches!(&ir.fields[0].1, EventPatternValueIr::Variant { tag } if tag == "Active"),
9381 "expected a bare nullary variant tag (the AST's own optional qualifying type_name \
9382 dropped), got {:?}",
9383 ir.fields[0].1
9384 );
9385 assert_eq!(ir.fields[1].0, "count");
9386 assert!(
9387 matches!(
9388 &ir.fields[1].1,
9389 EventPatternValueIr::Const(ConstVal::Int(3))
9390 ),
9391 "expected a literal Int constant, got {:?}",
9392 ir.fields[1].1
9393 );
9394 }
9395
9396 #[test]
9397 fn lower_capability_item_ir_assembles_ops_in_declaration_order() {
9398 let program = checked_context_program(
9399 r#"
9400context demo
9401
9402capability Store {
9403 fn get(key: String) -> Effect[Int]
9404 fn put(key: String, value: Int) -> Effect[()]
9405}
9406"#,
9407 );
9408 let cap = find_capability(&program, "Store");
9409 let ir = lower_capability_item_ir(cap, &program);
9410 let IrItem::Capability { def, ops } = &ir else {
9411 panic!("expected IrItem::Capability, got {:?}", ir)
9412 };
9413 assert_eq!(def, "Store");
9414 assert_eq!(ops.len(), 2, "declaration order preserved");
9415
9416 assert_eq!(ops[0].name, "get");
9417 assert!(ops[0].type_params.is_empty());
9418 assert_eq!(ops[0].params.len(), 1);
9419 assert_eq!(ops[0].params[0].0, "key");
9420 assert!(matches!(
9421 &*program.program().ty_intern.get(ops[0].params[0].1),
9422 Ty::Base(bynk_syntax::ast::BaseType::String)
9423 ));
9424 assert!(matches!(
9428 &*program.program().ty_intern.get(ops[0].return_ty),
9429 Ty::Effect(inner) if matches!(
9430 &*program.program().ty_intern.get(*inner),
9431 Ty::Base(bynk_syntax::ast::BaseType::Int)
9432 )
9433 ));
9434
9435 assert_eq!(ops[1].name, "put");
9436 assert_eq!(ops[1].params.len(), 2);
9437 assert_eq!(ops[1].params[0].0, "key");
9438 assert_eq!(ops[1].params[1].0, "value");
9439 assert!(matches!(
9440 &*program.program().ty_intern.get(ops[1].params[1].1),
9441 Ty::Base(bynk_syntax::ast::BaseType::Int)
9442 ));
9443 }
9444
9445 #[test]
9451 fn capability_op_sig_from_commons_finds_the_named_op() {
9452 let program = checked_context_program(
9453 r#"
9454context demo
9455
9456capability Store {
9457 fn get(key: String) -> Effect[Int]
9458 fn put(key: String, value: Int) -> Effect[()]
9459}
9460"#,
9461 );
9462 let commons = program.program();
9463
9464 let get = capability_op_sig_from_commons(commons, "Store", "get")
9465 .expect("Store.get should resolve");
9466 assert_eq!(get.params.len(), 1);
9467 assert_eq!(get.params[0].0, "key");
9468
9469 let put = capability_op_sig_from_commons(commons, "Store", "put")
9470 .expect("Store.put should resolve");
9471 assert_eq!(put.params.len(), 2);
9472 assert_eq!(put.params[0].0, "key");
9473 assert_eq!(put.params[1].0, "value");
9474
9475 assert!(capability_op_sig_from_commons(commons, "NoSuchCap", "get").is_none());
9480 assert!(capability_op_sig_from_commons(commons, "Store", "no_such_op").is_none());
9481 }
9482
9483 #[test]
9484 fn lower_op_sig_ir_resolves_generic_op_type_params_as_rigid_vars() {
9485 let program = checked_context_program(
9492 r#"
9493context demo
9494
9495capability Store {
9496 fn get[T](key: String) -> Effect[T]
9497 fn now() -> Effect[Int]
9498}
9499"#,
9500 );
9501 let cap = find_capability(&program, "Store");
9502 let ir = lower_capability_item_ir(cap, &program);
9503 let IrItem::Capability { ops, .. } = &ir else {
9504 panic!("expected IrItem::Capability, got {:?}", ir)
9505 };
9506
9507 let get = ops.iter().find(|o| o.name == "get").expect("op `get`");
9508 assert_eq!(get.type_params, vec!["T".to_string()]);
9509 assert!(matches!(
9510 &*program.program().ty_intern.get(get.params[0].1),
9511 Ty::Base(bynk_syntax::ast::BaseType::String)
9512 ));
9513 assert!(
9514 matches!(
9515 &*program.program().ty_intern.get(get.return_ty),
9516 Ty::Effect(inner) if matches!(
9517 &*program.program().ty_intern.get(*inner),
9518 Ty::Var(n) if n == "T"
9519 )
9520 ),
9521 "expected the op's own `T` to survive as Ty::Var, not collapse to Ty::Unit"
9522 );
9523
9524 let now = ops.iter().find(|o| o.name == "now").expect("op `now`");
9525 assert!(
9526 now.type_params.is_empty(),
9527 "a sibling non-generic op must not see `get`'s own `T` in scope"
9528 );
9529 }
9530
9531 #[test]
9532 fn lower_op_sig_ir_resolves_a_generic_op_type_param_inside_a_type_argument() {
9533 let program = checked_context_program(
9541 r#"
9542context demo
9543
9544type Box[A] = { value: A }
9545
9546capability Store {
9547 fn get[T](box: Box[T]) -> Effect[T]
9548}
9549"#,
9550 );
9551 let cap = find_capability(&program, "Store");
9552 let ir = lower_capability_item_ir(cap, &program);
9553 let IrItem::Capability { ops, .. } = &ir else {
9554 panic!("expected IrItem::Capability, got {:?}", ir)
9555 };
9556 let get = &ops[0];
9557 assert!(
9558 matches!(
9559 &*program.program().ty_intern.get(get.params[0].1),
9560 Ty::Named { name, args, .. }
9561 if name == "Box"
9562 && args.len() == 1
9563 && matches!(
9564 &*program.program().ty_intern.get(args[0]),
9565 Ty::Var(n) if n == "T"
9566 )
9567 ),
9568 "expected Box[T] with T resolved as a rigid Ty::Var argument, got {:?}",
9569 program.program().ty_intern.get(get.params[0].1)
9570 );
9571 }
9572
9573 #[test]
9574 fn lower_op_sig_ir_falls_back_to_unit_on_an_unresolvable_type_like_the_checker_does() {
9575 let program = checked_context_program(
9583 r#"
9584context demo
9585
9586capability Store {
9587 fn get(key: Bogus) -> Effect[Bogus]
9588}
9589"#,
9590 );
9591 let cap = find_capability(&program, "Store");
9592 let ir = lower_capability_item_ir(cap, &program);
9596 let IrItem::Capability { ops, .. } = &ir else {
9597 panic!("expected IrItem::Capability, got {:?}", ir)
9598 };
9599 assert!(matches!(
9600 &*program.program().ty_intern.get(ops[0].params[0].1),
9601 Ty::Unit
9602 ));
9603 assert!(matches!(
9604 &*program.program().ty_intern.get(ops[0].return_ty),
9605 Ty::Unit
9606 ));
9607 }
9608
9609 #[test]
9610 fn lower_op_sig_ir_agrees_with_the_checkers_own_capability_op_info() {
9611 let program = checked_context_program(
9618 r#"
9619context demo
9620
9621capability Store {
9622 fn get[T](key: String) -> Effect[T]
9623}
9624"#,
9625 );
9626 let cap = find_capability(&program, "Store");
9627 let ir = lower_capability_item_ir(cap, &program);
9628 let IrItem::Capability { ops, .. } = &ir else {
9629 panic!("expected IrItem::Capability, got {:?}", ir)
9630 };
9631 let op = &ops[0];
9632
9633 let info = context_checks::build_capability_op_info(
9634 &cap.ops[0],
9635 &program.program().types,
9636 &program.program().ty_intern,
9637 );
9638
9639 assert_eq!(op.name, info.name);
9640 assert_eq!(op.type_params, info.type_params);
9641 assert_eq!(
9642 op.params.iter().map(|(n, _)| n.clone()).collect::<Vec<_>>(),
9643 info.param_names
9644 );
9645 assert_eq!(
9646 op.params.iter().map(|(_, t)| *t).collect::<Vec<_>>(),
9647 info.params
9648 );
9649 assert_eq!(op.return_ty, info.return_ty);
9650 }
9651
9652 #[test]
9653 fn lower_provider_item_ir_assembles_bynk_ops_and_given_in_declaration_order() {
9654 let program = checked_context_program(
9660 r#"
9661context demo
9662
9663capability Clock {
9664 fn now() -> Effect[Int]
9665}
9666
9667capability Random {
9668 fn next() -> Effect[Int]
9669}
9670
9671capability Store {
9672 fn get(key: String) -> Effect[Int]
9673 fn put(key: String, value: Int) -> Effect[()]
9674}
9675
9676provides Store = MemStore given Random, Clock {
9677 fn get(key: String) -> Effect[Int] {
9678 Effect.pure(0)
9679 }
9680 fn put(key: String, value: Int) -> Effect[()] {
9681 Effect.pure(())
9682 }
9683}
9684"#,
9685 );
9686 let provider = find_provider(&program, "MemStore");
9687 let ir = lower_provider_item_ir(provider, &program);
9688 let IrItem::Provider { def, cap, body } = &ir else {
9689 panic!("expected IrItem::Provider, got {:?}", ir)
9690 };
9691 assert_eq!(def, "MemStore");
9692 assert_eq!(cap, "Store");
9693 let ProviderBody::Bynk { given, ops } = body else {
9694 panic!("expected ProviderBody::Bynk, got {:?}", body)
9695 };
9696 assert_eq!(
9697 given.iter().map(|g| g.name.as_str()).collect::<Vec<_>>(),
9698 vec!["Random", "Clock"],
9699 "given's own declaration order preserved, unused entries included"
9700 );
9701 assert!(given.iter().all(|g| g.context.is_none()));
9702 assert_eq!(ops.len(), 2, "declaration order preserved");
9703
9704 assert_eq!(ops[0].name, "get");
9705 assert_eq!(ops[0].params.len(), 1);
9706 assert_eq!(ops[0].params[0].0, "key");
9707 assert!(matches!(
9708 &*program.program().ty_intern.get(ops[0].params[0].1),
9709 Ty::Base(bynk_syntax::ast::BaseType::String)
9710 ));
9711 assert!(matches!(
9714 &*program.program().ty_intern.get(ops[0].return_ty),
9715 Ty::Effect(inner) if matches!(
9716 &*program.program().ty_intern.get(*inner),
9717 Ty::Base(bynk_syntax::ast::BaseType::Int)
9718 )
9719 ));
9720
9721 assert_eq!(ops[1].name, "put");
9722 assert_eq!(ops[1].params.len(), 2);
9723 assert_eq!(ops[1].params[0].0, "key");
9724 assert_eq!(ops[1].params[1].0, "value");
9725 }
9726
9727 #[test]
9728 fn lower_provider_item_ir_external_provider_has_no_ops() {
9729 let program = checked_context_program("context demo\n");
9741 let provider = ProviderDecl {
9742 capability: bynk_syntax::ast::Ident {
9743 name: "Store".to_string(),
9744 span: Span::default(),
9745 },
9746 provider_name: bynk_syntax::ast::Ident {
9747 name: "ExternalStore".to_string(),
9748 span: Span::default(),
9749 },
9750 given: vec![CapRef {
9757 context: None,
9758 name: bynk_syntax::ast::Ident {
9759 name: "Clock".to_string(),
9760 span: Span::default(),
9761 },
9762 span: Span::default(),
9763 }],
9764 ops: Vec::new(),
9765 external: true,
9766 documentation: None,
9767 span: Span::default(),
9768 trivia: Default::default(),
9769 };
9770
9771 let ir = lower_provider_item_ir(&provider, &program);
9772 let IrItem::Provider { def, cap, body } = &ir else {
9773 panic!("expected IrItem::Provider, got {:?}", ir)
9774 };
9775 assert_eq!(def, "ExternalStore");
9776 assert_eq!(cap, "Store");
9777 let ProviderBody::External { given } = body else {
9778 panic!("expected ProviderBody::External, got {:?}", body)
9779 };
9780 assert_eq!(given.len(), 1);
9781 assert_eq!(given[0].context, None);
9782 assert_eq!(given[0].name, "Clock");
9783 }
9784
9785 #[test]
9786 fn lower_provider_op_ir_lowers_a_given_capability_call_via_the_ordinary_callee_path() {
9787 let program = checked_context_program(
9794 r#"
9795context demo
9796
9797capability Clock {
9798 fn now() -> Effect[Int]
9799}
9800
9801capability Store {
9802 fn get(key: String) -> Effect[Int]
9803}
9804
9805provides Store = MemStore given Clock {
9806 fn get(key: String) -> Effect[Int] {
9807 Clock.now()
9808 }
9809}
9810"#,
9811 );
9812 let provider = find_provider(&program, "MemStore");
9813 let ir = lower_provider_item_ir(provider, &program);
9814 let IrItem::Provider { body, .. } = &ir else {
9815 panic!("expected IrItem::Provider, got {:?}", ir)
9816 };
9817 let ProviderBody::Bynk { given, ops } = body else {
9818 panic!("expected ProviderBody::Bynk, got {:?}", body)
9819 };
9820 assert_eq!(given.len(), 1);
9821 assert_eq!(given[0].name, "Clock");
9822 let get = &ops[0];
9823 let tail = fn_tail(&get.body);
9824 let IrExprKind::Call { callee, args, .. } = &tail.kind else {
9825 panic!("expected Call, got {:?}", tail.kind)
9826 };
9827 assert!(matches!(
9828 callee,
9829 Callee::Capability { cap, op } if cap == "Clock" && op == "now"
9830 ));
9831 assert!(args.is_empty());
9832 }
9833
9834 #[test]
9835 fn lower_cap_ref_ir_local_capability_has_no_context() {
9836 let cap_ref = CapRef {
9837 context: None,
9838 name: bynk_syntax::ast::Ident {
9839 name: "Clock".to_string(),
9840 span: Span::default(),
9841 },
9842 span: Span::default(),
9843 };
9844 let ir = lower_cap_ref_ir(&cap_ref);
9845 assert_eq!(ir.context, None);
9846 assert_eq!(ir.name, "Clock");
9847 }
9848
9849 #[test]
9850 fn lower_cap_ref_ir_preserves_a_cross_context_prefix() {
9851 let cap_ref = CapRef {
9858 context: Some(QualifiedName {
9859 parts: vec![bynk_syntax::ast::Ident {
9860 name: "Billing".to_string(),
9861 span: Span::default(),
9862 }],
9863 span: Span::default(),
9864 }),
9865 name: bynk_syntax::ast::Ident {
9866 name: "Ledger".to_string(),
9867 span: Span::default(),
9868 },
9869 span: Span::default(),
9870 };
9871 let ir = lower_cap_ref_ir(&cap_ref);
9872 assert_eq!(ir.context.as_deref(), Some("Billing"));
9873 assert_eq!(ir.name, "Ledger");
9874 }
9875
9876 #[test]
9877 fn lower_provider_op_ir_binds_its_own_param_into_scope() {
9878 let program = checked_context_program(
9885 r#"
9886context demo
9887
9888capability Store {
9889 fn get(key: String) -> Effect[String]
9890}
9891
9892provides Store = MemStore {
9893 fn get(key: String) -> Effect[String] {
9894 Effect.pure(key)
9895 }
9896}
9897"#,
9898 );
9899 let provider = find_provider(&program, "MemStore");
9900 let ir = lower_provider_item_ir(provider, &program);
9901 let IrItem::Provider { body, .. } = &ir else {
9902 panic!("expected IrItem::Provider, got {:?}", ir)
9903 };
9904 let ProviderBody::Bynk { ops, .. } = body else {
9905 panic!("expected ProviderBody::Bynk, got {:?}", body)
9906 };
9907 let get = &ops[0];
9908 let key_ty = get.params[0].1;
9909 let tail = fn_tail(&get.body);
9910 let IrExprKind::Pure { value } = &tail.kind else {
9913 panic!("expected Pure, got {:?}", tail.kind)
9914 };
9915 assert!(
9916 matches!(
9917 &value.kind,
9918 IrExprKind::Local(name) if name == "key"
9919 ),
9920 "expected the op's own `key` param to resolve as a bound Local, got {:?}",
9921 value.kind
9922 );
9923 assert_eq!(
9924 value.ty, key_ty,
9925 "the resolved Local's type must be the same TyId bind() seeded"
9926 );
9927 }
9928
9929 fn parsed_only_context(source: &str) -> bynk_syntax::ast::Context {
9940 let tokens = lexer::tokenize(source).expect("lex");
9941 let unit = parser::parse_unit(&tokens, source).expect("parse");
9942 let SourceUnit::Context(ctx) = unit else {
9943 panic!("expected a context unit, got {unit:?}")
9944 };
9945 ctx
9946 }
9947
9948 fn parsed_handler<'a>(
9949 ctx: &'a bynk_syntax::ast::Context,
9950 service: &str,
9951 index: usize,
9952 ) -> &'a Handler {
9953 let service = ctx
9954 .items
9955 .iter()
9956 .find_map(|item| match item {
9957 CommonsItem::Service(s) if s.name.name == service => Some(s),
9958 _ => None,
9959 })
9960 .unwrap_or_else(|| panic!("no service named `{service}` in this fixture"));
9961 &service.handlers[index]
9962 }
9963
9964 #[test]
9965 fn lower_route_cache_ir_reads_maxage_and_scope_off_a_get_handler() {
9966 let ctx = parsed_only_context(
9967 r#"
9968context demo
9969
9970service Api from http {
9971 @cache(maxAge: 5.minutes, scope: public)
9972 on GET("/config") () -> Effect[HttpResult[String]] by v: Visitor {
9973 Ok("cfg")
9974 }
9975
9976 @cache(maxAge: 30.seconds)
9977 on GET("/private") () -> Effect[HttpResult[String]] by v: Visitor {
9978 Ok("priv")
9979 }
9980
9981 on GET("/plain") () -> Effect[HttpResult[String]] by v: Visitor {
9982 Ok("plain")
9983 }
9984}
9985"#,
9986 );
9987 let public_cache = lower_route_cache_ir(parsed_handler(&ctx, "Api", 0))
9988 .unwrap_or_else(|| panic!("expected Some(CacheIr) for a well-formed @cache"));
9989 assert_eq!(public_cache.max_age_secs, 300, "5.minutes in whole seconds");
9990 assert_eq!(public_cache.scope, "public");
9991
9992 let default_scope_cache = lower_route_cache_ir(parsed_handler(&ctx, "Api", 1))
9993 .unwrap_or_else(|| panic!("expected Some(CacheIr) with no explicit scope:"));
9994 assert_eq!(default_scope_cache.max_age_secs, 30);
9995 assert_eq!(
9996 default_scope_cache.scope, "private",
9997 "no scope: argument written — must default to private"
9998 );
9999
10000 assert!(
10001 lower_route_cache_ir(parsed_handler(&ctx, "Api", 2)).is_none(),
10002 "no @cache annotation at all must yield None"
10003 );
10004 }
10005
10006 #[test]
10007 fn lower_route_cache_ir_returns_none_for_a_non_get_handler_even_with_a_cache_annotation() {
10008 let ctx = parsed_only_context(
10012 r#"
10013context demo
10014
10015service Api from http {
10016 @cache(maxAge: 5.minutes)
10017 on POST("/items") (body: String) -> Effect[HttpResult[String]] by v: Visitor {
10018 Created(body)
10019 }
10020}
10021"#,
10022 );
10023 assert!(
10024 lower_route_cache_ir(parsed_handler(&ctx, "Api", 0)).is_none(),
10025 "a @cache on a non-GET handler must not construct a CacheIr"
10026 );
10027 }
10028
10029 #[test]
10030 fn lower_route_cache_ir_discards_an_otherwise_well_formed_scope_when_maxage_is_missing() {
10031 let ctx = parsed_only_context(
10035 r#"
10036context demo
10037
10038service Api from http {
10039 @cache(scope: public)
10040 on GET("/broken") () -> Effect[HttpResult[String]] by v: Visitor {
10041 Ok("x")
10042 }
10043}
10044"#,
10045 );
10046 assert!(
10047 lower_route_cache_ir(parsed_handler(&ctx, "Api", 0)).is_none(),
10048 "a well-formed scope: must not survive a missing maxAge:"
10049 );
10050 }
10051
10052 #[test]
10053 fn lower_route_limit_ir_reads_maxbody_off_a_route_annotation() {
10054 let ctx = parsed_only_context(
10055 r#"
10056context demo
10057
10058service Api from http {
10059 @limit(maxBody: 26_214_400)
10060 on POST("/bulk") (body: String) -> Effect[HttpResult[String]] by v: Visitor {
10061 Created(body)
10062 }
10063
10064 on POST("/upload") (body: String) -> Effect[HttpResult[String]] by v: Visitor {
10065 Created(body)
10066 }
10067}
10068"#,
10069 );
10070 assert_eq!(
10071 lower_route_limit_ir(parsed_handler(&ctx, "Api", 0)),
10072 Some(26_214_400)
10073 );
10074 assert!(
10075 lower_route_limit_ir(parsed_handler(&ctx, "Api", 1)).is_none(),
10076 "no @limit annotation at all must yield None — the caller applies the \
10077 service-wide default, this function does not know it"
10078 );
10079 }
10080
10081 #[test]
10082 fn lower_route_limit_ir_returns_none_for_a_non_positive_maxbody() {
10083 let ctx = parsed_only_context(
10090 r#"
10091context demo
10092
10093service Api from http {
10094 @limit(maxBody: 0)
10095 on POST("/zero") (body: String) -> Effect[HttpResult[String]] by v: Visitor {
10096 Created(body)
10097 }
10098}
10099"#,
10100 );
10101 assert!(
10102 lower_route_limit_ir(parsed_handler(&ctx, "Api", 0)).is_none(),
10103 "a non-positive maxBody must not construct Some(0)"
10104 );
10105 }
10106}