Skip to main content

r/interpreter/builtins/
conditions.rs

1//! R condition system builtins — stop, warning, message, signalCondition,
2//! condition constructors, condition accessors, and restart invocation.
3
4use crate::interpreter::value::*;
5use crate::interpreter::{BuiltinContext, DiagnosticStyle};
6use itertools::Itertools;
7use minir_macros::{builtin, interpreter_builtin};
8
9// region: Helpers
10
11/// Check whether a named argument is a truthy boolean (default `default`).
12fn named_bool(named: &[(String, RValue)], key: &str, default: bool) -> bool {
13    named
14        .iter()
15        .find(|(k, _)| k == key)
16        .map(|(_, v)| match v {
17            RValue::Vector(rv) => rv.as_logical_scalar().unwrap_or(default),
18            _ => default,
19        })
20        .unwrap_or(default)
21}
22
23// endregion
24
25// region: stop / warning / message
26
27/// Signal an error condition and stop execution.
28///
29/// @param ... character strings concatenated into the error message, or a condition object
30/// @param call. logical, whether to include the call in the condition (default TRUE, currently ignored)
31/// @return does not return; signals an error
32#[builtin]
33fn builtin_stop(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
34    // If the first arg is already a condition object, re-signal it
35    if let Some(first) = args.first() {
36        let classes = get_class(first);
37        if classes.iter().any(|c| c == "condition") {
38            return Err(RError::Condition {
39                condition: first.clone(),
40                kind: ConditionKind::Error,
41            });
42        }
43    }
44    let msg = args
45        .iter()
46        .map(|v| match v {
47            RValue::Vector(vec) => vec.as_character_scalar().unwrap_or_default(),
48            other => format!("{}", other),
49        })
50        .join("");
51    let condition = make_condition(&msg, &["simpleError", "error", "condition"]);
52    Err(RError::Condition {
53        condition,
54        kind: ConditionKind::Error,
55    })
56}
57
58/// Signal a warning condition.
59///
60/// @param ... character strings concatenated into the warning message
61/// @param call. logical, whether to include the call (default TRUE, currently ignored)
62/// @param immediate. logical, whether to print immediately (default FALSE, currently ignored)
63/// @return NULL, invisibly
64#[interpreter_builtin]
65fn interp_warning(
66    args: &[RValue],
67    _named: &[(String, RValue)],
68    context: &BuiltinContext,
69) -> Result<RValue, RError> {
70    // If the first arg is already a condition object, re-signal it
71    if let Some(first) = args.first() {
72        let classes = get_class(first);
73        if classes.iter().any(|c| c == "condition") {
74            let muffled = context
75                .with_interpreter(|interp| interp.signal_condition(first, &interp.global_env))?;
76            if !muffled {
77                // Extract message from condition for display
78                let msg = condition_message_str(first);
79                context.write_err_colored("Warning message:\n", DiagnosticStyle::Warning);
80                context.write_err(&format!("{}\n", msg));
81            }
82            return Ok(RValue::Null);
83        }
84    }
85    let msg = args
86        .iter()
87        .map(|v| match v {
88            RValue::Vector(vec) => vec.as_character_scalar().unwrap_or_default(),
89            other => format!("{}", other),
90        })
91        .join("");
92    let condition = make_condition(&msg, &["simpleWarning", "warning", "condition"]);
93    let muffled = context
94        .with_interpreter(|interp| interp.signal_condition(&condition, &interp.global_env))?;
95    if !muffled {
96        context.write_err_colored("Warning message:\n", DiagnosticStyle::Warning);
97        context.write_err(&format!("{}\n", msg));
98    }
99    Ok(RValue::Null)
100}
101
102/// Print a diagnostic message to stderr.
103///
104/// @param ... character strings concatenated into the message
105/// @param domain character string for translation domain (currently ignored)
106/// @param appendLF logical, whether to append a newline (default TRUE)
107/// @return NULL, invisibly
108#[interpreter_builtin]
109fn interp_message(
110    args: &[RValue],
111    named: &[(String, RValue)],
112    context: &BuiltinContext,
113) -> Result<RValue, RError> {
114    let append_lf = named_bool(named, "appendLF", true);
115
116    // If the first arg is already a condition object, re-signal it
117    if let Some(first) = args.first() {
118        let classes = get_class(first);
119        if classes.iter().any(|c| c == "condition") {
120            let muffled = context
121                .with_interpreter(|interp| interp.signal_condition(first, &interp.global_env))?;
122            if !muffled {
123                let msg = condition_message_str(first);
124                if append_lf {
125                    context.write_err_colored(&format!("{}\n", msg), DiagnosticStyle::Message);
126                } else {
127                    context.write_err_colored(&msg, DiagnosticStyle::Message);
128                }
129            }
130            return Ok(RValue::Null);
131        }
132    }
133
134    let msg = args
135        .iter()
136        .map(|v| match v {
137            RValue::Vector(vec) => vec.as_character_scalar().unwrap_or_default(),
138            other => format!("{}", other),
139        })
140        .join("");
141    let condition = make_condition(&msg, &["simpleMessage", "message", "condition"]);
142    let muffled = context
143        .with_interpreter(|interp| interp.signal_condition(&condition, &interp.global_env))?;
144    if !muffled {
145        if append_lf {
146            context.write_err_colored(&format!("{}\n", msg), DiagnosticStyle::Message);
147        } else {
148            context.write_err_colored(&msg, DiagnosticStyle::Message);
149        }
150    }
151    Ok(RValue::Null)
152}
153
154// endregion
155
156// region: signalCondition
157
158/// Signal a condition object to calling handlers without unwinding.
159///
160/// This is the low-level primitive used by the condition system. It walks the
161/// handler stack and invokes matching handlers in calling-handler style (the
162/// handler runs and then returns to the signaler).
163///
164/// @param c a condition object (list with class attribute containing condition classes)
165/// @return NULL, invisibly
166#[interpreter_builtin(name = "signalCondition", min_args = 1)]
167fn interp_signal_condition(
168    args: &[RValue],
169    _named: &[(String, RValue)],
170    context: &BuiltinContext,
171) -> Result<RValue, RError> {
172    let condition = args.first().ok_or_else(|| {
173        RError::new(
174            RErrorKind::Argument,
175            "argument 'cond' is missing, with no default".to_string(),
176        )
177    })?;
178    context.with_interpreter(|interp| {
179        interp.signal_condition(condition, &interp.global_env)?;
180        Ok(RValue::Null)
181    })
182}
183
184// endregion
185
186/// Extract the message string from a condition object (list with "message" element).
187fn condition_message_str(cond: &RValue) -> String {
188    if let RValue::List(list) = cond {
189        for (name, val) in &list.values {
190            if name.as_deref() == Some("message") {
191                if let RValue::Vector(rv) = val {
192                    if let Some(s) = rv.as_character_scalar() {
193                        return s;
194                    }
195                }
196            }
197        }
198    }
199    String::new()
200}
201
202/// Construct a simple condition object.
203///
204/// @param msg character string giving the condition message
205/// @param call the call associated with the condition (default NULL)
206/// @param class additional class to prepend to "condition"
207/// @return a condition list with message, call, and class attributes
208#[builtin(name = "simpleCondition", min_args = 1)]
209fn builtin_simple_condition(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
210    let msg = args
211        .first()
212        .and_then(|v| v.as_vector().and_then(|rv| rv.as_character_scalar()))
213        .unwrap_or_default();
214    let call = args.get(1).cloned().unwrap_or(RValue::Null);
215    let extra_class = named
216        .iter()
217        .find(|(k, _)| k == "class")
218        .and_then(|(_, v)| v.as_vector().and_then(|rv| rv.as_character_scalar()))
219        .unwrap_or_default();
220    let mut classes: Vec<&str> = Vec::new();
221    if !extra_class.is_empty() {
222        classes.push(&extra_class);
223    }
224    classes.push("condition");
225    let mut list = RList::new(vec![
226        (
227            Some("message".to_string()),
228            RValue::vec(Vector::Character(vec![Some(msg)].into())),
229        ),
230        (Some("call".to_string()), call),
231    ]);
232    let class_vec: Vec<Option<String>> = classes.iter().map(|s| Some(s.to_string())).collect();
233    list.set_attr(
234        "class".to_string(),
235        RValue::vec(Vector::Character(class_vec.into())),
236    );
237    Ok(RValue::List(list))
238}
239
240/// Construct a simple error condition object.
241///
242/// @param message character string giving the error message
243/// @param call the call associated with the error (default NULL)
244/// @return a condition list with class c("simpleError", "error", "condition")
245#[builtin(name = "simpleError", min_args = 1)]
246fn builtin_simple_error(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
247    let msg = args
248        .first()
249        .and_then(|v| v.as_vector().and_then(|rv| rv.as_character_scalar()))
250        .unwrap_or_default();
251    let call = args.get(1).cloned().unwrap_or(RValue::Null);
252    Ok(make_condition_with_call(
253        &msg,
254        call,
255        &["simpleError", "error", "condition"],
256    ))
257}
258
259/// Construct a simple warning condition object.
260///
261/// @param message character string giving the warning message
262/// @param call the call associated with the warning (default NULL)
263/// @return a condition list with class c("simpleWarning", "warning", "condition")
264#[builtin(name = "simpleWarning", min_args = 1)]
265fn builtin_simple_warning(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
266    let msg = args
267        .first()
268        .and_then(|v| v.as_vector().and_then(|rv| rv.as_character_scalar()))
269        .unwrap_or_default();
270    let call = args.get(1).cloned().unwrap_or(RValue::Null);
271    Ok(make_condition_with_call(
272        &msg,
273        call,
274        &["simpleWarning", "warning", "condition"],
275    ))
276}
277
278/// Construct a simple message condition object.
279///
280/// @param message character string giving the message
281/// @param call the call associated with the message (default NULL)
282/// @return a condition list with class c("simpleMessage", "message", "condition")
283#[builtin(name = "simpleMessage", min_args = 1)]
284fn builtin_simple_message(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
285    let msg = args
286        .first()
287        .and_then(|v| v.as_vector().and_then(|rv| rv.as_character_scalar()))
288        .unwrap_or_default();
289    let call = args.get(1).cloned().unwrap_or(RValue::Null);
290    Ok(make_condition_with_call(
291        &msg,
292        call,
293        &["simpleMessage", "message", "condition"],
294    ))
295}
296
297/// Extract the message from a condition object.
298///
299/// @param c a condition object (list with "message" element)
300/// @return character string giving the condition message
301#[builtin(name = "conditionMessage", min_args = 1)]
302fn builtin_condition_message(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
303    match args.first() {
304        Some(RValue::List(list)) => {
305            for (name, val) in &list.values {
306                if name.as_deref() == Some("message") {
307                    return Ok(val.clone());
308                }
309            }
310            Ok(RValue::Null)
311        }
312        _ => Err(RError::new(
313            RErrorKind::Argument,
314            "conditionMessage requires a condition object".to_string(),
315        )),
316    }
317}
318
319/// Extract the call from a condition object.
320///
321/// @param c a condition object (list with "call" element)
322/// @return the call associated with the condition, or NULL
323#[builtin(name = "conditionCall", min_args = 1)]
324fn builtin_condition_call(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
325    match args.first() {
326        Some(RValue::List(list)) => {
327            for (name, val) in &list.values {
328                if name.as_deref() == Some("call") {
329                    return Ok(val.clone());
330                }
331            }
332            Ok(RValue::Null)
333        }
334        _ => Err(RError::new(
335            RErrorKind::Argument,
336            "conditionCall requires a condition object".to_string(),
337        )),
338    }
339}
340
341/// Invoke a restart by name, transferring control to the corresponding handler.
342///
343/// @param r character string naming the restart to invoke
344/// @return does not return; transfers control to the restart handler
345#[builtin(name = "invokeRestart", min_args = 1)]
346fn builtin_invoke_restart(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
347    let restart_name = args
348        .first()
349        .and_then(|v| v.as_vector().and_then(|rv| rv.as_character_scalar()))
350        .ok_or_else(|| {
351            RError::new(
352                RErrorKind::Argument,
353                "restart name must be a string".to_string(),
354            )
355        })?;
356    // Signal the restart by throwing it as an RError::other — caught by signal_condition
357    Err(RError::other(restart_name))
358}