Skip to main content

bynk_check/
shadow_uses.rs

1//! #1824: where a unit's own code meets an imported declaration's position
2//! that names a type the unit shadows.
3//!
4//! A unit's own `type Repo` shadows the `Repo` of a commons it `uses`
5//! directly (`compose_unit_symbols`: local first), and that stays legal:
6//! `1697_derived_names_collide` declares its own `Message` beside
7//! `uses bynk.locale.types`. But type names are bare strings to the checker,
8//! so an imported declaration that names `Repo` (`t.model`'s
9//! `type Run = { repo: Repo }`) is checked here against the *local* `Repo`.
10//! `Run { repo: 42 }` checks, and the emitted TypeScript, typed against
11//! `t.core`'s `Repo`, fails `tsc`.
12//!
13//! [`crate::project_model::close_reachable_types`] records each such reach
14//! ([`ShadowedReaches`]); this pass, run after a file's bodies are checked,
15//! reports `bynk.uses.name_conflict` where a value crosses between the unit's
16//! own code and one of those *shadowed positions*. A shadowed position is a
17//! position of an imported declaration whose declared type names a shadowed
18//! type, resolved in the declaring unit's scope:
19//!
20//! - a value goes **in** at a record field (`Run { repo: … }`, and the
21//!   overrides of `Run { ...r, repo: … }`), a fn, method or static parameter,
22//!   or a variant payload (`Text(…)`);
23//! - a value comes **out** of a field read (`r.repo`), a call's result, or a
24//!   payload a `match` arm or `is` test binds.
25//!
26//! A value that comes out of one shadowed position straight into another
27//! naming the same types never meets the unit's own code: both sides mean the
28//! imported type, in the checker and in the emitted TypeScript alike. That is
29//! 1697's `render(tag, message("greeting"))`, which stays legal. Every other
30//! crossing is a conflict, including one that goes through a `let` first:
31//! the rule reads the expression in the position, not where its value came
32//! from, so it errs towards rejecting.
33//!
34//! Not covered: a reference to an imported fn as a value (`list.map(f)`), and
35//! a payload bound by a nested pattern (`Some(Text(v))`). Test suites are
36//! checked by `crate::test_suites`, which does not run this pass.
37
38use 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
48/// The unit-level tables the pass resolves an imported declaration through.
49pub struct ShadowScope<'a> {
50    /// The unit being checked.
51    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
59/// A shadowed position of an imported declaration: what it is called in a
60/// diagnostic, the unit that declares it, and the shadowed types it names.
61struct Position<'a> {
62    what: String,
63    declared_in: &'a str,
64    types: Vec<&'a ShadowedType>,
65}
66
67/// Report every crossing in `typed`'s bodies: fns and methods (with their
68/// contracts), service and agent handlers, agent invariants and transitions,
69/// and provider operations. Empty when no imported declaration reaches a
70/// shadowed type, which is the common case and costs one check.
71pub 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    /// Values that come out of a shadowed position straight into one naming
122    /// the same types: not a crossing.
123    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    /// Pre-order, so a value an enclosing position accepts is marked before
143    /// the value itself is visited.
144    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    /// A value going into a shadowed position.
182    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                // Shorthand: the value is a local binding of the same name.
216                None => self.report(init.span, &pos, true),
217            }
218        }
219    }
220
221    /// A payload a top-level variant pattern binds, on a discriminant of an
222    /// imported sum type.
223    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    /// The shadowed position `e` reads a value out of, if any: a field of an
273    /// imported record, or an imported call's result.
274    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    /// Each argument of the call `e` paired with its parameter's shadowed
304    /// position, if the callee is an imported declaration and the parameter
305    /// is one.
306    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            // `params` holds no `self` (the parser keeps it as `has_self`),
309            // so a method's arguments line up with them one to one (#1879
310            // review).
311            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    /// The imported fn, method or static the call `e` dispatched to, with its
346    /// declaring unit and the type variables its signature binds.
347    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                // The bundle's own `render` declares `bynk.locale`'s signature
355                // (1697's `render(tag, message("greeting"))`).
356                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    /// An imported type declaration by name, with its declaring unit. A local
392    /// declaration of the name is not imported.
393    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    /// The name of the declared type `e` was checked as, if it is one.
400    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    /// `r` as a shadowed position, when it names a shadowed type in `unit`'s
408    /// scope.
409    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
456/// An argument without its parentheses, which carry no value of their own.
457fn 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    /// The `bynk.uses.name_conflict` messages for a `t.app` that shadows
490    /// `t.core`'s `Repo` and declares `body`. Any other error fails the test.
491    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        // The rule reads the expression in the position, not where its value
591        // came from: both ends of the `let` are crossings.
592        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    /// #1879 review: a method's explicit parameters line up with its
625    /// arguments (`params` holds no `self`), so the first is checked too.
626    #[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    /// #1879 review: provider operations and agent handlers are walked too.
659    #[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}