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 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| 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 = 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;