Skip to main content

miniextendr_macros/
r_shadowing.rs

1//! Calls to base functions that a formal of the same name would shadow.
2//!
3//! Generated R wrappers call base functions by their bare names: the
4//! precondition guards call `isTRUE()` and `length()`, a choice formal's
5//! default is `c("a", "b")`, a `Missing<T>` argument is forwarded as
6//! `if (missing(x)) quote(expr=) else x`, the check after `.Call()` calls
7//! `inherits()` and `attr()`, and dots are forwarded as `list(...)` (or, for
8//! `LazyDots`, the frame as `environment()`). R looks
9//! up the name of a called function starting in the wrapper's own frame, so a
10//! formal with the same name comes first: forcing it fails when the argument
11//! was omitted, and a function passed there is called in place of the base
12//! function.
13//!
14//! The rule: inside a generated wrapper, a call to one of these base functions
15//! always means the base function, even when a formal has that name.
16//! [`qualify_shadowed_calls`] writes `base::length(...)` for each call whose
17//! name is also a formal of the wrapper, and leaves every other call alone. It
18//! runs where every wrapper is finalized
19//! ([`r_wrapper_raw_literal`](crate::r_wrapper_raw_literal)), so no emitter
20//! can leave it out, and the author R code spliced into a wrapper (`r_entry`,
21//! `r_on_exit`, `r_post_checks`, parameter defaults) is qualified the same way.
22//!
23//! A formal is a Rust parameter name without its leading underscores and
24//! without `r#`, unless the parameter declares its R name with `formal`
25//! (#1842). A Rust identifier cannot contain `.`, so only a declared formal can
26//! be named `is.character`, `match.call` or `.Call`: dotted base callees are
27//! listed and qualified like the others. A declared formal can't take a name
28//! of the framework's own (`.miniextendr_arg_error`, `.mx_self`, `.call`,
29//! `.Generic`; `r_wrapper_builder::declared_formal_problem`), so the calls to
30//! those are never qualified.
31//!
32//! Only the colliding calls are qualified because `::` costs time on every
33//! call: the byte compiler compiles `base::f(x)` as a call to the `::`
34//! primitive instead of inlining `f`. On a byte-compiled one-argument wrapper,
35//! qualifying the guards added about 200 ns per call (+19%), and qualifying
36//! every call about 475 ns (+44%). When no formal collides, the text is
37//! returned unchanged.
38//!
39//! Three generators do not finalize through here: the sidecar accessors of
40//! `#[derive(ExternalPtr)]`, the functions `#[derive(Vctrs)]` emits and the
41//! `all.equal()` methods of `#[derive(AllEqual)]`. Their formals are fixed
42//! (`x`, `value`, `to`, `target`, `current`, `tolerance`, `scale`, ...), and
43//! none of their bodies calls a function of one of those names.
44
45use std::borrow::Cow;
46use std::collections::BTreeSet;
47use std::ops::Range;
48
49/// The base functions that generated wrapper text calls by their bare names.
50///
51/// A call to one of these is qualified when a formal of the wrapper has the
52/// same name. The `generated_callees_are_listed` test checks the generated
53/// wrappers in the macro snapshots and the tracked cross-package wrapper
54/// files, and fails on a callee that is missing here and is not one of the
55/// framework's names a declared formal can't take.
56pub(crate) const BASE_CALLEES: &[&str] = &[
57    ".Call",
58    ".subset2",
59    "NextMethod",
60    // The seed contract of `rng(seed = ..)` (#2198), with `as.list`,
61    // `assign`, `get`, `globalenv` and `set.seed`.
62    "RNGkind",
63    "UseMethod",
64    "all",
65    "anyNA",
66    "as.list",
67    "as.list.environment",
68    "assign",
69    "attr",
70    "c",
71    // The empty completion of a restored pointer under
72    // `restored(refuse = "every_read")` (#1901).
73    "character",
74    "class",
75    "double",
76    "emptyenv",
77    "environment",
78    "environmentName",
79    "exists",
80    "get",
81    "get0",
82    "getOption",
83    "globalenv",
84    "grep",
85    "identical",
86    "inherits",
87    "invisible",
88    "is.atomic",
89    "is.character",
90    "is.complex",
91    "is.double",
92    "is.environment",
93    "is.factor",
94    "is.integer",
95    "is.list",
96    "is.logical",
97    "is.na",
98    "is.null",
99    "is.numeric",
100    "is.primitive",
101    "is.raw",
102    "isTRUE",
103    "lapply",
104    "length",
105    "list",
106    "local",
107    "ls",
108    "match",
109    "missing",
110    "names",
111    // The argument count an `NArgs` parameter receives (#1860).
112    "nargs",
113    "new.env",
114    "on.exit",
115    // The environment a quoted argument is forwarded with.
116    "parent.frame",
117    "paste0",
118    // The list lookup of the S3 field methods (#1891).
119    "pmatch",
120    "quote",
121    "return",
122    "seq_along",
123    "set.seed",
124    "simpleWarning",
125    "sprintf",
126    "standardGeneric",
127    "stop",
128    "structure",
129    "substitute",
130    "switch",
131    "sys.call",
132    "topenv",
133    "trunc",
134    "typeof",
135    "unclass",
136    "unique",
137    "vector",
138    "warning",
139];
140
141/// The R formals of every wrapper built from `signatures`, each a parameter
142/// list with the formals it declares: the union of each signature's
143/// [`r_formal_names`](crate::r_wrapper_builder::r_formal_names).
144///
145/// An impl block or a trait impl is finalized as one text, so its formals are
146/// pooled across its methods. A method whose sibling has a colliding formal is
147/// then qualified too, which calls the same function.
148pub(crate) fn formal_names<'a>(
149    signatures: impl IntoIterator<
150        Item = (
151            &'a syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
152            crate::r_wrapper_builder::Formals,
153        ),
154    >,
155) -> BTreeSet<String> {
156    let mut out = BTreeSet::new();
157    for (inputs, formals) in signatures {
158        out.extend(
159            crate::r_wrapper_builder::r_formal_names(inputs, &formals).map(|(name, _)| name),
160        );
161    }
162    out
163}
164
165/// Write `base::` before every call in `text` to a [`BASE_CALLEES`] function
166/// that shares its name with one of `formals`.
167///
168/// A call is a name followed by optional spaces and `(`. Strings, raw strings,
169/// comments (roxygen blocks included) and backtick names are left as they are,
170/// as is a name after `::`, `:::`, `$` or `@`. A replacement call
171/// (`class(x) <- v`) is qualified too: R resolves `base::class(x) <- v`
172/// through `base::"class<-"`. The match_arg choices placeholder, which the
173/// wrapper writer replaces with `c(<choices>)`, counts
174/// as a call to `c`.
175///
176/// Returns `text` borrowed when nothing is qualified.
177pub(crate) fn qualify_shadowed_calls<'t>(
178    text: &'t str,
179    formals: &BTreeSet<String>,
180) -> Cow<'t, str> {
181    let hits: Vec<&str> = BASE_CALLEES
182        .iter()
183        .copied()
184        .filter(|name| formals.contains(*name))
185        .collect();
186    if hits.is_empty() {
187        return Cow::Borrowed(text);
188    }
189    let mut out = String::new();
190    let mut copied = 0;
191    for ident in identifiers(text) {
192        let name = &text[ident.clone()];
193        let callee = if name.starts_with(crate::match_arg_keys::CHOICES_PLACEHOLDER_PREFIX) {
194            "c"
195        } else if is_call(text, ident.end) {
196            name
197        } else {
198            continue;
199        };
200        if hits.contains(&callee) && !is_accessed(text, ident.start) {
201            out.push_str(&text[copied..ident.start]);
202            out.push_str("base::");
203            copied = ident.start;
204        }
205    }
206    if out.is_empty() {
207        return Cow::Borrowed(text);
208    }
209    out.push_str(&text[copied..]);
210    Cow::Owned(out)
211}
212
213// region: lexer
214
215/// Byte ranges of the R identifiers in `text`, skipping strings (`"..."`,
216/// `'...'`), raw strings (`r"(...)"`, `R'[...]'`, `r"-{...}-"`), comments,
217/// backtick names, `%op%` operators and numbers.
218fn identifiers(text: &str) -> Vec<Range<usize>> {
219    let mut out = Vec::new();
220    let mut i = 0;
221    while let Some(c) = text[i..].chars().next() {
222        match c {
223            '"' | '\'' | '`' => i = quoted_end(text, i, c),
224            '#' => i = text[i..].find('\n').map_or(text.len(), |n| i + n),
225            '%' => i = infix_end(text, i),
226            c if c.is_ascii_digit() => i = ident_end(text, i),
227            c if c.is_alphabetic() || c == '.' => {
228                let end = ident_end(text, i);
229                if #[allow(non_exhaustive_omitted_patterns)] match &text[i..end] {
    "r" | "R" => true,
    _ => false,
}matches!(&text[i..end], "r" | "R")
230                    && let Some(raw_end) = raw_string_end(text, end)
231                {
232                    i = raw_end;
233                    continue;
234                }
235                out.push(i..end);
236                i = end;
237            }
238            c => i += c.len_utf8(),
239        }
240    }
241    out
242}
243
244/// End of the identifier (or number) starting at `start`.
245fn ident_end(text: &str, start: usize) -> usize {
246    text[start..]
247        .char_indices()
248        .find(|&(_, c)| !(c.is_alphanumeric() || c == '.' || c == '_'))
249        .map_or(text.len(), |(n, _)| start + n)
250}
251
252/// End of the string or backtick name opened by `quote` at `start`, after its
253/// closing quote. A backslash escapes the next character.
254fn quoted_end(text: &str, start: usize, quote: char) -> usize {
255    let mut chars = text[start + 1..].char_indices();
256    while let Some((n, c)) = chars.next() {
257        if c == '\\' {
258            chars.next();
259        } else if c == quote {
260            return start + 1 + n + c.len_utf8();
261        }
262    }
263    text.len()
264}
265
266/// End of the `%op%` operator starting at `start`, or just past the `%` when
267/// the line has no closing `%`.
268fn infix_end(text: &str, start: usize) -> usize {
269    let rest = &text[start + 1..];
270    let line = &rest[..rest.find('\n').unwrap_or(rest.len())];
271    line.find('%').map_or(start + 1, |n| start + 1 + n + 1)
272}
273
274/// End of the raw string whose quote is at `quote_at`, right after an `r` or
275/// `R`: the quote, any dashes, and an opening `(`, `[` or `{`, closed by the
276/// matching bracket, the same dashes and the same quote. `None` when the
277/// quote does not open a raw string.
278fn raw_string_end(text: &str, quote_at: usize) -> Option<usize> {
279    let rest = &text[quote_at..];
280    let quote = rest.chars().next().filter(|c| #[allow(non_exhaustive_omitted_patterns)] match c {
    '"' | '\'' => true,
    _ => false,
}matches!(c, '"' | '\''))?;
281    let dashes = rest[1..].chars().take_while(|&c| c == '-').count();
282    let close = match rest[1 + dashes..].chars().next()? {
283        '(' => ')',
284        '[' => ']',
285        '{' => '}',
286        _ => return None,
287    };
288    let terminator = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1}{0}{2}", "-".repeat(dashes),
                close, quote))
    })format!("{close}{}{quote}", "-".repeat(dashes));
289    let body = 1 + dashes + 1;
290    Some(
291        rest[body..]
292            .find(&terminator)
293            .map_or(text.len(), |n| quote_at + body + n + terminator.len()),
294    )
295}
296
297/// Whether the identifier ending at `end` is called: followed by optional
298/// spaces and `(`.
299fn is_call(text: &str, end: usize) -> bool {
300    text[end..].trim_start_matches([' ', '\t']).starts_with('(')
301}
302
303/// Whether the identifier starting at `start` follows `::`, `:::`, `$` or `@`,
304/// so it names a namespace export or a member, not a function in scope.
305fn is_accessed(text: &str, start: usize) -> bool {
306    let before = text[..start].trim_end();
307    before.ends_with("::") || before.ends_with('$') || before.ends_with('@')
308}
309
310// endregion
311
312#[cfg(test)]
313mod tests;