Skip to main content

harn_parser/builtin_methods/
mod.rs

1//! The single owner of builtin value methods.
2//!
3//! [`BUILTIN_METHODS`] names every method a builtin receiver supports, with
4//! its signature, whether it mutates its receiver, and its reference doc.
5//! [`REMOVED_METHODS`] records retired spellings and how to repair them. The
6//! VM and portable-kernel dispatch tables are drift-checked against these
7//! rows, and the typechecker, linter, language server, and generated docs
8//! derive from them.
9
10mod reference;
11mod table;
12
13use crate::ast::{Node, SNode};
14
15pub use harn_builtin_meta::{
16    MethodDescriptor, MethodEffect, MethodMigration, MethodReceiver, RemovedMethod,
17};
18pub use reference::{
19    render_reference, splice_reference, REFERENCE_BEGIN_MARKER, REFERENCE_END_MARKER,
20};
21pub use table::{BUILTIN_METHODS, REMOVED_METHODS};
22
23/// The method `name` on `receiver`, if it exists.
24pub fn method(receiver: MethodReceiver, name: &str) -> Option<&'static MethodDescriptor> {
25    BUILTIN_METHODS
26        .iter()
27        .find(|row| row.receiver == receiver && row.name() == name)
28}
29
30/// Every method on `receiver`, in table order.
31pub fn methods_of(receiver: MethodReceiver) -> impl Iterator<Item = &'static MethodDescriptor> {
32    BUILTIN_METHODS
33        .iter()
34        .filter(move |row| row.receiver == receiver)
35}
36
37/// Method names on `receiver`, in table order.
38pub fn method_names(receiver: MethodReceiver) -> Vec<&'static str> {
39    methods_of(receiver).map(MethodDescriptor::name).collect()
40}
41
42/// True when some builtin receiver has a method called `name`.
43pub fn is_method_name(name: &str) -> bool {
44    BUILTIN_METHODS.iter().any(|row| row.name() == name)
45}
46
47/// The retired spelling `name` on `receiver`, if it was removed.
48pub fn removed_method(receiver: MethodReceiver, name: &str) -> Option<&'static RemovedMethod> {
49    REMOVED_METHODS
50        .iter()
51        .find(|row| row.receiver == receiver && row.name == name)
52}
53
54/// Retired spellings that are also common verbs on host and connector
55/// objects (`store.delete(key)`), so a call through an unknown receiver is not
56/// evidence of a stale builtin call.
57const HOST_VERBS: &[&str] = &["delete"];
58
59/// Every removed row for a retired spelling that no live method on any
60/// receiver still uses, so a call to it is stale whatever the receiver's type.
61/// `count` is not one (iterators keep it); `to_lower` is. Empty otherwise.
62pub fn removed_method_on_any_receiver(name: &str) -> Vec<&'static RemovedMethod> {
63    if is_method_name(name) || HOST_VERBS.contains(&name) {
64        return Vec::new();
65    }
66    REMOVED_METHODS
67        .iter()
68        .filter(|row| row.name == name)
69        .collect()
70}
71
72/// Mutating list methods and the copy-returning method that computes the
73/// same result as a new value.
74pub const COPY_COUNTERPARTS: &[(&str, &str)] = &[
75    ("sort", "sorted"),
76    ("sort_by", "sorted_by"),
77    ("reverse", "reversed"),
78];
79
80/// The copy-returning counterpart of a mutating method (`sorted` for `sort`).
81pub fn copy_counterpart(mutating: &str) -> Option<&'static str> {
82    COPY_COUNTERPARTS
83        .iter()
84        .find(|(name, _)| *name == mutating)
85        .map(|(_, copy)| *copy)
86}
87
88/// The mutating counterpart of a copy-returning method (`sort` for `sorted`).
89pub fn in_place_counterpart(copy: &str) -> Option<&'static str> {
90    COPY_COUNTERPARTS
91        .iter()
92        .find(|(_, name)| *name == copy)
93        .map(|(mutating, _)| *mutating)
94}
95
96/// True when the method's result type mentions one of its generic
97/// parameters, so the result depends on the receiver or the arguments.
98pub fn returns_generic(row: &MethodDescriptor) -> bool {
99    fn mentions(ty: &harn_builtin_meta::Ty, params: &[&str]) -> bool {
100        use harn_builtin_meta::Ty;
101        match ty {
102            Ty::Generic(name) | Ty::Named(name) => params.contains(name),
103            Ty::Optional(inner) => mentions(inner, params),
104            Ty::Apply(_, args) | Ty::Union(args) => args.iter().any(|t| mentions(t, params)),
105            Ty::Fn(args, ret) => args.iter().any(|t| mentions(t, params)) || mentions(ret, params),
106            Ty::Shape(fields) => fields.iter().any(|f| mentions(&f.ty, params)),
107            Ty::OpenShape(fields, rests) => {
108                fields.iter().any(|f| mentions(&f.ty, params))
109                    || rests.iter().any(|t| mentions(t, params))
110            }
111            Ty::SchemaOf(name) => params.contains(name),
112            Ty::Any | Ty::Never | Ty::LitInt(_) | Ty::LitString(_) => false,
113        }
114    }
115    mentions(&row.signature.returns, row.signature.type_params)
116}
117
118/// True when some builtin receiver has a mutating method called `name`.
119pub fn is_mutating_method_name(name: &str) -> bool {
120    BUILTIN_METHODS
121        .iter()
122        .any(|row| row.is_mutating() && row.name() == name)
123}
124
125/// True when some builtin receiver has a pure method called `name` and no
126/// receiver has a mutating method of that name, so discarding the call's
127/// result can never be intended.
128pub fn is_pure_on_every_receiver(name: &str) -> bool {
129    is_method_name(name) && !is_mutating_method_name(name)
130}
131
132/// A place a mutating method can rewrite: a binding, or a property or
133/// subscript path rooted at one.
134pub fn is_mutable_place(node: &SNode) -> bool {
135    place_root(node).is_some()
136}
137
138/// The binding at the root of a place expression.
139pub fn place_root(node: &SNode) -> Option<&str> {
140    match &node.node {
141        Node::Identifier(name) => Some(name),
142        Node::PropertyAccess { object, .. } | Node::SubscriptAccess { object, .. } => {
143            place_root(object)
144        }
145        _ => None,
146    }
147}
148
149/// When `node` is `place.method(...)` naming a mutating builtin method, the
150/// receiver place it may rewrite. This is syntactic: a user struct method
151/// that happens to share the name also reports its receiver, which keeps
152/// write analyses conservative.
153pub fn mutating_call_place(node: &SNode) -> Option<&SNode> {
154    match &node.node {
155        Node::MethodCall { object, method, .. } if is_mutating_method_name(method) => {
156            is_mutable_place(object).then_some(&**object)
157        }
158        _ => None,
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn method_names_are_unique_per_receiver() {
168        for receiver in MethodReceiver::ALL {
169            let mut names = method_names(receiver);
170            let total = names.len();
171            names.sort_unstable();
172            names.dedup();
173            assert_eq!(names.len(), total, "{receiver:?} lists a method twice");
174        }
175    }
176
177    #[test]
178    fn removed_spellings_are_not_live_on_their_receiver() {
179        for row in REMOVED_METHODS {
180            assert!(
181                method(row.receiver, row.name).is_none(),
182                "`{}` is both removed and live on {:?}",
183                row.name,
184                row.receiver
185            );
186            if let MethodMigration::Rename(to) = row.migration {
187                assert!(
188                    method(row.receiver, to).is_some(),
189                    "`{}` on {:?} renames to `{to}`, which does not exist",
190                    row.name,
191                    row.receiver
192                );
193            }
194        }
195    }
196
197    #[test]
198    fn receiver_generics_are_declared_by_the_signatures_that_use_them() {
199        for row in BUILTIN_METHODS {
200            for param in row.receiver.receiver_type_params() {
201                let rendered = row.signature.to_string();
202                let used = rendered
203                    .split(|c: char| !c.is_alphanumeric() && c != '_')
204                    .skip(1)
205                    .any(|token| token == *param);
206                if used {
207                    assert!(
208                        row.signature.is_type_param(param),
209                        "{:?}.{} uses `{param}` without declaring it: {rendered}",
210                        row.receiver,
211                        row.name()
212                    );
213                }
214            }
215        }
216    }
217
218    #[test]
219    fn copy_counterparts_pair_a_mutating_and_a_pure_list_method() {
220        for (mutating, copy) in COPY_COUNTERPARTS {
221            assert!(method(MethodReceiver::List, mutating).is_some_and(|row| row.is_mutating()));
222            assert!(method(MethodReceiver::List, copy).is_some_and(|row| !row.is_mutating()));
223        }
224    }
225
226    #[test]
227    fn retired_spellings_only_flag_unknown_receivers_when_unambiguous() {
228        assert_eq!(removed_method_on_any_receiver("to_lower").len(), 1);
229        assert_eq!(removed_method_on_any_receiver("removing").len(), 2);
230        // Iterators keep `count`, lists keep `reverse`: an unknown receiver
231        // could be either, so neither is flagged without a receiver type.
232        assert!(removed_method_on_any_receiver("count").is_empty());
233        assert!(removed_method_on_any_receiver("reverse").is_empty());
234        // `delete` is a host verb (`store.delete(key)`).
235        assert!(removed_method_on_any_receiver("delete").is_empty());
236    }
237}