hyperon/metta/runner/
environment.rs

1
2use std::path::{Path, PathBuf};
3use std::io::{BufReader, Write};
4use std::fs;
5use std::sync::Arc;
6
7use hyperon_atom::{sym, ExpressionAtom};
8use crate::metta::GroundingSpace;
9use hyperon_space::DynSpace;
10
11#[cfg(feature = "pkg_mgmt")]
12use crate::metta::runner::pkg_mgmt::{ModuleCatalog, DirCatalog, LocalCatalog, FsModuleFormat, SingleFileModuleFmt, DirModuleFmt, git_catalog::*};
13
14use directories::ProjectDirs;
15
16/// Contains state and host platform interfaces shared by all MeTTa runners.  This includes config settings
17/// and logger
18///
19/// Generally there will be only one environment object needed, and it can be accessed by calling the [Self::common_env] method
20#[derive(Debug)]
21pub struct Environment {
22    config_dir: Option<PathBuf>,
23    caches_dir: Option<PathBuf>,
24    init_metta_path: Option<PathBuf>,
25    working_dir: Option<PathBuf>,
26    is_test: bool,
27    #[cfg(feature = "pkg_mgmt")]
28    catalogs: Vec<Box<dyn ModuleCatalog>>,
29    #[cfg(feature = "pkg_mgmt")]
30    pub(crate) fs_mod_formats: Arc<Vec<Box<dyn FsModuleFormat>>>,
31    /// The store for modules cached locally after loading from a specific location, for example, via git.
32    #[cfg(feature = "pkg_mgmt")]
33    pub(crate) specified_mods: Option<LocalCatalog>,
34}
35
36const DEFAULT_INIT_METTA: &[u8] = include_bytes!("init.default.metta");
37const DEFAULT_ENVIRONMENT_METTA: &[u8] = include_bytes!("environment.default.metta");
38
39static COMMON_ENV: std::sync::OnceLock<Arc<Environment>> = std::sync::OnceLock::new();
40
41impl Environment {
42
43    /// Returns a reference to the shared common Environment
44    pub fn common_env() -> &'static Self {
45        COMMON_ENV.get_or_init(|| Arc::new(EnvBuilder::new().build()))
46    }
47
48    /// Internal function to get a copy of the common Environment's Arc ptr
49    pub(crate) fn common_env_arc() -> Arc<Self> {
50        COMMON_ENV.get_or_init(|| Arc::new(EnvBuilder::new().build())).clone()
51    }
52
53    /// Returns the [Path] to the config dir, in an OS-specific location
54    pub fn config_dir(&self) -> Option<&Path> {
55        self.config_dir.as_deref()
56    }
57
58    /// Returns the [Path] to a directory where the MeTTa runner can put persistent caches
59    ///
60    /// NOTE: The default location of the `caches_dir` dir is within `cfg_dir`, but if may be
61    /// overridden with [`EnvBuilder::set_caches_dir`].
62    pub fn caches_dir(&self) -> Option<&Path> {
63        self.caches_dir.as_deref()
64    }
65
66    /// Returns the [Path] to the environment's working_dir
67    ///
68    /// NOTE: The Environment's working_dir is not the same as the process working directory, and
69    /// changing the process's working directory will not affect the environment
70    pub fn working_dir(&self) -> Option<&Path> {
71        self.working_dir.as_deref()
72    }
73
74    /// Returns the path to the init.metta file, that is run to initialize a MeTTa runner and customize the MeTTa environment
75    pub fn initialization_metta_file_path(&self) -> Option<&Path> {
76        self.init_metta_path.as_deref()
77    }
78
79    /// Returns the [ModuleCatalog]s from the Environment, in search priority order
80    #[cfg(feature = "pkg_mgmt")]
81    pub fn catalogs(&self) -> impl Iterator<Item=&'_ dyn ModuleCatalog> + '_ {
82        self.catalogs.iter().map(|catalog| &**catalog as &dyn ModuleCatalog)
83    }
84
85    /// Returns the [FsModuleFormat]s from the Environment, in priority order
86    #[cfg(feature = "pkg_mgmt")]
87    pub fn fs_mod_formats(&self) -> impl Iterator<Item=&'_ dyn FsModuleFormat> + '_ {
88        self.fs_mod_formats.iter().map(|fmt| &**fmt as &dyn FsModuleFormat)
89    }
90
91    /// Private "default" function
92    fn new() -> Self {
93        Self {
94            config_dir: None,
95            caches_dir: None,
96            init_metta_path: None,
97            working_dir: std::env::current_dir().ok(),
98            is_test: false,
99            #[cfg(feature = "pkg_mgmt")]
100            catalogs: vec![],
101            #[cfg(feature = "pkg_mgmt")]
102            fs_mod_formats: Arc::new(vec![]),
103            #[cfg(feature = "pkg_mgmt")]
104            specified_mods: None,
105        }
106    }
107}
108
109/// Used to customize the [Environment] configuration
110///
111/// NOTE: It is not necessary to use the EnvBuilder if the default environment is acceptable
112pub struct EnvBuilder {
113    env: Environment,
114    create_cfg_dir: bool,
115    caches_dir: Option<PathBuf>,
116    #[cfg(feature = "pkg_mgmt")]
117    proto_catalogs: Vec<ProtoCatalog>,
118    #[cfg(feature = "pkg_mgmt")]
119    fs_mod_formats: Vec<Box<dyn FsModuleFormat>>
120}
121
122/// Private type representing something that will become an entry in the "Environment::catalogs" Vec
123#[cfg(feature = "pkg_mgmt")]
124#[derive(Debug)]
125enum ProtoCatalog {
126    Path(PathBuf),
127    Other(Box<dyn ModuleCatalog>),
128}
129
130impl EnvBuilder {
131
132    /// Returns a new EnvBuilder, to set the parameters for the MeTTa Environment
133    ///
134    /// NOTE: Unless otherwise specified, the default working directory will be the current process
135    /// working dir (`cwd`)
136    ///
137    /// NOTE: Unless otherwise specified by calling either [Self::set_default_config_dir] or [Self::set_config_dir], the
138    ///   [Environment] will be configured using no configuration directory.
139    ///
140    /// Depending on the host OS, the config directory locations will be:
141    /// * Linux: ~/.config/metta/
142    /// * Windows: ~\AppData\Roaming\TrueAGI\metta\config\
143    /// * Mac: ~/Library/Application Support/io.TrueAGI.metta/
144    ///
145    /// TODO: Repeat this documentation somewhere more prominent, like the top-level README
146    pub fn new() -> Self {
147        Self {
148            env: Environment::new(),
149            caches_dir: None,
150            create_cfg_dir: false,
151            #[cfg(feature = "pkg_mgmt")]
152            proto_catalogs: vec![],
153            #[cfg(feature = "pkg_mgmt")]
154            fs_mod_formats: vec![],
155        }
156    }
157
158    /// A convenience function to construct an environment suitable for unit tests
159    ///
160    /// The `test_env` Environment will not load or create any files.  Additionally
161    /// this method will initialize the logger for the test environment
162    pub fn test_env() -> Self {
163        EnvBuilder::new().set_working_dir(None).set_is_test(true)
164    }
165
166    /// Sets (or unsets) the working_dir for the environment
167    pub fn set_working_dir(mut self, working_dir: Option<&Path>) -> Self {
168        self.env.working_dir = working_dir.map(|dir| dir.into());
169        self
170    }
171
172    /// Sets the `config_dir` that the environment will load
173    pub fn set_config_dir(mut self, config_dir: &Path) -> Self {
174        self.env.config_dir = Some(config_dir.into());
175        self.set_create_config_dir(true)
176    }
177
178    /// Sets the directory used for caching files, such as those fetched from remote catalogs
179    ///
180    /// This location will override the default location within the config dir.
181    pub fn set_caches_dir(mut self, caches_dir: &Path) -> Self {
182        self.caches_dir = Some(caches_dir.into());
183        self
184    }
185
186    /// Sets whether or not a config directory with default config files will be created, if no directory is found
187    ///
188    /// NOTE: If the config directory exists but some config files are missing, default files will *not* be created.
189    pub fn set_create_config_dir(mut self, should_create: bool) -> Self {
190        self.create_cfg_dir = should_create;
191        if self.env.config_dir.is_none() && should_create {
192            panic!("Fatal Error: call set_default_config_dir() or set_config_dir(<path>) before calling set_create_config_dir(true)");
193        }
194        self
195    }
196
197    /// Sets the `config_dir` to the default configuration directory path
198    pub fn set_default_config_dir(self) -> Self {
199        match ProjectDirs::from("io", "TrueAGI",  "metta") {
200            Some(proj_dirs) => self.set_config_dir(proj_dirs.config_dir()),
201            None => {
202                eprint!("Failed to initialize config with OS config directory!");
203                self
204            }
205        }
206    }
207
208    /// Sets the `is_test` flag for the environment, to specify whether the environment is a unit-test
209    ///
210    /// NOTE: This currently applies to the logger, but may affect other behaviors in the future.
211    ///     See [env_logger::is_test](https://docs.rs/env_logger/latest/env_logger/struct.Builder.html#method.is_test)
212    pub fn set_is_test(mut self, is_test: bool) -> Self {
213        self.env.is_test = is_test;
214        self
215    }
216
217    /// Adds additional search paths to search for MeTTa modules in the file system
218    ///
219    /// NOTE: include paths are a type of [ModuleCatalog], and the first catalog added will have the highest
220    /// search priority, with subsequently added catalogs being search in order.  The `working_dir` will
221    /// always be searched before any other catalogs.
222    #[cfg(feature = "pkg_mgmt")]
223    pub fn push_include_path<P: AsRef<Path>>(mut self, path: P) -> Self {
224        self.proto_catalogs.push(ProtoCatalog::Path(path.as_ref().into()));
225        self
226    }
227
228    /// Adds an additional [ModuleCatalog] search for MeTTa modules
229    ///
230    /// NOTE: The first catalog added will have the highest search priority, with subsequently added catalogs
231    /// being search in order.  The `working_dir` will always be searched before any other catalogs.
232    #[cfg(feature = "pkg_mgmt")]
233    pub fn push_module_catalog<C: ModuleCatalog + 'static>(mut self, catalog: C) -> Self {
234        self.proto_catalogs.push(ProtoCatalog::Other(Box::new(catalog)));
235        self
236    }
237
238    /// Adds a [FsModuleFormat] to identify and load modules stored on file-system media
239    ///
240    /// This is the mechanism used to detect and load modules from the file system in foreign formats.
241    /// For example a format specific to a host language such as Python
242    ///
243    /// NOTE: The first format added will have the highest search priority, with subsequently added formats
244    /// being tried in order.  Built-in formats [SingleFileModuleFmt] and [DirModuleFmt] will be tried last.
245    #[cfg(feature = "pkg_mgmt")]
246    pub fn push_fs_module_format<F: FsModuleFormat + 'static>(mut self, fmt: F) -> Self {
247        self.fs_mod_formats.push(Box::new(fmt));
248        self
249    }
250
251    /// Initializes the shared common Environment, accessible with [Environment::common_env]
252    ///
253    /// NOTE: This method will panic if the common Environment has already been initialized
254    pub fn init_common_env(self) {
255        self.try_init_common_env().expect("Fatal Error: Common Environment already initialized");
256    }
257
258    /// Initializes the shared common Environment.  Non-panicking version of [Self::init_common_env]
259    pub fn try_init_common_env(self) -> Result<(), &'static str> {
260        COMMON_ENV.set(Arc::new(self.build())).map_err(|_| "Common Environment already initialized")
261    }
262
263    /// Returns a newly created Environment from the builder configuration
264    ///
265    /// NOTE: Creating owned Environments is usually not necessary.  It is usually sufficient to use the [Environment::common_env] method.
266    pub(crate) fn build(self) -> Environment {
267        let mut env = self.env;
268        #[cfg(feature = "pkg_mgmt")]
269        let mut proto_catalogs = self.proto_catalogs;
270        #[cfg(feature = "pkg_mgmt")]
271        let mut fs_mod_formats = self.fs_mod_formats;
272
273        //Init the logger.  This will have no effect if the logger has already been initialized
274        let _ = env_logger::builder().is_test(env.is_test).try_init();
275
276        //If we have a working_dir, make sure it gets searched first by building catalogs for it
277        #[cfg(feature = "pkg_mgmt")]
278        if let Some(working_dir) = &env.working_dir {
279            proto_catalogs.insert(0, ProtoCatalog::Path(working_dir.into()));
280        }
281
282        if let Some(config_dir) = &env.config_dir {
283
284            let init_metta_path = config_dir.join("init.metta");
285
286            //Create the default config dir and its contents, if that part of our directive
287            if self.create_cfg_dir && !config_dir.exists() {
288
289                std::fs::create_dir_all(&config_dir).unwrap();
290
291                //Create the default init.metta file
292                let mut file = fs::OpenOptions::new()
293                    .create(true)
294                    .write(true)
295                    .open(&init_metta_path)
296                    .expect(&format!("Error creating default init file at {init_metta_path:?}"));
297                file.write_all(&DEFAULT_INIT_METTA).unwrap();
298            }
299
300            //If the config_dir in the Environment still doesn't exist (and we couldn't create it), then set it to None
301            if !config_dir.exists() {
302                env.config_dir = None;
303            }
304
305            // If an explicit caches_dir wasn't provided then set the caches dir within the config dir
306            env.caches_dir = match self.caches_dir {
307                Some(caches_dir) => Some(caches_dir),
308                None => env.config_dir.as_ref().map(|cfg_dir| cfg_dir.join("caches"))
309            };
310
311            if init_metta_path.exists() {
312                env.init_metta_path = Some(init_metta_path);
313            }
314        }
315
316        #[cfg(feature = "pkg_mgmt")]
317        {
318            //Append the built-in [FSModuleFormat]s, [SingleFileModuleFmt] and [DirModuleFmt]
319            fs_mod_formats.push(Box::new(SingleFileModuleFmt));
320            fs_mod_formats.push(Box::new(DirModuleFmt));
321
322            //Wrap the fs_mod_formats in an Arc, so it can be shared with the instances of DirCatalog
323            env.fs_mod_formats = Arc::new(fs_mod_formats);
324
325            //Convert each proto_catalog into a real ModuleCatalog
326            for proto in proto_catalogs.into_iter() {
327                match proto {
328                    ProtoCatalog::Path(path) => {
329                        //Make a DirCatalog for the directory
330                        env.catalogs.push(Box::new(DirCatalog::new(path, env.fs_mod_formats.clone())));
331                    }
332                    ProtoCatalog::Other(catalog) => {
333                        env.catalogs.push(catalog);
334                    }
335                }
336            }
337        }
338
339        if let Some(config_dir) = &env.config_dir {
340            let env_metta_path = config_dir.join("environment.metta");
341
342            //Create the default environment.metta file if it doesn't exist
343            if !env_metta_path.exists() {
344                let mut file = fs::OpenOptions::new()
345                    .create(true)
346                    .write(true)
347                    .open(&env_metta_path)
348                    .expect(&format!("Error creating default environment config file at {env_metta_path:?}"));
349                file.write_all(&DEFAULT_ENVIRONMENT_METTA).unwrap();
350            }
351
352            interpret_environment_metta(env_metta_path, &mut env).unwrap_or_else(|e| {
353                log::warn!("Error occurred interpreting environment.metta file: {e}");
354            });
355        }
356
357        #[cfg(feature = "pkg_mgmt")]
358        {
359            //If we have a caches dir to cache modules locally then register remote catalogs
360            if let Some(caches_dir) = &env.caches_dir {
361
362                //Setup the specified_mods managed catalog to hold mods fetched by explicit means
363                let mut specified_mods = LocalCatalog::new(caches_dir, "specified-mods").unwrap();
364                let git_mod_catalog = GitCatalog::new_without_source_repo(caches_dir, env.fs_mod_formats.clone(), "specified-mods").unwrap();
365                specified_mods.push_upstream_catalog(Box::new(git_mod_catalog));
366                env.specified_mods = Some(specified_mods);
367            }
368        }
369
370        env
371    }
372
373}
374
375/// Interprets the file at `env_metta_path`, and modifies settings in the Environment
376///
377/// NOTE: I wonder if users will get confused by the fact that the full set of runner
378/// features aren't available in the environment.metta file.  But there is a bootstrapping
379/// problem trying to using a runner here
380fn interpret_environment_metta<P: AsRef<Path>>(env_metta_path: P, env: &mut Environment) -> Result<(), String> {
381    let file = fs::File::open(env_metta_path).map_err(|e| e.to_string())?;
382    let buf_reader = BufReader::new(file);
383
384    let space = DynSpace::new(GroundingSpace::new());
385    let tokenizer = crate::metta::runner::Tokenizer::new();
386    let mut parser = crate::metta::runner::SExprParser::new(buf_reader);
387    while let Some(atom) = parser.parse(&tokenizer)? {
388        let atoms = crate::metta::runner::interpret(space.clone(), &atom)?;
389        let atom = if atoms.len() != 1 {
390            return Err(format!("Error in environment.metta. Atom must evaluate into a single deterministic result.  Found {atoms:?}"));
391        } else {
392            atoms.into_iter().next().unwrap()
393        };
394
395        //TODO-FUTURE: Use atom-serde here to cut down on boilerplate from interpreting these atoms
396        let expr = ExpressionAtom::try_from(atom)?;
397        match expr.children().get(0) {
398            Some(atom_0) if *atom_0 == sym!("#includePath") => {
399                #[cfg(feature = "pkg_mgmt")]
400                env.catalogs.push(include_path_from_cfg_atom(&expr, env)?);
401                #[cfg(not(feature = "pkg_mgmt"))]
402                {
403                    let _ = &env;
404                    log::warn!("#includePath in environment.metta not supported without pkg_mgmt feature");
405                }
406            },
407            Some(atom_0) if *atom_0 == sym!("#gitCatalog") => {
408                #[cfg(feature = "pkg_mgmt")]
409                env.catalogs.push(git_catalog_from_cfg_atom(&expr, env)?);
410                #[cfg(not(feature = "pkg_mgmt"))]
411                log::warn!("#gitCatalog in environment.metta not supported without pkg_mgmt feature");
412            },
413            _ => return Err(format!("Error in environment.metta. Unrecognized setting: {expr:?}"))
414        }
415    }
416    Ok(())
417}
418
419#[cfg(feature = "pkg_mgmt")]
420fn git_catalog_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Result<Box<dyn ModuleCatalog>, String> {
421
422    let mut catalog_name = None;
423    let mut catalog_url = None;
424    let mut refresh_time = None;
425
426    let mut atom_iter = atom.children().iter();
427    let _ = atom_iter.next();
428    for atom in atom_iter {
429        let expr = <&ExpressionAtom>::try_from(atom)?;
430        if expr.children().len() < 1 {
431            continue;
432        }
433        let key_atom = expr.children().get(0).unwrap();
434        let val_atom = match expr.children().get(1) {
435            Some(atom) => atom,
436            None => return Err(format!("Error in environment.metta. Key without value: {key_atom}"))
437        };
438
439        match key_atom {
440            _ if *key_atom == sym!("#name") => catalog_name = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
441            _ if *key_atom == sym!("#url") => catalog_url = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
442            _ if *key_atom == sym!("#refreshTime") => refresh_time = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
443            _ => return Err(format!("Error in environment.metta. Unknown key: {key_atom}"))
444        }
445    }
446
447    let caches_dir = env.caches_dir.as_ref().unwrap();
448    let catalog_name = catalog_name.ok_or_else(|| format!("Error in environment.metta. \"name\" property required for #gitCatalog"))?;
449    let catalog_url = catalog_url.ok_or_else(|| format!("Error in environment.metta. \"url\" property required for #gitCatalog"))?;
450    let refresh_time = refresh_time.ok_or_else(|| format!("Error in environment.metta. \"refreshTime\" property required for #gitCatalog"))?
451        .parse::<u64>().map_err(|e| format!("Error in environment.metta.  Error parsing \"refreshTime\": {e}"))?;
452
453    let catalog_name = hyperon_atom::gnd::str::strip_quotes(catalog_name);
454    let catalog_url = hyperon_atom::gnd::str::strip_quotes(catalog_url);
455    let mut managed_remote_catalog = LocalCatalog::new(caches_dir, catalog_name).unwrap();
456    let remote_catalog = GitCatalog::new(caches_dir, env.fs_mod_formats.clone(), catalog_name, catalog_url, refresh_time).unwrap();
457    managed_remote_catalog.push_upstream_catalog(Box::new(remote_catalog));
458    Ok(Box::new(managed_remote_catalog))
459}
460
461#[cfg(feature = "pkg_mgmt")]
462fn include_path_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Result<Box<dyn ModuleCatalog>, String> {
463
464    let mut atom_iter = atom.children().iter();
465    let _ = atom_iter.next();
466    let path_atom = match atom_iter.next() {
467        Some(atom) => atom,
468        None => return Err(format!("Error in environment.metta. #includePath missing path value"))
469    };
470    let path = <&hyperon_atom::SymbolAtom>::try_from(path_atom)?.name();
471    // At this stage stdlib is not loaded and thus path is parsed as a symbol not string. 
472    // This is why we should manuall stip quotes from the symbol's name.
473    let path = hyperon_atom::gnd::str::strip_quotes(path);
474
475    //TODO-FUTURE: In the future we may want to replace dyn-fmt with strfmt, and do something a
476    // little bit nicer than this
477    let path = match path.strip_prefix("{$cfgdir}/") {
478        Some(rel_path) => env.config_dir().unwrap().join(rel_path),
479        None => PathBuf::from(path)
480    };
481
482    if !path.exists() {
483        log::info!("Creating search directory for modules: \"{}\"", path.display());
484        std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
485    }
486
487    Ok(Box::new(DirCatalog::new(path, env.fs_mod_formats.clone())))
488}