hyperon/metta/runner/stdlib/
module.rs

1use hyperon_atom::*;
2use hyperon_space::*;
3use crate::metta::*;
4use crate::metta::text::Tokenizer;
5use hyperon_common::shared::Shared;
6use crate::metta::runner::{Metta, RunContext, ResourceKey};
7use super::{grounded_op, regex, unit_result};
8use hyperon_atom::gnd::str::expect_string_like_atom;
9use hyperon_atom::gnd::GroundedFunctionAtom;
10use crate::space::module::ModuleSpace;
11
12use regex::Regex;
13
14#[derive(Clone, Debug)]
15pub struct ImportOp {
16    //TODO-HACK: This is a terrible horrible ugly hack that should be fixed ASAP
17    context: std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<std::sync::Mutex<&'static mut RunContext<'static, 'static>>>>>>,
18}
19
20grounded_op!(ImportOp, "import!");
21
22impl ImportOp {
23    pub fn new(metta: Metta) -> Self {
24        Self{ context: metta.0.context.clone() }
25    }
26}
27
28impl Grounded for ImportOp {
29    fn type_(&self) -> Atom {
30        //TODO: Ideally the "import as" / "import into" part would be optional
31        //A deeper discussion on arg semantics as it relates to import! is here:
32        // https://github.com/trueagi-io/hyperon-experimental/pull/580#discussion_r1491332304
33        Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, UNIT_TYPE])
34    }
35
36    fn as_execute(&self) -> Option<&dyn CustomExecute> {
37        Some(self)
38    }
39}
40
41impl CustomExecute for ImportOp {
42    fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
43        //QUESTION: "Import" can mean several (3) different things.  In Python parlance, it can mean
44        //1. "import module" opt. ("as bar")
45        //2. "from module import foo" opt. ("as bar")
46        //3. "from module import *"
47        //
48        //Do we want one MeTTa operation with multiple ways of invoking it?  Or do we want different
49        // implementations for different versions of the import operation?  (since we don't have key-words)
50        // like "from" and "as" (unless we want to add them)
51        //
52        //The old version of this operation supported 1. or 3., depending on whether a "space" argument
53        // mapped to an atom that already existed or not.  If the space atom existed and was a Space, then
54        // the operation would perform behavior 3 (by importing only the space atom and no token).
55        // Otherwise it would perform behavior 1, by adding a token, but not adding the child space atom to
56        // the parent space.
57        //
58        //For now, in order to not lose functionality, I have kept this behavior.
59        //
60        // ** TO SUMMARIZE **
61        // If the destination argument is the &self Space atom, the behavior is (3) ie "from module import *",
62        // and if the destination argument is a Symbol atom, the behavior is (1) ie "import module as foo"
63        //
64        //The Underlying functionality for behavior 2 exists in MettaMod::import_item_from_dependency_as,
65        //  but it isn't called yet because I wanted to discuss the way to expose it as a MeTTa op.
66        //For behavior 3, there are deeper questions about desired behavior around tokenizer entries,
67        //  transitive imports, etc.  I have summarized those concerns in the discussion comments above
68        //  MettaMod::import_all_from_dependency
69        //
70
71        let arg_error = || ExecError::from("import! expects a destination &space and a module name argument");
72        let dest_arg = args.get(0).ok_or_else(arg_error)?;
73        let mod_name = args.get(1).and_then(expect_string_like_atom).ok_or_else(arg_error)?;
74
75        // Load the module into the runner, or get the ModId if it's already loaded
76        //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute`
77        let ctx_ref = self.context.lock().unwrap().last().unwrap().clone();
78        let mut context = ctx_ref.lock().unwrap();
79        let mod_id = context.load_module(&mod_name)?;
80
81        // Import the module, as per the behavior described above
82        match dest_arg {
83            Atom::Symbol(dest_sym) => {
84                context.import_dependency_as(mod_id, Some(dest_sym.name().to_string()))?;
85            }
86            other_atom => {
87                match &other_atom {
88                    Atom::Grounded(_) if Atom::as_gnd::<DynSpace>(other_atom) == Some(&context.module().space()) => {
89                        context.import_all_from_dependency(mod_id)?;
90                    },
91                    _ => {
92                        return Err(format!("import! destination argument must be a symbol atom naming a new space, or &self.  Found: {other_atom:?}").into());
93                    }
94                }
95            }
96            // None => {
97            //     //TODO: Currently this pattern is unreachable on account of arity-checking in the MeTTa
98            //     // interpreter, but I have the code path in here for when it is possible
99            //     context.module().import_dependency_as(&context.metta, mod_id, None)?;
100            // },
101        }
102
103        unit_result()
104    }
105}
106
107#[derive(Clone, Debug)]
108pub struct IncludeOp {
109    //TODO-HACK: This is a terrible horrible ugly hack that should be fixed ASAP
110    context: std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<std::sync::Mutex<&'static mut RunContext<'static, 'static>>>>>>,
111}
112
113grounded_op!(IncludeOp, "include");
114
115impl IncludeOp {
116    pub fn new(metta: Metta) -> Self {
117        Self{ context: metta.0.context.clone() }
118    }
119}
120
121impl Grounded for IncludeOp {
122    fn type_(&self) -> Atom {
123        Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_UNDEFINED])
124    }
125
126    fn as_execute(&self) -> Option<&dyn CustomExecute> {
127        Some(self)
128    }
129}
130
131impl CustomExecute for IncludeOp {
132    fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
133        let arg_error = || ExecError::from("include expects a module name argument");
134        let mod_name = args.get(0).and_then(expect_string_like_atom).ok_or_else(arg_error)?;
135
136        //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute`
137        let ctx_ref = self.context.lock().unwrap().last().unwrap().clone();
138        let mut context = ctx_ref.lock().unwrap();
139        let resource = context.load_resource_from_module(&mod_name, ResourceKey::MainMettaSrc)?;
140        let parser = crate::metta::text::SExprParser::new(resource);
141        let eval_result = context.run_inline(|context| {
142            context.push_parser(Box::new(parser));
143            Ok(())
144        })?;
145
146        //NOTE: Current behavior returns the result of the last sub-eval to match the old
147        // `import!` before before module isolation.  However that means the results prior to
148        // the last are dropped.  I don't know how to fix this or if it's even wrong, but it's
149        // different from the way "eval-type" APIs work when called from host code, e.g. Rust
150        Ok(eval_result.into_iter().last().unwrap_or_else(|| vec![]))
151    }
152}
153
154/// mod-space! returns the space of a specified module, loading the module if it's not loaded already
155//NOTE: The "impure" '!' denoted in the op atom name is due to the side effect of loading the module.  If
156// we want a side-effect-free version, it could be implemented by calling `RunContext::get_module_by_name`
157// instead of `RunContext::load_module`, but then the user would need to use `register-module!`, `import!`,
158// or some other mechanism to make sure the module is loaded in advance.
159#[derive(Clone, Debug)]
160pub struct ModSpaceOp {
161    //TODO-HACK: This is a terrible horrible ugly hack that should be fixed ASAP
162    context: std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<std::sync::Mutex<&'static mut RunContext<'static, 'static>>>>>>,
163}
164
165grounded_op!(ModSpaceOp, "mod-space!");
166
167impl ModSpaceOp {
168    pub fn new(metta: Metta) -> Self {
169        Self{ context: metta.0.context.clone() }
170    }
171}
172
173impl Grounded for ModSpaceOp {
174    fn type_(&self) -> Atom {
175        Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_SPACE])
176    }
177
178    fn as_execute(&self) -> Option<&dyn CustomExecute> {
179        Some(self)
180    }
181}
182
183impl CustomExecute for ModSpaceOp {
184    fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
185        let arg_error = "mod-space! expects a module name argument";
186        let mod_name = args.get(0).and_then(expect_string_like_atom).ok_or_else(|| ExecError::from(arg_error))?;
187
188        // Load the module into the runner, or get the ModId if it's already loaded
189        //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute`
190        let ctx_ref = self.context.lock().unwrap().last().unwrap().clone();
191        let mut context = ctx_ref.lock().unwrap();
192        let mod_id = context.load_module(&mod_name)?;
193
194        let space = Atom::gnd(context.metta().module_space(mod_id));
195        Ok(vec![space])
196    }
197}
198
199fn module_space_no_deps(args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
200    let arg_error = "module-space-no-deps expects a space as an argument";
201    let space = args.get(0).ok_or(arg_error)?;
202    let space = Atom::as_gnd::<DynSpace>(space).ok_or(arg_error)?;
203
204    if let Some(space) = space.borrow().as_any().downcast_ref::<ModuleSpace>() {
205        return Ok(vec![Atom::gnd(space.main())]);
206    }
207    Ok(vec![Atom::gnd(space.clone())])
208}
209
210/// This operation prints the modules loaded from the top of the runner
211///
212/// NOTE: This is a temporary stop-gap to help MeTTa users inspect which modules they have loaded and
213/// debug module import issues.  Ultimately it probably makes sense to make this information accessible
214/// as a special kind of Space, so that it would be possible to work with it programmatically.
215#[derive(Clone, Debug)]
216pub struct PrintModsOp {
217    metta: Metta
218}
219
220grounded_op!(PrintModsOp, "print-mods!");
221
222impl PrintModsOp {
223    pub fn new(metta: Metta) -> Self {
224        Self{ metta }
225    }
226}
227
228impl Grounded for PrintModsOp {
229    fn type_(&self) -> Atom {
230        Atom::expr([ARROW_SYMBOL, UNIT_TYPE])
231    }
232
233    fn as_execute(&self) -> Option<&dyn CustomExecute> {
234        Some(self)
235    }
236}
237
238impl CustomExecute for PrintModsOp {
239    fn execute(&self, _args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
240        self.metta.display_loaded_modules();
241        unit_result()
242    }
243}
244
245#[derive(Clone, Debug)]
246pub struct BindOp {
247    tokenizer: Shared<Tokenizer>,
248}
249
250grounded_op!(BindOp, "bind!");
251
252impl BindOp {
253    pub fn new(tokenizer: Shared<Tokenizer>) -> Self {
254        Self{ tokenizer }
255    }
256}
257
258impl Grounded for BindOp {
259    fn type_(&self) -> Atom {
260        Atom::expr([ARROW_SYMBOL, ATOM_TYPE_SYMBOL, ATOM_TYPE_UNDEFINED, UNIT_TYPE])
261    }
262
263    fn as_execute(&self) -> Option<&dyn CustomExecute> {
264        Some(self)
265    }
266}
267
268impl CustomExecute for BindOp {
269    fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
270        let arg_error = || ExecError::from("bind! expects two arguments: token and atom");
271        let token = <&SymbolAtom>::try_from(args.get(0).ok_or_else(arg_error)?).map_err(|_| "bind! expects symbol atom as a token")?.name();
272        let atom = args.get(1).ok_or_else(arg_error)?.clone();
273
274        let token_regex = Regex::new(token).map_err(|err| format!("Could convert token {} into regex: {}", token, err))?;
275        self.tokenizer.borrow_mut().register_token(token_regex, move |_| { atom.clone() });
276        unit_result()
277    }
278}
279
280pub(super) fn register_context_independent_tokens(tref: &mut Tokenizer) {
281    tref.register_function(GroundedFunctionAtom::new(
282        r"module-space-no-deps".into(),
283        expr!("->" "SpaceType" "SpaceType"),
284        module_space_no_deps,
285    ));
286}
287
288pub(super) fn register_context_dependent_tokens(tref: &mut Tokenizer, tokenizer: Shared<Tokenizer>, metta: &Metta) {
289    let import_op = Atom::gnd(ImportOp::new(metta.clone()));
290    tref.register_token(regex(r"import!"), move |_| { import_op.clone() });
291    let include_op = Atom::gnd(IncludeOp::new(metta.clone()));
292    tref.register_token(regex(r"include"), move |_| { include_op.clone() });
293    let bind_op = Atom::gnd(BindOp::new(tokenizer.clone()));
294    tref.register_token(regex(r"bind!"), move |_| { bind_op.clone() });
295    let mod_space_op = Atom::gnd(ModSpaceOp::new(metta.clone()));
296    tref.register_token(regex(r"mod-space!"), move |_| { mod_space_op.clone() });
297    let print_mods_op = Atom::gnd(PrintModsOp::new(metta.clone()));
298    tref.register_token(regex(r"print-mods!"), move |_| { print_mods_op.clone() });
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn bind_new_space_op() {
307        let tokenizer = Shared::new(Tokenizer::new());
308
309        let bind_op = BindOp::new(tokenizer.clone());
310
311        assert_eq!(bind_op.execute(&mut vec![sym!("&my"), sym!("definition")]), unit_result());
312        let borrowed = tokenizer.borrow();
313        let constr = borrowed.find_token("&my");
314        assert!(constr.is_some());
315        assert_eq!(constr.unwrap()("&my"), Ok(sym!("definition")));
316    }
317}