Skip to main content

r/interpreter/builtins/
s4.rs

1//! S4 OOP system — class registry, method dispatch, and object construction.
2//!
3//! S4 is R's formal object system. This module implements:
4//! - Class definitions with slots, inheritance, and prototypes (setClass)
5//! - Object construction with slot validation (new)
6//! - Inheritance-aware type checking (is)
7//! - Generic functions and method dispatch (setGeneric, setMethod)
8//! - Slot access and modification (slot, slot<-)
9
10use crate::interpreter::value::*;
11use crate::interpreter::{BuiltinContext, S4ClassDef, S4GenericDef};
12use minir_macros::{builtin, interpreter_builtin};
13
14// region: Helpers
15
16/// Extract slot definitions from a `representation` or `slots` argument.
17/// Accepts a named list (list(x = "numeric", y = "character")) or a named
18/// character vector (c(x = "numeric", y = "character")).
19fn extract_slots(val: &RValue) -> Vec<(String, String)> {
20    match val {
21        RValue::List(list) => list
22            .values
23            .iter()
24            .filter_map(|(name, v)| {
25                let slot_name = name.as_ref()?;
26                let type_name = match v {
27                    RValue::Vector(rv) => rv.as_character_scalar(),
28                    _ => None,
29                }
30                .unwrap_or_else(|| "ANY".to_string());
31                Some((slot_name.clone(), type_name))
32            })
33            .collect(),
34        RValue::Vector(rv) => {
35            // Named character vector: names are slot names, values are type names
36            let names = rv
37                .attrs
38                .as_ref()
39                .and_then(|a| a.get("names"))
40                .and_then(|v| {
41                    if let RValue::Vector(nv) = v {
42                        if let Vector::Character(c) = &nv.inner {
43                            Some(c.clone())
44                        } else {
45                            None
46                        }
47                    } else {
48                        None
49                    }
50                });
51            if let (Some(names), Vector::Character(types)) = (names, &rv.inner) {
52                names
53                    .iter()
54                    .zip(types.iter())
55                    .filter_map(|(n, t)| {
56                        let slot_name = n.as_ref()?;
57                        let type_name = t.clone().unwrap_or_else(|| "ANY".to_string());
58                        Some((slot_name.clone(), type_name))
59                    })
60                    .collect()
61            } else {
62                Vec::new()
63            }
64        }
65        _ => Vec::new(),
66    }
67}
68
69/// Extract a character vector from an RValue (for `contains` argument).
70fn extract_character_vec(val: &RValue) -> Vec<String> {
71    match val {
72        RValue::Vector(rv) => rv.to_characters().into_iter().flatten().collect(),
73        _ => Vec::new(),
74    }
75}
76
77/// Extract prototype values from a named list or named vector.
78fn extract_prototype(val: &RValue) -> Vec<(String, RValue)> {
79    match val {
80        RValue::List(list) => list
81            .values
82            .iter()
83            .filter_map(|(name, v)| Some((name.as_ref()?.clone(), v.clone())))
84            .collect(),
85        _ => Vec::new(),
86    }
87}
88
89/// Collect the full inheritance chain for a class, including the class itself.
90/// Uses the S4 class registry to walk the `contains` hierarchy.
91fn inheritance_chain(
92    class_name: &str,
93    registry: &std::collections::HashMap<String, S4ClassDef>,
94) -> Vec<String> {
95    let mut chain = vec![class_name.to_string()];
96    let mut visited = std::collections::HashSet::new();
97    visited.insert(class_name.to_string());
98    let mut queue = std::collections::VecDeque::new();
99    queue.push_back(class_name.to_string());
100
101    while let Some(current) = queue.pop_front() {
102        if let Some(def) = registry.get(&current) {
103            for parent in &def.contains {
104                if visited.insert(parent.clone()) {
105                    chain.push(parent.clone());
106                    queue.push_back(parent.clone());
107                }
108            }
109        }
110    }
111
112    chain
113}
114
115/// Collect all slot definitions for a class including inherited slots.
116fn all_slots_for_class(
117    class_name: &str,
118    registry: &std::collections::HashMap<String, S4ClassDef>,
119) -> Vec<(String, String)> {
120    let chain = inheritance_chain(class_name, registry);
121    let mut slots = Vec::new();
122    let mut seen = std::collections::HashSet::new();
123    // Walk from most ancestral to most derived so derived slots override
124    for class in chain.iter().rev() {
125        if let Some(def) = registry.get(class) {
126            for (name, typ) in &def.slots {
127                if seen.insert(name.clone()) {
128                    slots.push((name.clone(), typ.clone()));
129                }
130            }
131        }
132    }
133    slots
134}
135
136/// Collect all prototype defaults for a class including inherited prototypes.
137fn all_prototypes_for_class(
138    class_name: &str,
139    registry: &std::collections::HashMap<String, S4ClassDef>,
140) -> Vec<(String, RValue)> {
141    let chain = inheritance_chain(class_name, registry);
142    let mut proto = Vec::new();
143    let mut seen = std::collections::HashSet::new();
144    // Walk from most ancestral to most derived so derived prototypes override
145    for class in chain.iter().rev() {
146        if let Some(def) = registry.get(class) {
147            for (name, val) in &def.prototype {
148                if seen.insert(name.clone()) {
149                    proto.push((name.clone(), val.clone()));
150                }
151            }
152        }
153    }
154    proto
155}
156
157// endregion
158
159// region: setClass
160
161/// Define an S4 class.
162///
163/// Registers the class in the per-interpreter S4 class registry with its
164/// slot definitions, superclasses (inheritance), and prototype defaults.
165///
166/// @param Class character string naming the class
167/// @param representation named list/vector of slot types (synonym for slots)
168/// @param slots named list/vector of slot types
169/// @param contains character vector of superclass names
170/// @param prototype named list of default slot values
171/// @param validity validity-checking function
172/// @param sealed logical, whether the class definition is sealed
173/// @return the class name (invisibly)
174#[interpreter_builtin(name = "setClass", min_args = 1, namespace = "methods")]
175fn interp_set_class_s4(
176    args: &[RValue],
177    named: &[(String, RValue)],
178    context: &BuiltinContext,
179) -> Result<RValue, RError> {
180    let class_name = args
181        .first()
182        .and_then(|v| v.as_vector()?.as_character_scalar())
183        .ok_or_else(|| {
184            RError::new(
185                RErrorKind::Argument,
186                "setClass() requires a character string for the class name",
187            )
188        })?;
189
190    // Extract slots from:
191    // 1. Named arg "representation" or "slots"
192    // 2. Positional arg 2 (common pattern: setClass("Name", representation(...)))
193    let slots_val = named
194        .iter()
195        .find(|(n, _)| n == "representation" || n == "slots")
196        .map(|(_, v)| v)
197        .or_else(|| args.get(1));
198
199    let slots = slots_val.map(extract_slots).unwrap_or_default();
200
201    // Extract superclasses from "contains" named arg or positional arg 3
202    let contains = named
203        .iter()
204        .find(|(n, _)| n == "contains")
205        .map(|(_, v)| v)
206        .or_else(|| args.get(2))
207        .map(extract_character_vec)
208        .unwrap_or_default();
209
210    // Extract prototype defaults from "prototype" named arg
211    let prototype = named
212        .iter()
213        .find(|(n, _)| n == "prototype")
214        .map(|(_, v)| extract_prototype(v))
215        .unwrap_or_default();
216
217    // Check for virtual class
218    let is_virtual = named
219        .iter()
220        .find(|(n, _)| n == "virtual")
221        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
222        .unwrap_or(false);
223
224    // Extract validity function
225    let validity = named
226        .iter()
227        .find(|(n, _)| n == "validity")
228        .map(|(_, v)| v.clone())
229        .filter(|v| matches!(v, RValue::Function(_)));
230
231    let class_def = S4ClassDef {
232        name: class_name.clone(),
233        slots,
234        contains,
235        prototype,
236        is_virtual,
237        validity,
238    };
239
240    // Store in the per-interpreter registry
241    context.with_interpreter(|interp| {
242        interp
243            .s4_classes
244            .borrow_mut()
245            .insert(class_name.clone(), class_def);
246    });
247
248    Ok(RValue::vec(Vector::Character(
249        vec![Some(class_name)].into(),
250    )))
251}
252
253// endregion
254
255// region: new
256
257/// Create a new S4 object.
258///
259/// Validates slot values against the registered class definition and
260/// initializes unspecified slots with prototype defaults.
261///
262/// @param Class character string naming the S4 class
263/// @param ... slot values as named arguments
264/// @return a list with slots as named elements and the class attribute set
265#[interpreter_builtin(name = "new", min_args = 1, namespace = "methods")]
266fn interp_new(
267    args: &[RValue],
268    named: &[(String, RValue)],
269    context: &BuiltinContext,
270) -> Result<RValue, RError> {
271    let class_name = args
272        .first()
273        .and_then(|v| v.as_vector()?.as_character_scalar())
274        .ok_or_else(|| {
275            RError::new(
276                RErrorKind::Argument,
277                "new() requires a character string for the class name",
278            )
279        })?;
280
281    context.with_interpreter(|interp| {
282        let registry = interp.s4_classes.borrow();
283
284        if let Some(class_def) = registry.get(&class_name) {
285            // Check if virtual
286            if class_def.is_virtual {
287                return Err(RError::new(
288                    RErrorKind::Other,
289                    format!(
290                        "trying to generate an object from a virtual class (\"{}\")",
291                        class_name
292                    ),
293                ));
294            }
295
296            // Collect all valid slots (including inherited)
297            let all_slots = all_slots_for_class(&class_name, &registry);
298            let all_protos = all_prototypes_for_class(&class_name, &registry);
299            let slot_names: std::collections::HashSet<&str> =
300                all_slots.iter().map(|(n, _)| n.as_str()).collect();
301
302            // Validate that all named args are valid slot names
303            let mut errors = Vec::new();
304            for (name, _) in named {
305                if !slot_names.contains(name.as_str()) {
306                    errors.push(format!("\"{}\"", name));
307                }
308            }
309            if !errors.is_empty() {
310                return Err(RError::new(
311                    RErrorKind::Argument,
312                    format!(
313                        "invalid name{} for slot{} of class \"{}\": {}.\n  \
314                         Valid slots are: {}",
315                        if errors.len() > 1 { "s" } else { "" },
316                        if errors.len() > 1 { "s" } else { "" },
317                        class_name,
318                        errors.join(", "),
319                        all_slots
320                            .iter()
321                            .map(|(n, _)| format!("\"{}\"", n))
322                            .collect::<Vec<_>>()
323                            .join(", "),
324                    ),
325                ));
326            }
327
328            // Build the object: start with prototype defaults, then override
329            // with user-supplied values
330            let named_map: std::collections::HashMap<&str, &RValue> =
331                named.iter().map(|(n, v)| (n.as_str(), v)).collect();
332
333            let proto_map: std::collections::HashMap<&str, &RValue> =
334                all_protos.iter().map(|(n, v)| (n.as_str(), v)).collect();
335
336            let mut values: Vec<(Option<String>, RValue)> = Vec::new();
337            for (slot_name, _slot_type) in &all_slots {
338                let val = if let Some(v) = named_map.get(slot_name.as_str()) {
339                    (*v).clone()
340                } else if let Some(v) = proto_map.get(slot_name.as_str()) {
341                    (*v).clone()
342                } else {
343                    // No prototype, no user value — use NULL
344                    RValue::Null
345                };
346                values.push((Some(slot_name.clone()), val));
347            }
348
349            // Build class vector: the class itself + all ancestors
350            let chain = inheritance_chain(&class_name, &registry);
351            let class_vec: Vec<Option<String>> = chain.into_iter().map(Some).collect();
352
353            let mut list = RList::new(values);
354            list.set_attr(
355                "class".to_string(),
356                RValue::vec(Vector::Character(class_vec.into())),
357            );
358
359            // Run validity check if defined.
360            // R convention: validity returns TRUE if valid, or a character
361            // string describing the problem if invalid.
362            if let Some(ref validity_fn) = class_def.validity {
363                let obj = RValue::List(list.clone());
364                let result = interp
365                    .call_function(validity_fn, &[obj], &[], &interp.global_env)
366                    .map_err(RError::from)?;
367                if let Some(vec) = result.as_vector() {
368                    match vec {
369                        // Logical TRUE means valid — do nothing
370                        Vector::Logical(vals) if vals.first() == Some(&Some(true)) => {}
371                        // Character string means error message
372                        Vector::Character(vals) => {
373                            if let Some(Some(msg)) = vals.first() {
374                                return Err(RError::new(
375                                    RErrorKind::Other,
376                                    format!("invalid class \"{}\" object: {}", class_name, msg),
377                                ));
378                            }
379                        }
380                        _ => {}
381                    }
382                }
383            }
384
385            Ok(RValue::List(list))
386        } else {
387            // No registered class — fall back to simple list construction
388            let mut values: Vec<(Option<String>, RValue)> = Vec::new();
389            for arg in args.iter().skip(1) {
390                values.push((None, arg.clone()));
391            }
392            for (name, val) in named {
393                values.push((Some(name.clone()), val.clone()));
394            }
395
396            let mut list = RList::new(values);
397            list.set_attr(
398                "class".to_string(),
399                RValue::vec(Vector::Character(vec![Some(class_name)].into())),
400            );
401
402            Ok(RValue::List(list))
403        }
404    })
405}
406
407// endregion
408
409// region: is
410
411/// Check if an object is an instance of a class (S4-compatible).
412///
413/// Checks the class attribute and then walks the S4 inheritance chain
414/// from the class registry to determine if the object inherits from
415/// the specified class.
416///
417/// @param object any R object
418/// @param class2 character string naming the class to check
419/// @return TRUE if the object inherits from class2, FALSE otherwise
420#[interpreter_builtin(min_args = 1, namespace = "methods")]
421fn interp_is(
422    args: &[RValue],
423    _named: &[(String, RValue)],
424    context: &BuiltinContext,
425) -> Result<RValue, RError> {
426    let object = args
427        .first()
428        .ok_or_else(|| RError::new(RErrorKind::Argument, "is() requires at least one argument"))?;
429
430    let class2 = match args.get(1) {
431        Some(v) => v
432            .as_vector()
433            .and_then(|v| v.as_character_scalar())
434            .ok_or_else(|| {
435                RError::new(
436                    RErrorKind::Argument,
437                    "is() requires a character string for class2",
438                )
439            })?,
440        // With one argument, is() returns the class — match R behavior
441        None => {
442            let classes = get_class(object);
443            if classes.is_empty() {
444                return Ok(RValue::vec(Vector::Character(vec![None].into())));
445            }
446            return Ok(RValue::vec(Vector::Character(
447                classes.into_iter().map(Some).collect::<Vec<_>>().into(),
448            )));
449        }
450    };
451
452    let classes = get_class(object);
453
454    // Direct class match
455    if classes.iter().any(|c| c == &class2) {
456        return Ok(RValue::vec(Vector::Logical(vec![Some(true)].into())));
457    }
458
459    // Walk the inheritance chain from the S4 registry
460    let result = context.with_interpreter(|interp| {
461        let registry = interp.s4_classes.borrow();
462        for obj_class in &classes {
463            let chain = inheritance_chain(obj_class, &registry);
464            if chain.iter().any(|c| c == &class2) {
465                return true;
466            }
467        }
468        false
469    });
470
471    Ok(RValue::vec(Vector::Logical(vec![Some(result)].into())))
472}
473
474// endregion
475
476// region: setGeneric / setMethod
477
478/// Define an S4 generic function.
479///
480/// Registers the generic in the per-interpreter S4 generic registry and
481/// creates a dispatching function binding in the calling environment.
482///
483/// @param name character string naming the generic
484/// @param def default function definition
485/// @return the generic name (invisibly)
486#[interpreter_builtin(name = "setGeneric", min_args = 1, namespace = "methods")]
487fn interp_set_generic(
488    args: &[RValue],
489    named: &[(String, RValue)],
490    context: &BuiltinContext,
491) -> Result<RValue, RError> {
492    let name = args
493        .first()
494        .and_then(|v| v.as_vector()?.as_character_scalar())
495        .ok_or_else(|| {
496            RError::new(
497                RErrorKind::Argument,
498                "setGeneric() requires a character string for the generic name",
499            )
500        })?;
501
502    // Look for the definition in positional arg 2 or named "def"
503    let def = args.get(1).cloned().or_else(|| {
504        named
505            .iter()
506            .find(|(n, _)| n == "def")
507            .map(|(_, v)| v.clone())
508    });
509
510    let default_fn = def.filter(|v| matches!(v, RValue::Function(_)));
511
512    // Register the generic in the interpreter
513    context.with_interpreter(|interp| {
514        interp.s4_generics.borrow_mut().insert(
515            name.clone(),
516            S4GenericDef {
517                name: name.clone(),
518                default: default_fn.clone(),
519            },
520        );
521    });
522
523    // Create a dispatching function and bind it in the environment.
524    // If we have a default, bind that as the callable. The S4 dispatch
525    // logic in call_eval will check the method table before falling back.
526    if let Some(func) = default_fn {
527        context.env().set(name.clone(), func);
528    }
529
530    Ok(RValue::vec(Vector::Character(vec![Some(name)].into())))
531}
532
533/// Register an S4 method.
534///
535/// Stores the method in the per-interpreter S4 method dispatch table,
536/// keyed by (generic_name, signature). Falls back to binding the method
537/// under the generic name if no dispatch table entry can be created.
538///
539/// @param f character string naming the generic
540/// @param signature character vector or string specifying the method signature
541/// @param def function implementing the method
542/// @return the function name (invisibly)
543#[interpreter_builtin(name = "setMethod", min_args = 1, namespace = "methods")]
544fn interp_set_method(
545    args: &[RValue],
546    named: &[(String, RValue)],
547    context: &BuiltinContext,
548) -> Result<RValue, RError> {
549    let f = args
550        .first()
551        .and_then(|v| v.as_vector()?.as_character_scalar())
552        .ok_or_else(|| {
553            RError::new(
554                RErrorKind::Argument,
555                "setMethod() requires a character string for the function name",
556            )
557        })?;
558
559    // Extract signature from positional arg 2 or named "signature"
560    let sig_val = args.get(1).cloned().or_else(|| {
561        named
562            .iter()
563            .find(|(n, _)| n == "signature")
564            .map(|(_, v)| v.clone())
565    });
566
567    let signature: Vec<String> = sig_val
568        .as_ref()
569        .map(extract_character_vec)
570        .unwrap_or_default();
571
572    // Extract definition from positional arg 3 or named "def"
573    let def = args.get(2).cloned().or_else(|| {
574        named
575            .iter()
576            .find(|(n, _)| n == "def")
577            .map(|(_, v)| v.clone())
578    });
579
580    if let Some(func) = def.filter(|v| matches!(v, RValue::Function(_))) {
581        context.with_interpreter(|interp| {
582            // Store in the method dispatch table
583            interp
584                .s4_methods
585                .borrow_mut()
586                .insert((f.clone(), signature), func.clone());
587
588            // Also bind under the generic name if there is no existing binding,
589            // or if no generic was registered (backwards compat)
590            let generics = interp.s4_generics.borrow();
591            if !generics.contains_key(&f) {
592                drop(generics);
593                context.env().set(f.clone(), func);
594            }
595        });
596    }
597
598    Ok(RValue::vec(Vector::Character(vec![Some(f)].into())))
599}
600
601// endregion
602
603// region: isVirtualClass / validObject / setValidity / showClass / existsMethod
604
605/// Check if a class is virtual.
606///
607/// Looks up the class in the S4 registry and returns its virtual status.
608///
609/// @param Class character string naming the class
610/// @return TRUE if the class is virtual, FALSE otherwise
611#[interpreter_builtin(name = "isVirtualClass", min_args = 1, namespace = "methods")]
612fn interp_is_virtual_class(
613    args: &[RValue],
614    _named: &[(String, RValue)],
615    context: &BuiltinContext,
616) -> Result<RValue, RError> {
617    let class_name = args
618        .first()
619        .and_then(|v| v.as_vector()?.as_character_scalar())
620        .unwrap_or_default();
621
622    let is_virtual = context.with_interpreter(|interp| {
623        interp
624            .s4_classes
625            .borrow()
626            .get(&class_name)
627            .is_some_and(|def| def.is_virtual)
628    });
629
630    Ok(RValue::vec(Vector::Logical(vec![Some(is_virtual)].into())))
631}
632
633/// Validate an S4 object.
634///
635/// Runs the validity function registered for the object's class, if any.
636///
637/// @param object an S4 object
638/// @return the object if valid, error otherwise
639#[interpreter_builtin(name = "validObject", min_args = 1, namespace = "methods")]
640fn interp_valid_object(
641    args: &[RValue],
642    _named: &[(String, RValue)],
643    context: &BuiltinContext,
644) -> Result<RValue, RError> {
645    let object = args.first().cloned().ok_or_else(|| {
646        RError::new(
647            RErrorKind::Argument,
648            "validObject() requires an object argument",
649        )
650    })?;
651
652    let classes = get_class(&object);
653    if let Some(class_name) = classes.first() {
654        context.with_interpreter(|interp| {
655            let registry = interp.s4_classes.borrow();
656            if let Some(def) = registry.get(class_name) {
657                if let Some(ref validity_fn) = def.validity {
658                    let validity_fn = validity_fn.clone();
659                    drop(registry);
660                    let result = interp
661                        .call_function(
662                            &validity_fn,
663                            std::slice::from_ref(&object),
664                            &[],
665                            &interp.global_env,
666                        )
667                        .map_err(RError::from)?;
668                    if let Some(vec) = result.as_vector() {
669                        match vec {
670                            // Logical TRUE means valid
671                            Vector::Logical(vals) if vals.first() == Some(&Some(true)) => {}
672                            // Character string means error message
673                            Vector::Character(vals) => {
674                                if let Some(Some(msg)) = vals.first() {
675                                    return Err(RError::new(
676                                        RErrorKind::Other,
677                                        format!("invalid class \"{}\" object: {}", class_name, msg),
678                                    ));
679                                }
680                            }
681                            _ => {}
682                        }
683                    }
684                }
685            }
686            Ok(())
687        })?;
688    }
689
690    Ok(object)
691}
692
693/// Set a validity method for an S4 class.
694///
695/// Stores the validity function in the class registry entry.
696///
697/// @param Class character string naming the class
698/// @param method validity-checking function
699/// @return the class name
700#[interpreter_builtin(name = "setValidity", min_args = 1, namespace = "methods")]
701fn interp_set_validity(
702    args: &[RValue],
703    named: &[(String, RValue)],
704    context: &BuiltinContext,
705) -> Result<RValue, RError> {
706    let class_name = args
707        .first()
708        .and_then(|v| v.as_vector()?.as_character_scalar())
709        .ok_or_else(|| {
710            RError::new(
711                RErrorKind::Argument,
712                "setValidity() requires a character string for the class name",
713            )
714        })?;
715
716    let method = args.get(1).cloned().or_else(|| {
717        named
718            .iter()
719            .find(|(n, _)| n == "method")
720            .map(|(_, v)| v.clone())
721    });
722
723    if let Some(func) = method.filter(|v| matches!(v, RValue::Function(_))) {
724        context.with_interpreter(|interp| {
725            let mut registry = interp.s4_classes.borrow_mut();
726            if let Some(def) = registry.get_mut(&class_name) {
727                def.validity = Some(func);
728            }
729        });
730    }
731
732    Ok(RValue::vec(Vector::Character(
733        vec![Some(class_name)].into(),
734    )))
735}
736
737/// Display information about an S4 class.
738///
739/// Shows class details from the registry including slots and inheritance.
740///
741/// @param Class character string naming the class
742/// @return NULL, invisibly
743#[interpreter_builtin(name = "showClass", min_args = 1, namespace = "methods")]
744fn interp_show_class(
745    args: &[RValue],
746    _named: &[(String, RValue)],
747    context: &BuiltinContext,
748) -> Result<RValue, RError> {
749    let class_name = args
750        .first()
751        .and_then(|v| v.as_vector()?.as_character_scalar())
752        .ok_or_else(|| {
753            RError::new(
754                RErrorKind::Argument,
755                "showClass() requires a character string for the class name",
756            )
757        })?;
758
759    context.with_interpreter(|interp| {
760        let registry = interp.s4_classes.borrow();
761        if let Some(def) = registry.get(&class_name) {
762            interp.write_stderr(&format!("Class \"{}\"\n", class_name));
763            if !def.slots.is_empty() {
764                interp.write_stderr("Slots:\n");
765                for (name, typ) in &def.slots {
766                    interp.write_stderr(&format!("  Name: {}  Class: {}\n", name, typ));
767                }
768            }
769            if !def.contains.is_empty() {
770                interp.write_stderr(&format!("Extends: {}\n", def.contains.join(", ")));
771            }
772            if def.is_virtual {
773                interp.write_stderr("(virtual class)\n");
774            }
775        } else {
776            interp.write_stderr(&format!(
777                "Class \"{}\" (not registered in S4 class registry)\n",
778                class_name
779            ));
780        }
781    });
782
783    Ok(RValue::Null)
784}
785
786/// Check if a method exists for a given generic and signature.
787///
788/// Looks up the method in the S4 dispatch table.
789///
790/// @param f character string naming the generic function
791/// @param signature character string or vector for the method signature
792/// @return TRUE if a method exists, FALSE otherwise
793#[interpreter_builtin(name = "existsMethod", min_args = 1, namespace = "methods")]
794fn interp_exists_method(
795    args: &[RValue],
796    _named: &[(String, RValue)],
797    context: &BuiltinContext,
798) -> Result<RValue, RError> {
799    let f = args
800        .first()
801        .and_then(|v| v.as_vector()?.as_character_scalar())
802        .unwrap_or_default();
803
804    let sig_val = args.get(1);
805    let signature: Vec<String> = sig_val.map(extract_character_vec).unwrap_or_default();
806
807    let exists = context.with_interpreter(|interp| {
808        let methods = interp.s4_methods.borrow();
809        methods.contains_key(&(f, signature))
810    });
811
812    Ok(RValue::vec(Vector::Logical(vec![Some(exists)].into())))
813}
814
815// endregion
816
817// region: slot / slot<-
818
819/// Extract a slot from an S4 object.
820///
821/// Extracts a named element from the underlying list, equivalent to the
822/// `@` operator.
823///
824/// @param object an S4 object (list with class attribute)
825/// @param name character string naming the slot
826/// @return the slot value, or an error if the slot doesn't exist
827#[builtin(min_args = 2, namespace = "methods")]
828fn builtin_slot(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
829    let object = args
830        .first()
831        .ok_or_else(|| RError::new(RErrorKind::Argument, "slot() requires an object argument"))?;
832
833    let slot_name = args
834        .get(1)
835        .and_then(|v| v.as_vector()?.as_character_scalar())
836        .ok_or_else(|| {
837            RError::new(
838                RErrorKind::Argument,
839                "slot() requires a character string for the slot name",
840            )
841        })?;
842
843    match object {
844        RValue::List(list) => {
845            for (name, val) in &list.values {
846                if name.as_deref() == Some(&slot_name) {
847                    return Ok(val.clone());
848                }
849            }
850            Err(RError::new(
851                RErrorKind::Name,
852                format!(
853                    "no slot of name \"{}\" for this object of class \"{}\"",
854                    slot_name,
855                    get_class(object).first().unwrap_or(&"unknown".to_string())
856                ),
857            ))
858        }
859        _ => Err(RError::new(
860            RErrorKind::Type,
861            "slot() requires an S4 object (list with class attribute)",
862        )),
863    }
864}
865
866/// Set a slot on an S4 object (replacement function).
867///
868/// Sets a named element on the underlying list, equivalent to `@<-`.
869///
870/// @param object an S4 object (list with class attribute)
871/// @param name character string naming the slot
872/// @param value the new value for the slot
873/// @return the modified object
874#[builtin(name = "slot<-", min_args = 3, namespace = "methods")]
875fn builtin_slot_set(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
876    let object = args
877        .first()
878        .ok_or_else(|| RError::new(RErrorKind::Argument, "slot<-() requires an object argument"))?;
879
880    let slot_name = args
881        .get(1)
882        .and_then(|v| v.as_vector()?.as_character_scalar())
883        .ok_or_else(|| {
884            RError::new(
885                RErrorKind::Argument,
886                "slot<-() requires a character string for the slot name",
887            )
888        })?;
889
890    let value = args.get(2).cloned().unwrap_or(RValue::Null);
891
892    match object {
893        RValue::List(list) => {
894            let mut new_list = list.clone();
895            let mut found = false;
896            for entry in &mut new_list.values {
897                if entry.0.as_deref() == Some(&slot_name) {
898                    entry.1 = value.clone();
899                    found = true;
900                    break;
901                }
902            }
903            if !found {
904                new_list.values.push((Some(slot_name.to_string()), value));
905            }
906            Ok(RValue::List(new_list))
907        }
908        _ => Err(RError::new(
909            RErrorKind::Type,
910            "slot<-() requires an S4 object (list with class attribute)",
911        )),
912    }
913}
914
915// endregion
916
917// region: representation
918
919/// Create a named character vector describing S4 slot types.
920///
921/// This is used as the `representation` or `slots` argument to `setClass()`.
922/// Each named argument specifies a slot name and its type as a character string.
923///
924/// @param ... named arguments where names are slot names and values are type strings
925/// @return a named character vector
926#[builtin(min_args = 0, namespace = "methods")]
927fn builtin_representation(_args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
928    let names: Vec<Option<String>> = named.iter().map(|(n, _)| Some(n.clone())).collect();
929    let values: Vec<Option<String>> = named
930        .iter()
931        .map(|(_, v)| {
932            v.as_vector()
933                .and_then(|rv| rv.as_character_scalar())
934                .or_else(|| Some(format!("{}", v)))
935        })
936        .collect();
937
938    let mut rv = RVector::from(Vector::Character(values.into()));
939    rv.set_attr(
940        "names".to_string(),
941        RValue::vec(Vector::Character(names.into())),
942    );
943    Ok(RValue::Vector(rv))
944}
945
946// endregion