Skip to main content

r/interpreter/packages/
loader.rs

1//! Package loading runtime — discovers, loads, and attaches R packages.
2//!
3//! Implements the core package loading sequence:
4//! 1. Find the package directory on `.libPaths()`
5//! 2. Parse DESCRIPTION and NAMESPACE
6//! 3. Create a namespace environment (parent = base env)
7//! 4. Source all `R/*.R` files into the namespace env
8//! 5. Build an exports environment (filtered view of namespace)
9//! 6. For `library()`, attach the exports env to the search path
10//! 7. Register S3 methods declared in NAMESPACE
11//! 8. Run `.onLoad()` and `.onAttach()` hooks
12
13use std::path::{Path, PathBuf};
14
15use tracing::{debug, trace};
16
17use crate::interpreter::environment::Environment;
18use crate::interpreter::value::{RError, RErrorKind, RValue, Vector};
19use crate::interpreter::Interpreter;
20
21use super::description::PackageDescription;
22use super::namespace::PackageNamespace;
23
24/// State for a single loaded package namespace.
25#[derive(Debug, Clone)]
26pub struct LoadedNamespace {
27    /// Package name.
28    pub name: String,
29    /// The directory the package was loaded from.
30    pub lib_path: PathBuf,
31    /// Parsed DESCRIPTION metadata.
32    pub description: PackageDescription,
33    /// Parsed NAMESPACE directives.
34    pub namespace: PackageNamespace,
35    /// The namespace environment (all package code lives here).
36    pub namespace_env: Environment,
37    /// The exports environment (user-visible subset attached to search path).
38    pub exports_env: Environment,
39}
40
41/// An entry on the search path. In R, the search path is:
42/// `.GlobalEnv` -> `package:foo` -> `package:bar` -> ... -> `package:base`
43#[derive(Debug, Clone)]
44pub struct SearchPathEntry {
45    /// Display name, e.g. "package:dplyr" or ".GlobalEnv".
46    pub name: String,
47    /// The environment on the search path.
48    pub env: Environment,
49}
50
51impl Interpreter {
52    /// Find a package directory by name, searching `.libPaths()`.
53    ///
54    /// Returns the path to the package directory (e.g. `/path/to/lib/myPkg/`)
55    /// if found, or None if the package is not installed in any library path.
56    pub(crate) fn find_package_dir(&self, pkg_name: &str) -> Option<PathBuf> {
57        let lib_paths = self.get_lib_paths();
58        for lib_path in &lib_paths {
59            let pkg_dir = Path::new(lib_path).join(pkg_name);
60            // A valid package directory must contain a DESCRIPTION file
61            if pkg_dir.join("DESCRIPTION").is_file() {
62                return Some(pkg_dir);
63            }
64        }
65        None
66    }
67
68    /// Get the library search paths (same logic as `.libPaths()` builtin).
69    ///
70    /// Builds the search path from (in order):
71    /// 1. `R_LIBS` environment variable (colon-separated on Unix, semicolon on Windows)
72    /// 2. `R_LIBS_USER` environment variable
73    /// 3. The default miniR library directory (`<data_dir>/miniR/library`)
74    pub(crate) fn get_lib_paths(&self) -> Vec<String> {
75        let mut paths: Vec<String> = Vec::new();
76        let sep = if cfg!(windows) { ';' } else { ':' };
77
78        if let Some(r_libs) = self.get_env_var("R_LIBS") {
79            for p in r_libs.split(sep) {
80                let p = p.trim();
81                if !p.is_empty() {
82                    let resolved = self.resolve_path(p);
83                    if resolved.is_dir() {
84                        paths.push(resolved.to_string_lossy().to_string());
85                    }
86                }
87            }
88        }
89
90        if let Some(r_libs_user) = self.get_env_var("R_LIBS_USER") {
91            for p in r_libs_user.split(sep) {
92                let p = p.trim();
93                if !p.is_empty() {
94                    let resolved = self.resolve_path(p);
95                    if resolved.is_dir() {
96                        paths.push(resolved.to_string_lossy().to_string());
97                    }
98                }
99            }
100        }
101
102        // Default miniR library directory — always included even if it doesn't
103        // exist yet, as it's the canonical install location. This matches the
104        // behavior of the `.libPaths()` builtin.
105        let default_lib = self.default_library_path();
106        if !paths.contains(&default_lib) {
107            paths.push(default_lib);
108        }
109
110        paths
111    }
112
113    /// Return the default miniR library directory path.
114    ///
115    /// Uses `dirs::data_dir()` when available (feature-gated), otherwise
116    /// falls back to `$HOME/.miniR/library`.
117    pub(crate) fn default_library_path(&self) -> String {
118        let data_dir = {
119            #[cfg(feature = "dirs-support")]
120            {
121                if let Some(data) = dirs::data_dir() {
122                    data.join("miniR").to_string_lossy().to_string()
123                } else {
124                    self.fallback_data_dir()
125                }
126            }
127            #[cfg(not(feature = "dirs-support"))]
128            {
129                self.fallback_data_dir()
130            }
131        };
132        format!("{}/library", data_dir)
133    }
134
135    /// Fallback data directory when `dirs` crate is not available.
136    fn fallback_data_dir(&self) -> String {
137        self.get_env_var("HOME")
138            .or_else(|| self.get_env_var("USERPROFILE"))
139            .map(|h| format!("{}/.miniR", h))
140            .unwrap_or_else(|| "/tmp/miniR".to_string())
141    }
142
143    /// Load a package namespace without attaching it to the search path.
144    ///
145    /// This is the core of `loadNamespace()`. It:
146    /// 1. Finds the package on `.libPaths()`
147    /// 2. Parses DESCRIPTION and NAMESPACE
148    /// 3. Creates a namespace environment
149    /// 4. Sources R files
150    /// 5. Builds exports
151    /// 6. Registers S3 methods
152    /// 7. Calls `.onLoad()`
153    ///
154    /// Returns the namespace environment.
155    /// R's base packages are built into miniR — they don't exist as installable
156    /// directories.  `library(base)`, `library(stats)`, etc. should be no-ops.
157    pub(crate) fn is_base_package(name: &str) -> bool {
158        matches!(
159            name,
160            "base"
161                | "stats"
162                | "stats4"
163                | "utils"
164                | "methods"
165                | "grDevices"
166                | "graphics"
167                | "datasets"
168                | "tools"
169                | "compiler"
170                | "grid"
171                | "splines"
172                | "parallel"
173                | "tcltk"
174                | "translations"
175        )
176    }
177
178    pub(crate) fn load_namespace(&self, pkg_name: &str) -> Result<Environment, RError> {
179        debug!(pkg = pkg_name, "loading namespace");
180
181        // Check if already loaded
182        if let Some(ns) = self.loaded_namespaces.borrow().get(pkg_name) {
183            debug!(pkg = pkg_name, "namespace already loaded");
184            return Ok(ns.namespace_env.clone());
185        }
186
187        // Base packages are built-in — register a synthetic namespace so that
188        // getNamespaceExports(), asNamespace(), etc. work correctly.
189        if Self::is_base_package(pkg_name) {
190            debug!(
191                pkg = pkg_name,
192                "base package — registering synthetic namespace"
193            );
194            let base = self.base_env().clone();
195            let ns = LoadedNamespace {
196                name: pkg_name.to_string(),
197                lib_path: PathBuf::from("<builtin>"),
198                description: PackageDescription {
199                    package: pkg_name.to_string(),
200                    version: "0.0.0".to_string(),
201                    title: Some(format!("miniR built-in: {}", pkg_name)),
202                    depends: vec![],
203                    imports: vec![],
204                    suggests: vec![],
205                    linking_to: vec![],
206                    fields: std::collections::HashMap::new(),
207                },
208                namespace: PackageNamespace::export_all(),
209                namespace_env: base.clone(),
210                exports_env: base.clone(),
211            };
212            self.loaded_namespaces
213                .borrow_mut()
214                .insert(pkg_name.to_string(), ns);
215            return Ok(base);
216        }
217
218        let pkg_dir = self.find_package_dir(pkg_name).ok_or_else(|| {
219            RError::new(
220                RErrorKind::Other,
221                format!(
222                    "there is no package called '{pkg_name}'\n  \
223                     Hint: check that the package is installed in one of the library paths \
224                     returned by .libPaths()"
225                ),
226            )
227        })?;
228
229        debug!(pkg = pkg_name, path = %pkg_dir.display(), "found package");
230        self.load_namespace_from_dir(pkg_name, &pkg_dir)
231    }
232
233    /// Load a namespace from a specific directory.
234    fn load_namespace_from_dir(
235        &self,
236        pkg_name: &str,
237        pkg_dir: &Path,
238    ) -> Result<Environment, RError> {
239        // Parse DESCRIPTION
240        let desc_path = pkg_dir.join("DESCRIPTION");
241        let desc_text = std::fs::read_to_string(&desc_path).map_err(|e| {
242            RError::other(format!(
243                "cannot read DESCRIPTION for package '{}': {}",
244                pkg_name, e
245            ))
246        })?;
247        let description = PackageDescription::parse(&desc_text).map_err(|e| {
248            RError::other(format!(
249                "cannot parse DESCRIPTION for package '{}': {}",
250                pkg_name, e
251            ))
252        })?;
253
254        // Parse NAMESPACE
255        let ns_path = pkg_dir.join("NAMESPACE");
256        let namespace = if ns_path.is_file() {
257            let ns_text = std::fs::read_to_string(&ns_path).map_err(|e| {
258                RError::other(format!(
259                    "cannot read NAMESPACE for package '{}': {}",
260                    pkg_name, e
261                ))
262            })?;
263            PackageNamespace::parse(&ns_text).map_err(|e| {
264                RError::other(format!(
265                    "cannot parse NAMESPACE for package '{}': {}",
266                    pkg_name, e
267                ))
268            })?
269        } else {
270            // Packages without NAMESPACE export everything (legacy behavior)
271            PackageNamespace::default()
272        };
273
274        // Load dependencies from Imports
275        for dep in &description.imports {
276            if dep.package == "R" || Self::is_base_package(&dep.package) {
277                continue;
278            }
279            // Silently skip unresolvable imports for now — they may be
280            // packages we can't load (native deps, etc.)
281            self.load_namespace(&dep.package)?;
282        }
283
284        // Load Depends (non-R) namespaces too
285        for dep in &description.depends {
286            if dep.package == "R" || Self::is_base_package(&dep.package) {
287                continue;
288            }
289            self.load_namespace(&dep.package)?;
290        }
291
292        // Create namespace environment with base env as parent.
293        // Register it immediately so re-entrant loadNamespace() calls
294        // (e.g. from import() directives in R files) return early instead
295        // of recursing infinitely.
296        let base_env = self.base_env();
297        let namespace_env = Environment::new_child(&base_env);
298        namespace_env.set_name(format!("namespace:{}", pkg_name));
299        // Pre-register so load_namespace() sees it as "already loaded".
300        // Use namespace_env as the exports env during loading — all symbols
301        // being sourced are immediately visible to re-entrant imports.
302        // The real filtered exports_env is built after sourcing completes.
303        self.loaded_namespaces.borrow_mut().insert(
304            pkg_name.to_string(),
305            LoadedNamespace {
306                name: pkg_name.to_string(),
307                lib_path: pkg_dir.to_path_buf(),
308                description: description.clone(),
309                namespace: namespace.clone(),
310                namespace_env: namespace_env.clone(),
311                exports_env: namespace_env.clone(), // temporary: all symbols visible
312            },
313        );
314
315        // Set .packageName — R packages reference this during loading
316        namespace_env.set(
317            ".packageName".to_string(),
318            RValue::vec(Vector::Character(vec![Some(pkg_name.to_string())].into())),
319        );
320
321        // Pre-create .__rlang_hook__. for packages using rlang's on_load() pattern.
322        namespace_env.set(
323            ".__rlang_hook__.".to_string(),
324            RValue::List(crate::interpreter::value::RList::new(vec![])),
325        );
326
327        // Populate imports into the namespace env
328        self.populate_imports(&namespace, &namespace_env)?;
329
330        // Initialize global environment SEXPs (R_BaseEnv, R_GlobalEnv) so C code
331        // that references them during init gets valid ENVSXP pointers.
332        #[cfg(feature = "native")]
333        {
334            let base = self.base_env();
335            crate::interpreter::native::runtime::init_global_envs(&base, &self.global_env);
336        }
337
338        // Load native code BEFORE sourcing R files — R code may reference
339        // native symbols (e.g. .Call(C_func, ...)) that need to be bound first.
340        #[cfg(feature = "native")]
341        if !namespace.use_dyn_libs.is_empty() {
342            debug!(pkg = pkg_name, "loading native code");
343            self.load_package_native_code(pkg_name, pkg_dir, &namespace.use_dyn_libs)?;
344
345            // Bind native symbol names into the namespace environment
346            for directive in &namespace.use_dyn_libs {
347                let fixes_prefix = directive
348                    .registrations
349                    .iter()
350                    .find(|r| r.contains(".fixes"))
351                    .and_then(|r| {
352                        r.split('=')
353                            .nth(1)
354                            .map(|v| v.trim().trim_matches('"').trim_matches('\'').to_string())
355                    })
356                    .unwrap_or_default();
357
358                let mut bound_any = false;
359                for reg in &directive.registrations {
360                    let sym_name = reg.trim();
361                    if sym_name.is_empty() {
362                        continue;
363                    }
364                    if sym_name.contains('=') && !sym_name.starts_with('.') {
365                        continue;
366                    }
367                    if sym_name.starts_with('.') {
368                        if reg.contains("registration") {
369                            let dlls = self.loaded_dlls.borrow();
370                            for dll in dlls.iter() {
371                                // Bind .Call registered methods
372                                for name in dll.registered_calls.keys() {
373                                    let r_name = format!("{fixes_prefix}{name}");
374                                    bind_native_symbol(&namespace_env, &r_name, name, pkg_name);
375                                }
376                                // Bind .C registered methods
377                                for name in dll.registered_c_methods.keys() {
378                                    let r_name = format!("{fixes_prefix}{name}");
379                                    bind_native_symbol(&namespace_env, &r_name, name, pkg_name);
380                                }
381                            }
382                            bound_any = true;
383                        }
384                        continue;
385                    }
386                    bind_native_symbol(&namespace_env, sym_name, sym_name, pkg_name);
387                    bound_any = true;
388                }
389                if !bound_any {
390                    let dlls = self.loaded_dlls.borrow();
391                    for dll in dlls.iter() {
392                        for name in dll.registered_calls.keys() {
393                            bind_native_symbol(&namespace_env, name, name, pkg_name);
394                        }
395                        for name in dll.registered_c_methods.keys() {
396                            bind_native_symbol(&namespace_env, name, name, pkg_name);
397                        }
398                    }
399                }
400            }
401            debug!(pkg = pkg_name, "native code loaded");
402        }
403
404        // Load R/sysdata.rda if present — internal package data that R functions
405        // reference at definition time (must be loaded before sourcing R files).
406        let r_dir = pkg_dir.join("R");
407        let sysdata_path = r_dir.join("sysdata.rda");
408        if sysdata_path.is_file() {
409            debug!(pkg = pkg_name, "loading sysdata.rda");
410            match std::fs::read(&sysdata_path) {
411                Ok(raw_bytes) => {
412                    match crate::interpreter::builtins::io::try_load_binary_rdata(
413                        &raw_bytes,
414                        &namespace_env,
415                    ) {
416                        Ok(Some(names)) => {
417                            debug!(pkg = pkg_name, count = names.len(), "sysdata.rda loaded");
418                        }
419                        Ok(None) => {
420                            tracing::warn!(
421                                pkg = pkg_name,
422                                "sysdata.rda: not a recognized binary format"
423                            );
424                        }
425                        Err(e) => {
426                            tracing::warn!(
427                                pkg = pkg_name,
428                                err = %e,
429                                "sysdata.rda: failed to load"
430                            );
431                        }
432                    }
433                }
434                Err(e) => {
435                    tracing::warn!(pkg = pkg_name, err = %e, "sysdata.rda: failed to read");
436                }
437            }
438        }
439
440        // Source all R files from the R/ directory, respecting Collate order
441        if r_dir.is_dir() {
442            debug!(pkg = pkg_name, "sourcing R files");
443            let collate = description.fields.get("Collate").map(|s| s.as_str());
444            self.source_r_directory(&r_dir, &namespace_env, collate)?;
445            debug!(pkg = pkg_name, "R files sourced");
446        }
447
448        // Build the real filtered exports environment
449        let exports_env = Environment::new_child(&base_env);
450        exports_env.set_name(format!("package:{}", pkg_name));
451        self.build_exports(&namespace, &namespace_env, &exports_env);
452
453        // Register S3 methods declared in NAMESPACE
454        self.register_s3_methods(&namespace, &namespace_env);
455
456        // Update the pre-registered entry with the real exports
457        if let Some(entry) = self.loaded_namespaces.borrow_mut().get_mut(pkg_name) {
458            entry.exports_env = exports_env;
459        }
460
461        debug!(pkg = pkg_name, "namespace loaded");
462
463        // Call .onLoad() if it exists
464        if let Some(on_load) = namespace_env.get(".onLoad") {
465            debug!(pkg = pkg_name, "calling .onLoad");
466            let lib_path_str = pkg_dir
467                .parent()
468                .unwrap_or(pkg_dir)
469                .to_string_lossy()
470                .to_string();
471            let lib_val = RValue::vec(Vector::Character(vec![Some(lib_path_str)].into()));
472            let pkg_val = RValue::vec(Vector::Character(vec![Some(pkg_name.to_string())].into()));
473            self.call_function(&on_load, &[lib_val, pkg_val], &[], &namespace_env)?;
474        }
475
476        Ok(namespace_env)
477    }
478
479    /// Get the base environment (root of the environment chain).
480    pub(crate) fn base_env(&self) -> Environment {
481        let mut current = self.global_env.clone();
482        while let Some(parent) = current.parent() {
483            current = parent;
484        }
485        current
486    }
487
488    /// Populate the namespace environment with imports from other packages.
489    fn populate_imports(
490        &self,
491        namespace: &PackageNamespace,
492        namespace_env: &Environment,
493    ) -> Result<(), RError> {
494        // Handle `import(pkg)` — import all exports from a package
495        for pkg_name in &namespace.imports {
496            if Self::is_base_package(pkg_name) {
497                // Base package bindings are already accessible through the parent chain
498                continue;
499            }
500            if let Some(ns) = self.loaded_namespaces.borrow().get(pkg_name) {
501                // Copy all exports into our namespace
502                for name in ns.exports_env.ls() {
503                    if let Some(val) = ns.exports_env.get(&name) {
504                        namespace_env.set(name, val);
505                    }
506                }
507            }
508        }
509
510        // Handle `importFrom(pkg, sym)` — import specific symbols
511        for (pkg_name, sym_name) in &namespace.imports_from {
512            if Self::is_base_package(pkg_name) {
513                // Try to get from base env
514                let base = self.base_env();
515                if let Some(val) = base.get(sym_name) {
516                    namespace_env.set(sym_name.clone(), val);
517                }
518                continue;
519            }
520            if let Some(ns) = self.loaded_namespaces.borrow().get(pkg_name) {
521                // Try exports first, then namespace
522                if let Some(val) = ns.exports_env.get(sym_name) {
523                    namespace_env.set(sym_name.clone(), val);
524                } else if let Some(val) = ns.namespace_env.get(sym_name) {
525                    namespace_env.set(sym_name.clone(), val);
526                }
527            }
528        }
529
530        Ok(())
531    }
532
533    /// Source all .R files from a directory into an environment.
534    ///
535    /// If `collate` is provided (from the DESCRIPTION `Collate` field), files
536    /// listed there are sourced first in that exact order. Any files in the
537    /// directory not mentioned in the Collate list are sourced afterwards in
538    /// alphabetical order (C locale). If `collate` is `None`, all R/S files
539    /// are sourced alphabetically (the default).
540    fn source_r_directory(
541        &self,
542        r_dir: &Path,
543        env: &Environment,
544        collate: Option<&str>,
545    ) -> Result<(), RError> {
546        // Collect all R/S files present in the directory
547        let mut all_files: Vec<PathBuf> = Vec::new();
548
549        let entries = std::fs::read_dir(r_dir).map_err(|e| {
550            RError::other(format!(
551                "cannot read R/ directory '{}': {}",
552                r_dir.display(),
553                e
554            ))
555        })?;
556
557        for entry in entries {
558            let entry = entry
559                .map_err(|e| RError::other(format!("error reading R/ directory entry: {}", e)))?;
560            let path = entry.path();
561            if let Some(ext) = path.extension() {
562                let ext_lower = ext.to_string_lossy().to_lowercase();
563                if ext_lower == "r" || ext_lower == "s" {
564                    all_files.push(path);
565                }
566            }
567        }
568
569        // Sort all files alphabetically for the fallback / remainder ordering
570        all_files.sort();
571
572        // Build the ordered file list based on Collate field
573        let r_files = if let Some(collate_str) = collate {
574            let collate_names = parse_collate_field(collate_str);
575            let mut ordered: Vec<PathBuf> = Vec::new();
576
577            // First: files listed in Collate, in that exact order
578            for name in &collate_names {
579                let path = r_dir.join(name);
580                if path.is_file() {
581                    ordered.push(path);
582                }
583                // If a Collate entry doesn't exist on disk, silently skip it
584                // (matches R CMD build behavior)
585            }
586
587            // Second: files in R/ not mentioned in Collate, alphabetically
588            let collate_set: std::collections::HashSet<&str> =
589                collate_names.iter().map(|s| s.as_str()).collect();
590            for path in &all_files {
591                if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
592                    if !collate_set.contains(file_name) {
593                        ordered.push(path.clone());
594                    }
595                }
596            }
597
598            ordered
599        } else {
600            all_files
601        };
602
603        debug!(count = r_files.len(), "sourcing R files");
604        for r_file in &r_files {
605            trace!(file = %r_file.display(), "sourcing R file");
606            if let Err(e) = self.source_file_into(r_file, env) {
607                // Warn but continue — some files may reference unavailable packages
608                tracing::warn!(file = %r_file.display(), error = %e, "error sourcing R file");
609            }
610        }
611
612        Ok(())
613    }
614
615    /// Source a single R file into an environment.
616    fn source_file_into(&self, path: &Path, env: &Environment) -> Result<(), RError> {
617        trace!(path = %path.display(), "source_file_into start");
618        let source = match std::fs::read_to_string(path) {
619            Ok(s) => s,
620            Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
621                let bytes = std::fs::read(path).map_err(|e2| {
622                    RError::other(format!("cannot read '{}': {}", path.display(), e2))
623                })?;
624                String::from_utf8_lossy(&bytes).into_owned()
625            }
626            Err(e) => {
627                return Err(RError::other(format!(
628                    "cannot read '{}': {}",
629                    path.display(),
630                    e
631                )));
632            }
633        };
634
635        let ast = crate::parser::parse_program(&source)
636            .map_err(|e| RError::other(format!("parse error in '{}': {}", path.display(), e)))?;
637
638        // Evaluate each top-level expression independently.
639        // If one expression errors (e.g. calling an undefined function at top level),
640        // continue with the remaining expressions so later definitions survive.
641        // This is critical for packages like sp where bpy.colors() fails at top level
642        // but .onLoad and other definitions in the same file must still be created.
643        use crate::parser::ast::Expr;
644        match &ast {
645            Expr::Program(exprs) => {
646                for expr in exprs {
647                    if let Err(e) = self.eval_in(expr, env) {
648                        tracing::trace!(
649                            path = %path.display(),
650                            error = %crate::interpreter::value::RError::from(e),
651                            "top-level expression error (continuing)"
652                        );
653                    }
654                }
655            }
656            other => {
657                self.eval_in(other, env).map_err(RError::from)?;
658            }
659        }
660        trace!(path = %path.display(), "source_file_into done");
661        Ok(())
662    }
663
664    /// Build the exports environment from namespace directives.
665    fn build_exports(
666        &self,
667        namespace: &PackageNamespace,
668        namespace_env: &Environment,
669        exports_env: &Environment,
670    ) {
671        // Handle explicit exports
672        for name in &namespace.exports {
673            if let Some(val) = namespace_env.get(name) {
674                exports_env.set(name.clone(), val);
675            }
676        }
677
678        // Handle exportPattern — match regex against all namespace bindings
679        let patterns: Vec<regex::Regex> = namespace
680            .export_patterns
681            .iter()
682            .filter_map(|pat| regex::Regex::new(pat).ok())
683            .collect();
684
685        if !patterns.is_empty() {
686            for name in namespace_env.ls() {
687                if patterns.iter().any(|pat| pat.is_match(&name)) {
688                    if let Some(val) = namespace_env.get(&name) {
689                        exports_env.set(name, val);
690                    }
691                }
692            }
693        }
694
695        // If no export directives at all, export everything (legacy packages
696        // without NAMESPACE or with empty NAMESPACE)
697        if namespace.exports.is_empty() && namespace.export_patterns.is_empty() {
698            for name in namespace_env.ls() {
699                if let Some(val) = namespace_env.get(&name) {
700                    exports_env.set(name, val);
701                }
702            }
703        }
704    }
705
706    /// Register S3 methods declared in NAMESPACE into the per-interpreter
707    /// S3 method registry so they're discoverable by S3 dispatch.
708    fn register_s3_methods(&self, namespace: &PackageNamespace, namespace_env: &Environment) {
709        for reg in &namespace.s3_methods {
710            let method_name = reg
711                .method
712                .clone()
713                .unwrap_or_else(|| format!("{}.{}", reg.generic, reg.class));
714
715            // Look up the method function in the namespace
716            if let Some(method_fn) = namespace_env.get(&method_name) {
717                self.register_s3_method(reg.generic.clone(), reg.class.clone(), method_fn);
718            }
719        }
720    }
721
722    /// Attach a loaded package to the search path.
723    ///
724    /// Inserts the package's exports environment right after `.GlobalEnv`
725    /// in the environment parent chain, and adds it to the search path list.
726    pub(crate) fn attach_package(&self, pkg_name: &str) -> Result<(), RError> {
727        // Base packages are built-in — their exports are already on the search path
728        if Self::is_base_package(pkg_name) {
729            return Ok(());
730        }
731
732        let loaded = self
733            .loaded_namespaces
734            .borrow()
735            .get(pkg_name)
736            .cloned()
737            .ok_or_else(|| {
738                RError::other(format!(
739                    "namespace '{}' is not loaded — cannot attach",
740                    pkg_name
741                ))
742            })?;
743
744        let entry_name = format!("package:{}", pkg_name);
745
746        // Check if already on search path
747        {
748            let sp = self.search_path.borrow();
749            if sp.iter().any(|e| e.name == entry_name) {
750                return Ok(());
751            }
752        }
753
754        // Insert between global env and its current parent.
755        // R's search path: global -> pkg1 -> pkg2 -> ... -> base
756        let current_parent = self.global_env.parent();
757        loaded.exports_env.set_parent(current_parent);
758        self.global_env.set_parent(Some(loaded.exports_env.clone()));
759
760        // Add to search path list
761        // Insert at front — newest package is searched first, matching the
762        // environment chain where we inserted between global and its old parent.
763        self.search_path.borrow_mut().insert(
764            0,
765            SearchPathEntry {
766                name: entry_name,
767                env: loaded.exports_env.clone(),
768            },
769        );
770
771        // Call .onAttach() if it exists
772        let namespace_env = loaded.namespace_env.clone();
773        let lib_path = loaded.lib_path.clone();
774        if let Some(on_attach) = namespace_env.get(".onAttach") {
775            let lib_path_str = lib_path
776                .parent()
777                .unwrap_or(&lib_path)
778                .to_string_lossy()
779                .to_string();
780            let lib_val = RValue::vec(Vector::Character(vec![Some(lib_path_str)].into()));
781            let pkg_val = RValue::vec(Vector::Character(vec![Some(pkg_name.to_string())].into()));
782            // Best-effort: ignore errors from .onAttach
783            self.call_function(&on_attach, &[lib_val, pkg_val], &[], &namespace_env)?;
784        }
785
786        Ok(())
787    }
788
789    /// Detach a package from the search path by name (e.g. "package:dplyr").
790    pub(crate) fn detach_package(&self, entry_name: &str) -> Result<(), RError> {
791        let mut sp = self.search_path.borrow_mut();
792        let idx = sp
793            .iter()
794            .position(|e| e.name == entry_name)
795            .ok_or_else(|| {
796                RError::new(
797                    RErrorKind::Argument,
798                    format!(
799                        "invalid 'name' argument: '{}' not found on search path",
800                        entry_name
801                    ),
802                )
803            })?;
804
805        let entry = sp.remove(idx);
806
807        // Rewire the environment parent chain: find who points to this env
808        // and make them point to this env's parent instead.
809        let detached_parent = entry.env.parent();
810
811        // Walk from global env to find the env whose parent is entry.env
812        let mut current = self.global_env.clone();
813        loop {
814            let parent = current.parent();
815            match parent {
816                Some(ref p) if p.ptr_eq(&entry.env) => {
817                    current.set_parent(detached_parent);
818                    break;
819                }
820                Some(p) => current = p,
821                None => break,
822            }
823        }
824
825        Ok(())
826    }
827
828    /// Get the search path as a vector of names.
829    pub(crate) fn get_search_path(&self) -> Vec<String> {
830        let mut result = vec![".GlobalEnv".to_string()];
831        for entry in self.search_path.borrow().iter() {
832            result.push(entry.name.clone());
833        }
834        result.push("package:base".to_string());
835        result
836    }
837}
838
839/// Parse the `Collate` field from a DESCRIPTION file into an ordered list of
840/// filenames.
841///
842/// The Collate field is a whitespace-separated list of filenames (possibly
843/// spanning multiple continuation lines). Filenames may be quoted with single
844/// or double quotes (required when they contain spaces).
845fn parse_collate_field(collate: &str) -> Vec<String> {
846    let mut names = Vec::new();
847    let mut chars = collate.chars().peekable();
848
849    while let Some(&ch) = chars.peek() {
850        // Skip whitespace (including newlines from DCF continuation)
851        if ch.is_whitespace() {
852            chars.next();
853            continue;
854        }
855
856        // Quoted filename
857        if ch == '\'' || ch == '"' {
858            let quote = ch;
859            chars.next(); // consume opening quote
860            let mut name = String::new();
861            for c in chars.by_ref() {
862                if c == quote {
863                    break;
864                }
865                name.push(c);
866            }
867            if !name.is_empty() {
868                names.push(name);
869            }
870            continue;
871        }
872
873        // Unquoted filename — runs until whitespace
874        let mut name = String::new();
875        while let Some(&c) = chars.peek() {
876            if c.is_whitespace() {
877                break;
878            }
879            name.push(c);
880            chars.next();
881        }
882        if !name.is_empty() {
883            names.push(name);
884        }
885    }
886
887    names
888}
889
890/// Create a NativeSymbolInfo-like binding in a namespace environment.
891#[cfg(feature = "native")]
892fn bind_native_symbol(
893    env: &crate::interpreter::environment::Environment,
894    r_name: &str,
895    c_name: &str,
896    pkg_name: &str,
897) {
898    let info = RValue::List(crate::interpreter::value::RList::new(vec![
899        (
900            Some("name".to_string()),
901            RValue::vec(Vector::Character(vec![Some(c_name.to_string())].into())),
902        ),
903        (
904            Some("package".to_string()),
905            RValue::vec(Vector::Character(vec![Some(pkg_name.to_string())].into())),
906        ),
907    ]));
908    env.set(r_name.to_string(), info);
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    #[test]
916    fn parse_collate_simple() {
917        let collate = "aaa.R bbb.R ccc.R";
918        let result = parse_collate_field(collate);
919        assert_eq!(result, vec!["aaa.R", "bbb.R", "ccc.R"]);
920    }
921
922    #[test]
923    fn parse_collate_multiline() {
924        // DCF continuation joins with newlines
925        let collate = "aaa.R bbb.R\nccc.R\nddd.R";
926        let result = parse_collate_field(collate);
927        assert_eq!(result, vec!["aaa.R", "bbb.R", "ccc.R", "ddd.R"]);
928    }
929
930    #[test]
931    fn parse_collate_quoted_filenames() {
932        let collate = r#"'aaa.R' "bbb.R" ccc.R"#;
933        let result = parse_collate_field(collate);
934        assert_eq!(result, vec!["aaa.R", "bbb.R", "ccc.R"]);
935    }
936
937    #[test]
938    fn parse_collate_quoted_with_spaces() {
939        let collate = r#"'file with spaces.R' normal.R"#;
940        let result = parse_collate_field(collate);
941        assert_eq!(result, vec!["file with spaces.R", "normal.R"]);
942    }
943
944    #[test]
945    fn parse_collate_extra_whitespace() {
946        let collate = "  aaa.R   bbb.R  \n  ccc.R  ";
947        let result = parse_collate_field(collate);
948        assert_eq!(result, vec!["aaa.R", "bbb.R", "ccc.R"]);
949    }
950
951    #[test]
952    fn parse_collate_empty() {
953        let result = parse_collate_field("");
954        assert!(result.is_empty());
955    }
956
957    #[test]
958    fn parse_collate_whitespace_only() {
959        let result = parse_collate_field("   \n  \n  ");
960        assert!(result.is_empty());
961    }
962}