1use std::collections::{HashMap, HashSet};
39use std::sync::Arc;
40
41use crate::checker::{Callee, Ty, TypedCommons};
42use crate::project_model::{ShadowedReaches, ShadowedType};
43use crate::resolver::MethodTable as ResolverMethodTable;
44use bynk_syntax::ast::*;
45use bynk_syntax::error::CompileError;
46use bynk_syntax::span::Span;
47
48pub struct ShadowScope<'a> {
50 pub unit: &'a str,
52 pub shadowed: &'a ShadowedReaches,
53 pub combined_types: &'a HashMap<String, Arc<TypeDecl>>,
54 pub combined_fns: &'a HashMap<String, Arc<FnDecl>>,
55 pub combined_methods: &'a HashMap<String, ResolverMethodTable>,
56 pub imported_from: &'a HashMap<String, String>,
57}
58
59struct Position<'a> {
62 what: String,
63 declared_in: &'a str,
64 types: Vec<&'a ShadowedType>,
65}
66
67pub fn check_shadowed_uses(typed: &TypedCommons, scope: &ShadowScope<'_>) -> Vec<CompileError> {
72 if scope.shadowed.is_empty() {
73 return Vec::new();
74 }
75 let mut walk = Walk {
76 typed,
77 scope,
78 accepted: HashSet::new(),
79 errors: Vec::new(),
80 };
81 for item in &typed.commons.items {
82 match item {
83 CommonsItem::Fn(f) => {
84 walk.block(&f.body);
85 for c in f.requires.iter().chain(&f.ensures) {
86 walk.expr(&c.predicate);
87 }
88 }
89 CommonsItem::Service(s) => {
90 for h in &s.handlers {
91 walk.block(&h.body);
92 }
93 }
94 CommonsItem::Agent(a) => {
95 for h in &a.handlers {
96 walk.block(&h.body);
97 }
98 for p in a
99 .invariants
100 .iter()
101 .map(|i| &i.predicate)
102 .chain(a.transitions.iter().map(|t| &t.predicate))
103 {
104 walk.expr(p);
105 }
106 }
107 CommonsItem::Provider(p) => {
108 for op in &p.ops {
109 walk.block(&op.body);
110 }
111 }
112 _ => {}
113 }
114 }
115 walk.errors
116}
117
118struct Walk<'a, 's> {
119 typed: &'a TypedCommons,
120 scope: &'a ShadowScope<'s>,
121 accepted: HashSet<ExprId>,
124 errors: Vec<CompileError>,
125}
126
127impl<'a, 's> Walk<'a, 's>
128where
129 's: 'a,
130{
131 fn block(&mut self, b: &Block) {
132 let mut exprs = Vec::new();
133 for s in &b.statements {
134 statement_exprs(s, &mut exprs);
135 }
136 exprs.push(&b.tail);
137 for e in exprs {
138 self.expr(e);
139 }
140 }
141
142 fn expr(&mut self, e: &Expr) {
145 match &e.kind {
146 ExprKind::Call { args, .. } | ExprKind::MethodCall { args, .. } => {
147 for (arg, pos) in self.params_of(e, args.len()) {
148 if let Some(pos) = pos {
149 self.value_in(&args[arg], args[arg].span, &pos);
150 }
151 }
152 }
153 ExprKind::RecordConstruction { type_name, fields } => {
154 self.field_inits(&type_name.name, fields);
155 }
156 ExprKind::RecordSpread {
157 type_name: Some(type_name),
158 overrides,
159 ..
160 } => {
161 self.field_inits(&type_name.name, overrides);
162 }
163 ExprKind::Match { discriminant, arms } => {
164 for arm in arms {
165 self.pattern(discriminant, &arm.pattern);
166 }
167 }
168 ExprKind::Is { value, pattern } => self.pattern(value, pattern),
169 _ => {}
170 }
171 if !self.accepted.contains(&e.id)
172 && let Some(pos) = self.produced_by(e)
173 {
174 self.report(e.span, &pos, false);
175 }
176 for child in expr_children(e) {
177 self.expr(child);
178 }
179 }
180
181 fn value_in(&mut self, value: &Expr, span: Span, pos: &Position<'_>) {
183 let inner = peel(value);
184 if let Some(out) = self.produced_by(inner)
185 && out.types == pos.types
186 {
187 self.accepted.insert(inner.id);
188 self.accepted.insert(value.id);
189 return;
190 }
191 self.report(span, pos, true);
192 }
193
194 fn field_inits(&mut self, type_name: &str, fields: &[FieldInit]) {
195 let Some((decl, unit)) = self.imported_type(type_name) else {
196 return;
197 };
198 let TypeBody::Record(r) = &decl.body else {
199 return;
200 };
201 for init in fields {
202 let Some(field) = r.fields.iter().find(|f| f.name.name == init.name.name) else {
203 continue;
204 };
205 let Some(pos) = self.position(
206 format!("field `{type_name}.{}`", field.name.name),
207 unit,
208 &field.type_ref,
209 &decl.type_params,
210 ) else {
211 continue;
212 };
213 match &init.value {
214 Some(v) => self.value_in(v, v.span, &pos),
215 None => self.report(init.span, &pos, true),
217 }
218 }
219 }
220
221 fn pattern(&mut self, discriminant: &Expr, pattern: &Pattern) {
224 match pattern {
225 Pattern::Or(alts, _) => {
226 for p in alts {
227 self.pattern(discriminant, p);
228 }
229 }
230 Pattern::Variant {
231 variant, bindings, ..
232 } => {
233 let Some(name) = self.named_type_of(discriminant) else {
234 return;
235 };
236 let Some((decl, unit)) = self.imported_type(&name) else {
237 return;
238 };
239 let TypeBody::Sum(s) = &decl.body else {
240 return;
241 };
242 let Some(v) = s.variants.iter().find(|v| v.name.name == variant.name) else {
243 return;
244 };
245 for (i, b) in bindings.iter().enumerate() {
246 if b.is_wildcard() {
247 continue;
248 }
249 let field = match &b.kind {
250 PatternBindingKind::Positional { .. } => v.payload.get(i),
251 PatternBindingKind::Named { field, .. } => {
252 v.payload.iter().find(|p| p.name.name == field.name)
253 }
254 };
255 let Some(field) = field else {
256 continue;
257 };
258 if let Some(pos) = self.position(
259 format!("payload `{}` of `{name}.{}`", field.name.name, v.name.name),
260 unit,
261 &field.type_ref,
262 &decl.type_params,
263 ) {
264 self.report(b.span, &pos, false);
265 }
266 }
267 }
268 _ => {}
269 }
270 }
271
272 fn produced_by(&self, e: &Expr) -> Option<Position<'s>> {
275 match &e.kind {
276 ExprKind::FieldAccess { receiver, field } => {
277 let name = self.named_type_of(receiver)?;
278 let (decl, unit) = self.imported_type(&name)?;
279 let TypeBody::Record(r) = &decl.body else {
280 return None;
281 };
282 let f = r.fields.iter().find(|f| f.name.name == field.name)?;
283 self.position(
284 format!("field `{name}.{}`", f.name.name),
285 unit,
286 &f.type_ref,
287 &decl.type_params,
288 )
289 }
290 ExprKind::Call { .. } | ExprKind::MethodCall { .. } => {
291 let (f, unit, vars) = self.imported_fn(e)?;
292 self.position(
293 format!("the result of `{}`", fn_display(f)),
294 unit,
295 &f.return_type,
296 &vars,
297 )
298 }
299 _ => None,
300 }
301 }
302
303 fn params_of(&self, e: &Expr, arity: usize) -> Vec<(usize, Option<Position<'s>>)> {
307 if let Some((f, unit, vars)) = self.imported_fn(e) {
308 return f
312 .params
313 .iter()
314 .take(arity)
315 .enumerate()
316 .map(|(i, p)| {
317 let what = format!("parameter `{}` of `{}`", p.name.name, fn_display(f));
318 (i, self.position(what, unit, &p.type_ref, &vars))
319 })
320 .collect();
321 }
322 if let Some(Callee::Ctor { sum, tag }) = self.typed.callee(e.id)
323 && let Some((decl, unit)) = self.imported_type(&sum.name.name)
324 && decl.span == sum.span
325 && let TypeBody::Sum(s) = &decl.body
326 && let Some(v) = s.variants.iter().find(|v| &v.name.name == tag)
327 {
328 return v
329 .payload
330 .iter()
331 .take(arity)
332 .enumerate()
333 .map(|(i, p)| {
334 let what = format!(
335 "payload `{}` of `{}.{}`",
336 p.name.name, decl.name.name, v.name.name
337 );
338 (i, self.position(what, unit, &p.type_ref, &decl.type_params))
339 })
340 .collect();
341 }
342 Vec::new()
343 }
344
345 fn imported_fn(&self, e: &Expr) -> Option<(&'s FnDecl, &'s str, Vec<TypeParam>)> {
348 let sc = self.scope;
349 match self.typed.callee(e.id)? {
350 Callee::Fn(f) => {
351 let FnName::Free(n) = &f.name else {
352 return None;
353 };
354 if crate::symbols::is_synthetic_render(f) {
357 let decl = sc.combined_fns.get(&n.name)?;
358 return Some((decl.as_ref(), "bynk.locale", Vec::new()));
359 }
360 let unit = sc.imported_from.get(&n.name)?;
361 let decl = sc.combined_fns.get(&n.name)?;
362 (decl.span == f.span)
363 .then(|| (decl.as_ref(), unit.as_str(), decl.type_params.clone()))
364 }
365 Callee::Method(f) | Callee::Static(f) => {
366 let FnName::Method {
367 type_name,
368 method_name,
369 } = &f.name
370 else {
371 return None;
372 };
373 let unit = sc.imported_from.get(&type_name.name)?;
374 let table = sc.combined_methods.get(&type_name.name)?;
375 let decl = table
376 .instance
377 .get(&method_name.name)
378 .into_iter()
379 .chain(table.statics.get(&method_name.name))
380 .find(|d| d.span == f.span)?;
381 let mut vars = decl.type_params.clone();
382 if let Some(t) = sc.combined_types.get(&type_name.name) {
383 vars.extend(t.type_params.iter().cloned());
384 }
385 Some((decl.as_ref(), unit.as_str(), vars))
386 }
387 _ => None,
388 }
389 }
390
391 fn imported_type(&self, name: &str) -> Option<(&'s TypeDecl, &'s str)> {
394 let unit = self.scope.imported_from.get(name)?;
395 let decl = self.scope.combined_types.get(name)?;
396 Some((decl.as_ref(), unit.as_str()))
397 }
398
399 fn named_type_of(&self, e: &Expr) -> Option<String> {
401 match &*self.typed.expr_ty(e.id)? {
402 Ty::Named { name, .. } => Some(name.clone()),
403 _ => None,
404 }
405 }
406
407 fn position(
410 &self,
411 what: String,
412 unit: &'s str,
413 r: &TypeRef,
414 vars: &[TypeParam],
415 ) -> Option<Position<'s>> {
416 let types = self.scope.shadowed.in_type_ref(unit, r, vars);
417 (!types.is_empty()).then_some(Position {
418 what,
419 declared_in: unit,
420 types,
421 })
422 }
423
424 fn report(&mut self, span: Span, pos: &Position<'_>, into: bool) {
425 let t = pos.types[0];
426 let here = self.scope.unit;
427 let verb = if into { "takes" } else { "gives" };
428 let shadow = if t.bound == here {
429 format!("`{here}` declares its own `{}`", t.name)
430 } else {
431 format!("`{}` here names `{}`'s declaration", t.name, t.bound)
432 };
433 self.errors.push(
434 CompileError::new(
435 "bynk.uses.name_conflict",
436 span,
437 format!(
438 "{what} from `{decl}` {verb} `{owner}`'s `{name}`, but `{name}` in `{here}` \
439 names `{bound}`'s type",
440 what = pos.what,
441 decl = pos.declared_in,
442 owner = t.owner,
443 name = t.name,
444 bound = t.bound,
445 ),
446 )
447 .with_note(format!(
448 "{shadow}, which shadows `{owner}`'s; a value that crosses here cannot be \
449 checked against both, so rename one of the two types",
450 owner = t.owner,
451 )),
452 );
453 }
454}
455
456fn peel(e: &Expr) -> &Expr {
458 match &e.kind {
459 ExprKind::Paren(inner) => peel(inner),
460 _ => e,
461 }
462}
463
464fn fn_display(f: &FnDecl) -> String {
465 match &f.name {
466 FnName::Free(n) => n.name.clone(),
467 FnName::Method {
468 type_name,
469 method_name,
470 } => format!("{}.{}", type_name.name, method_name.name),
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use crate::testkit::analyse;
477
478 const CORE: &str = "commons t.core\n\ntype Repo = String where NonEmpty\n\n\
479 type Tag = | Named(value: Repo) | Anon\n";
480 const MODEL: &str = "commons t.model\n\nuses t.core\n\n\
481 type Run = { repo: Repo, n: Int }\n\n\
482 fn keep(r: Repo) -> Repo { r }\n\n\
483 fn first(r: Run) -> Repo { r.repo }\n\n\
484 fn count(r: Run) -> Int { r.n }\n\n\
485 fn Run.repoOf(self) -> Repo { self.repo }\n\n\
486 fn Run.withRepo(self, repo: Repo, k: Int) -> Run { Run { repo: repo, n: k } }\n\n\
487 fn Run.make(repo: Repo) -> Run { Run { repo: repo, n: 0 } }\n";
488
489 fn conflicts(body: &str) -> Vec<String> {
492 let app =
493 format!("commons t.app\n\nuses t.core\nuses t.model\n\ntype Repo = Int\n\n{body}\n");
494 let a = analyse(&[
495 ("t/core.bynk", CORE),
496 ("t/model.bynk", MODEL),
497 ("t/app.bynk", &app),
498 ]);
499 let other: Vec<_> = a
500 .error_categories()
501 .into_iter()
502 .filter(|c| *c != "bynk.uses.name_conflict")
503 .collect();
504 assert!(other.is_empty(), "unexpected diagnostics:\n{}", a.render());
505 a.analysis
506 .errors
507 .iter()
508 .filter(|e| e.error.category == "bynk.uses.name_conflict")
509 .map(|e| e.error.message.clone())
510 .collect()
511 }
512
513 fn assert_one(body: &str, what: &str) {
514 let got = conflicts(body);
515 assert!(
516 got.len() == 1 && got[0].starts_with(what),
517 "expected one conflict at {what}, got {got:#?}"
518 );
519 }
520
521 #[test]
522 fn a_shadow_the_code_never_meets_is_legal() {
523 assert_eq!(
524 conflicts("fn size(r: Run) -> Int { r.n }\n\nfn mine() -> Repo { 7 }"),
525 Vec::<String>::new()
526 );
527 }
528
529 #[test]
530 fn a_value_moved_between_imported_positions_is_legal() {
531 assert_eq!(
532 conflicts(
533 "fn retag(r: Run) -> Int { count(Run { repo: keep((r.repo)), n: 1 }) }\n\n\
534 fn again(r: Run) -> Run { Run { repo: first(r), n: 2 } }"
535 ),
536 Vec::<String>::new()
537 );
538 }
539
540 #[test]
541 fn a_local_value_into_a_record_field_conflicts() {
542 assert_one(
543 "fn make() -> Run { Run { repo: 42, n: 1 } }",
544 "field `Run.repo` from `t.model` takes `t.core`'s `Repo`",
545 );
546 }
547
548 #[test]
549 fn a_shorthand_field_conflicts() {
550 assert_one(
551 "fn make(repo: Repo) -> Run { Run { repo, n: 1 } }",
552 "field `Run.repo` from `t.model` takes",
553 );
554 }
555
556 #[test]
557 fn a_field_read_into_local_code_conflicts() {
558 assert_one(
559 "fn read(r: Run) -> Int { r.repo + 1 }",
560 "field `Run.repo` from `t.model` gives `t.core`'s `Repo`",
561 );
562 }
563
564 #[test]
565 fn a_call_result_into_local_code_conflicts() {
566 assert_one(
567 "fn read(r: Run) -> Int { first(r) }",
568 "the result of `first` from `t.model` gives",
569 );
570 }
571
572 #[test]
573 fn a_method_result_into_local_code_conflicts() {
574 assert_one(
575 "fn read(r: Run) -> Int { r.repoOf() }",
576 "the result of `Run.repoOf` from `t.model` gives",
577 );
578 }
579
580 #[test]
581 fn a_local_argument_conflicts() {
582 assert_one(
583 "fn pass(x: Repo) -> Int { count(Run { repo: keep(x), n: 1 }) }",
584 "parameter `r` of `keep` from `t.model` takes",
585 );
586 }
587
588 #[test]
589 fn a_value_through_a_let_conflicts() {
590 let got = conflicts(
593 "fn via(r: Run) -> Int {\n let x = first(r)\n count(Run { repo: x, n: 1 })\n}",
594 );
595 assert_eq!(got.len(), 2, "{got:#?}");
596 }
597
598 #[test]
599 fn a_bound_payload_conflicts() {
600 assert_one(
601 "fn tag(t: Tag) -> Int {\n match t {\n Named(v) => 1,\n Anon => 0,\n }\n}",
602 "payload `value` of `Tag.Named` from `t.core` gives",
603 );
604 }
605
606 #[test]
607 fn a_discarded_payload_is_legal() {
608 assert_eq!(
609 conflicts(
610 "fn tag(t: Tag) -> Int {\n match t {\n Named(_) => 1,\n Anon => 0,\n }\n}"
611 ),
612 Vec::<String>::new()
613 );
614 }
615
616 #[test]
617 fn a_local_payload_conflicts() {
618 assert_one(
619 "fn tag() -> Tag { Named(5) }",
620 "payload `value` of `Tag.Named` from `t.core` takes",
621 );
622 }
623
624 #[test]
627 fn a_method_parameter_is_checked() {
628 let got = conflicts("fn re(r: Run) -> Run { r.withRepo(42, 1) }");
629 assert!(
630 got.len() == 1 && got[0].contains("parameter `repo`"),
631 "expected one conflict at `repo`, got {got:#?}"
632 );
633 }
634
635 #[test]
636 fn a_static_parameter_is_checked() {
637 let got = conflicts("fn mk() -> Run { Run.make(42) }");
638 assert!(
639 got.len() == 1 && got[0].contains("parameter `repo`"),
640 "expected one conflict at `repo`, got {got:#?}"
641 );
642 }
643
644 #[test]
645 fn a_handler_body_is_checked() {
646 let a = analyse(&[
647 ("t/core.bynk", CORE),
648 ("t/model.bynk", MODEL),
649 (
650 "src/t/web.bynk",
651 "context t.web\n\nuses t.core\nuses t.model\n\ntype Repo = Int\n\n\
652 service read {\n on call(r: Run) -> Effect[Int] {\n r.repo + 1\n }\n}\n",
653 ),
654 ]);
655 a.assert_reports("bynk.uses.name_conflict");
656 }
657
658 #[test]
660 fn provider_ops_and_agent_handlers_are_checked() {
661 for (name, body) in [
662 (
663 "provider op",
664 "capability Reader {\n fn read(r: Run) -> Effect[Int]\n}\n\n\
665 provides Reader = Plain {\n fn read(r: Run) -> Effect[Int] {\n r.repo + 1\n }\n}\n",
666 ),
667 (
668 "agent handler",
669 "agent Holder {\n key id: String\n\n store hits: Cell[Int]\n\n \
670 on call read(r: Run) -> Effect[Int] {\n r.repo + 1\n }\n}\n",
671 ),
672 ] {
673 let src =
674 format!("context t.web\n\nuses t.core\nuses t.model\n\ntype Repo = Int\n\n{body}");
675 let a = analyse(&[
676 ("t/core.bynk", CORE),
677 ("t/model.bynk", MODEL),
678 ("src/t/web.bynk", &src),
679 ]);
680 assert!(
681 a.error_categories().contains(&"bynk.uses.name_conflict"),
682 "{name}: expected a conflict:\n{}",
683 a.render()
684 );
685 }
686 }
687}