diff --git a/c/src/space.rs b/c/src/space.rs index 87c60f8ae..442d06b9c 100644 --- a/c/src/space.rs +++ b/c/src/space.rs @@ -7,6 +7,7 @@ use hyperon::matcher::*; use crate::atom::*; use std::os::raw::*; +use std::borrow::Cow; // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // Space Client Interface @@ -238,14 +239,9 @@ pub extern "C" fn space_atom_count(space: *const space_t) -> isize { pub extern "C" fn space_iterate(space: *const space_t, callback: c_atom_callback_t, context: *mut c_void) -> bool { let dyn_space = unsafe{ &*space }.borrow(); - match dyn_space.borrow().atom_iter() { - Some(atom_iter) => { - for atom in atom_iter { - callback(atom.into(), context); - } - true - }, - None => false + match dyn_space.visit(&mut |atom: Cow| callback(atom.as_ref().into(), context)) { + Ok(()) => true, + Err(()) => false, } } @@ -736,46 +732,22 @@ impl Space for CSpace { None } } - fn atom_iter(&self) -> Option { - struct CSpaceIterator<'a>(&'a CSpace, *mut c_void); - impl<'a> Iterator for CSpaceIterator<'a> { - type Item = &'a Atom; - fn next(&mut self) -> Option<&'a Atom> { - let api = unsafe{ &*self.0.api }; - if let Some(next_atom) = api.next_atom { - let atom_ref = next_atom(&self.0.params, self.1); + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { + let api = unsafe{ &*self.api }; + match (api.new_atom_iterator_state, api.next_atom) { + (Some(new_atom_iterator_state), Some(next_atom)) => { + let ctx = new_atom_iterator_state(&self.params); + loop { + let atom_ref = next_atom(&self.params, ctx); if atom_ref.is_null() { - None + break; } else { - Some(atom_ref.into_ref()) + v.accept(Cow::Borrowed(atom_ref.into_ref())); } - } else { - panic!("next_atom function must be implemented if new_atom_iterator_state is implemented"); - } - } - } - impl Drop for CSpaceIterator<'_> { - fn drop(&mut self) { - let api = unsafe{ &*self.0.api }; - if let Some(free_atom_iterator_state) = api.free_atom_iterator_state { - free_atom_iterator_state(&self.0.params, self.1); - } else { - panic!("free_atom_iterator_state function must be implemented if new_atom_iterator_state is implemented"); } - } - } - - let api = unsafe{ &*self.api }; - if let Some(new_atom_iterator_state) = api.new_atom_iterator_state { - let ctx = new_atom_iterator_state(&self.params); - if ctx.is_null() { - None - } else { - let new_iter = CSpaceIterator(self, ctx); - Some(SpaceIter::new(new_iter)) - } - } else { - None + Ok(()) + }, + _ => Err(()), } } fn as_any(&self) -> Option<&dyn std::any::Any> { @@ -803,6 +775,7 @@ struct DefaultSpace<'a>(&'a CSpace); impl Space for DefaultSpace<'_> { fn common(&self) -> FlexRef { self.0.common() } fn query(&self, query: &Atom) -> BindingsSet { self.0.query(query) } + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { self.0.visit(v) } fn as_any(&self) -> Option<&dyn std::any::Any> { Some(self.0) } fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { None } } @@ -827,7 +800,7 @@ impl SpaceMut for CSpace { let from: atom_ref_t = from.into(); (api.replace)(&self.params, &from, to.into()) } - fn as_space(&self) -> &dyn Space { + fn as_space<'a>(&self) -> &(dyn Space + 'a) { self } } diff --git a/lib/src/metta/runner/modules/mod.rs b/lib/src/metta/runner/modules/mod.rs index a65dbc532..bc44e9ac4 100644 --- a/lib/src/metta/runner/modules/mod.rs +++ b/lib/src/metta/runner/modules/mod.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use crate::metta::*; use crate::metta::runner::*; +use crate::space::module::ModuleSpace; use regex::Regex; @@ -56,7 +57,7 @@ impl ModId { pub struct MettaMod { mod_path: String, resource_dir: Option, - space: DynSpace, + space: Rc>, tokenizer: Shared, imported_deps: Mutex>, loader: Option>, @@ -75,6 +76,7 @@ impl MettaMod { } } } + let space = Rc::new(RefCell::new(ModuleSpace::new(space))); let new_mod = Self { mod_path, @@ -86,8 +88,8 @@ impl MettaMod { }; // Load the base tokens for the module's new Tokenizer - register_runner_tokens(&mut *new_mod.tokenizer().borrow_mut(), new_mod.tokenizer().clone(), &new_mod.space, metta); - register_common_tokens(&mut *new_mod.tokenizer().borrow_mut(), new_mod.tokenizer().clone(), &new_mod.space, metta); + register_runner_tokens(&mut *new_mod.tokenizer().borrow_mut(), new_mod.tokenizer().clone(), &DynSpace::with_rc(new_mod.space.clone()), metta); + register_common_tokens(&mut *new_mod.tokenizer().borrow_mut(), new_mod.tokenizer().clone(), &DynSpace::with_rc(new_mod.space.clone()), metta); //Load the stdlib unless this module is no_std if !no_stdlib { @@ -192,7 +194,7 @@ impl MettaMod { let (dep_space, transitive_deps) = mod_ptr.stripped_space(); // Add a new Grounded Space atom to the &self space, so we can access the dependent module - self.insert_dep(mod_id, dep_space)?; + self.insert_dep(mod_id, DynSpace::with_rc(dep_space.clone()))?; // Add all the transitive deps from the dependency if let Some(transitive_deps) = transitive_deps { @@ -228,7 +230,7 @@ impl MettaMod { fn insert_dep(&self, mod_id: ModId, dep_space: DynSpace) -> Result<(), String> { let mut deps_table = self.imported_deps.lock().unwrap(); if !deps_table.contains_key(&mod_id) { - self.add_atom(Atom::gnd(dep_space.clone()), false).map_err(|a| a.to_string())?; + self.space.borrow_mut().add_dep(dep_space.clone()); deps_table.insert(mod_id, dep_space); } Ok(()) @@ -251,39 +253,9 @@ impl MettaMod { /// Private function that returns a deep copy of a module's space, with the module's dependency /// sub-spaces stripped out and returned separately - // - // HACK. This is a terrible design. It is a stop-gap to get around problems caused by transitive - // imports described above, but it has some serious downsides, To name a few: - // - It means we must copy every atom in the space for every import - // - It only works when the dep module's space is a GroundingSpace - fn stripped_space(&self) -> (DynSpace, Option>) { + fn stripped_space(&self) -> (Rc>, Option>) { let deps_table = self.imported_deps.lock().unwrap(); - if deps_table.len() == 0 { - (self.space.clone(), None) - } else { - if let Some(any_space) = self.space.borrow().as_any() { - if let Some(dep_g_space) = any_space.downcast_ref::() { - - // Do a deep-clone of the dep-space atom-by-atom, because `space.remove()` doesn't recognize - // two GroundedAtoms wrapping DynSpaces as being the same, even if the underlying space is - let mut new_space = GroundingSpace::new(); - new_space.set_name(self.path().to_string()); - for atom in dep_g_space.atom_iter().unwrap() { - if let Some(sub_space) = atom.as_gnd::() { - if !deps_table.values().any(|space| space == sub_space) { - new_space.add(atom.clone()); - } - } else { - new_space.add(atom.clone()); - } - } - - return (DynSpace::new(new_space), Some(deps_table.clone())); - } - } - log::warn!("import_all_from_dependency: Importing from module based on a non-GroundingSpace is currently unsupported"); - (self.space.clone(), None) - } + (self.space.clone(), Some(deps_table.clone())) } /// Returns the full path of a loaded module. For example: "top:parent_mod:this_mod" @@ -302,8 +274,8 @@ impl MettaMod { self.loader.as_ref().and_then(|loader| loader.pkg_info()) } - pub fn space(&self) -> &DynSpace { - &self.space + pub fn space(&self) -> DynSpace { + DynSpace::with_rc(self.space.clone()) } pub fn tokenizer(&self) -> &Shared { diff --git a/lib/src/metta/runner/stdlib/module.rs b/lib/src/metta/runner/stdlib/module.rs index 5b2bed8a2..817aded00 100644 --- a/lib/src/metta/runner/stdlib/module.rs +++ b/lib/src/metta/runner/stdlib/module.rs @@ -90,7 +90,7 @@ impl CustomExecute for ImportOp { } other_atom => { match &other_atom { - Atom::Grounded(_) if Atom::as_gnd::(other_atom) == Some(context.module().space()) => { + Atom::Grounded(_) if Atom::as_gnd::(other_atom) == Some(&context.module().space()) => { context.import_all_from_dependency(mod_id)?; }, _ => { diff --git a/lib/src/metta/runner/stdlib/space.rs b/lib/src/metta/runner/stdlib/space.rs index 0fe7058ea..2a0efc4d6 100644 --- a/lib/src/metta/runner/stdlib/space.rs +++ b/lib/src/metta/runner/stdlib/space.rs @@ -160,9 +160,10 @@ impl CustomExecute for GetAtomsOp { let arg_error = || ExecError::from("get-atoms expects one argument: space"); let space = args.get(0).ok_or_else(arg_error)?; let space = Atom::as_gnd::(space).ok_or("get-atoms expects a space as its argument")?; - space.borrow().as_space().atom_iter() - .map(|iter| iter.cloned().map(|a| make_variables_unique(a)).collect()) - .ok_or(ExecError::Runtime("Unsupported Operation. Can't traverse atoms in this space".to_string())) + let mut result = Vec::new(); + space.borrow().as_space().visit(&mut |atom: std::borrow::Cow| { + result.push(make_variables_unique(atom.into_owned())) + }).map_or(Err(ExecError::Runtime("Unsupported Operation. Can't traverse atoms in this space".to_string())), |_| Ok(result)) } } @@ -259,6 +260,13 @@ mod tests { assert_eq!(result[2], vec![Atom::gnd(stdlib_space)]); } + fn collect_atoms(space: &dyn Space) -> Vec { + let mut atoms = Vec::new(); + space.visit(&mut |atom: std::borrow::Cow| atoms.push(atom.into_owned())) + .expect("Space::visit is not implemented"); + atoms + } + #[test] fn remove_atom_op() { let space = DynSpace::new(metta_space(" @@ -269,7 +277,7 @@ mod tests { let res = RemoveAtomOp{}.execute(&mut vec![satom, expr!(("foo" "bar"))]).expect("No result returned"); // REM: can return Bool in future assert_eq!(res, vec![UNIT_ATOM]); - let space_atoms: Vec = space.borrow().as_space().atom_iter().unwrap().cloned().collect(); + let space_atoms = collect_atoms(space.borrow().as_space()); assert_eq_no_order!(space_atoms, vec![expr!(("bar" "foo"))]); } @@ -281,7 +289,7 @@ mod tests { ")); let satom = Atom::gnd(space.clone()); let res = GetAtomsOp{}.execute(&mut vec![satom]).expect("No result returned"); - let space_atoms: Vec = space.borrow().as_space().atom_iter().unwrap().cloned().collect(); + let space_atoms = collect_atoms(space.borrow().as_space()); assert_eq_no_order!(res, space_atoms); assert_eq_no_order!(res, vec![expr!(("foo" "bar")), expr!(("bar" "foo"))]); } @@ -291,7 +299,7 @@ mod tests { let res = NewSpaceOp{}.execute(&mut vec![]).expect("No result returned"); let space = res.get(0).expect("Result is empty"); let space = space.as_gnd::().expect("Result is not space"); - let space_atoms: Vec = space.borrow().as_space().atom_iter().unwrap().cloned().collect(); + let space_atoms = collect_atoms(space.borrow().as_space()); assert_eq_no_order!(space_atoms, Vec::::new()); } @@ -301,7 +309,7 @@ mod tests { let satom = Atom::gnd(space.clone()); let res = AddAtomOp{}.execute(&mut vec![satom, expr!(("foo" "bar"))]).expect("No result returned"); assert_eq!(res, vec![UNIT_ATOM]); - let space_atoms: Vec = space.borrow().as_space().atom_iter().unwrap().cloned().collect(); + let space_atoms = collect_atoms(space.borrow().as_space()); assert_eq_no_order!(space_atoms, vec![expr!(("foo" "bar"))]); } diff --git a/lib/src/space/grounding.rs b/lib/src/space/grounding.rs index 783a9bee9..5feba04ba 100644 --- a/lib/src/space/grounding.rs +++ b/lib/src/space/grounding.rs @@ -291,8 +291,8 @@ impl GroundingSpace { } /// Returns the iterator over content of the space. - pub fn iter(&self) -> SpaceIter { - SpaceIter::new(GroundingSpaceIter::new(self)) + fn iter(&self) -> GroundingSpaceIter<'_> { + GroundingSpaceIter::new(self) } /// Sets the name property for the `GroundingSpace` which can be useful for debugging @@ -316,8 +316,8 @@ impl Space for GroundingSpace { fn atom_count(&self) -> Option { Some(self.iter().count()) } - fn atom_iter(&self) -> Option { - Some(self.iter()) + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { + Ok(self.iter().for_each(|atom| v.accept(Cow::Borrowed(atom)))) } fn as_any(&self) -> Option<&dyn std::any::Any> { Some(self) @@ -337,7 +337,7 @@ impl SpaceMut for GroundingSpace { fn replace(&mut self, from: &Atom, to: Atom) -> bool { GroundingSpace::replace(self, from, to) } - fn as_space(&self) -> &dyn Space { + fn as_space<'a>(&self) -> &(dyn Space + 'a) { self } } diff --git a/lib/src/space/mod.rs b/lib/src/space/mod.rs index 0bd604ba1..2778ce892 100644 --- a/lib/src/space/mod.rs +++ b/lib/src/space/mod.rs @@ -2,10 +2,12 @@ //! This module is intended to keep different space implementations. pub mod grounding; +pub mod module; use std::fmt::Display; use std::rc::{Rc, Weak}; use std::cell::{RefCell, Ref, RefMut}; +use std::borrow::Cow; use crate::common::FlexRef; use crate::atom::*; @@ -87,25 +89,6 @@ impl From>> for SpaceObserverRef { } } -/// Space iterator. -pub struct SpaceIter<'a> { - iter: Box + 'a> -} - -impl<'a> SpaceIter<'a> { - pub fn new + 'a>(iter: I) -> Self { - Self{ iter: Box::new(iter) } - } -} - -impl<'a> Iterator for SpaceIter<'a> { - type Item = &'a Atom; - - fn next(&mut self) -> Option<&'a Atom> { - self.iter.next() - } -} - /// A common object that needs to be maintained by all objects implementing the Space trait #[derive(Default)] pub struct SpaceCommon { @@ -148,6 +131,19 @@ impl Clone for SpaceCommon { } } +/// An interface for visiting space atoms. +pub trait SpaceVisitor { + /// Method is called by [Space::visit] implementation for each atom from the atomspace. + fn accept(&mut self, atom: Cow); +} + +/// One can use closure instead of implementing [SpaceVisitor] interface manually. +impl SpaceVisitor for T where T: FnMut(Cow) { + fn accept(&mut self, atom: Cow) { + (*self)(atom) + } +} + /// Read-only space trait. pub trait Space: std::fmt::Debug + std::fmt::Display { /// Access the SpaceCommon object owned by the Space @@ -202,10 +198,13 @@ pub trait Space: std::fmt::Debug + std::fmt::Display { None } - /// Returns an iterator over every atom in the space, or None if that is not possible - fn atom_iter(&self) -> Option { - None - } + /// Visit each atom of the space and call [SpaceVisitor::accept] method. + /// This method is optional. Return `Err(())` if method is not implemented. + /// `Cow` is used to allow passing both references and values. First + /// is appropriate for collection based atomspace. Second one is more + /// usable if atomspace is a generator or when values cannot be extracted + /// easily and should be reconstructed instead. + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()>; /// Returns an `&dyn `[Any](std::any::Any) for spaces where this is possible fn as_any(&self) -> Option<&dyn std::any::Any>; @@ -275,13 +274,16 @@ pub trait SpaceMut: Space { /// Turn a &dyn SpaceMut into an &dyn Space. Obsolete when Trait Upcasting is stabilized. /// [Rust issue #65991](https://github.com/rust-lang/rust/issues/65991) Any month now. - fn as_space(&self) -> &dyn Space; + fn as_space<'a>(&self) -> &(dyn Space + 'a); } #[derive(Clone)] pub struct DynSpace(Rc>); impl DynSpace { + pub fn with_rc(space: Rc>) -> Self { + Self(space) + } pub fn new(space: T) -> Self { let shared = Rc::new(RefCell::new(space)); DynSpace(shared) @@ -320,7 +322,7 @@ impl SpaceMut for DynSpace { fn replace(&mut self, from: &Atom, to: Atom) -> bool { self.0.borrow_mut().replace(from, to) } - fn as_space(&self) -> &dyn Space { + fn as_space<'a>(&self) -> &(dyn Space + 'a) { self } } @@ -338,8 +340,8 @@ impl Space for DynSpace { fn atom_count(&self) -> Option { self.0.borrow().atom_count() } - fn atom_iter(&self) -> Option { - None + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { + self.0.borrow().visit(v) } fn as_any(&self) -> Option<&dyn std::any::Any> { None @@ -384,8 +386,8 @@ impl Space for &T { fn atom_count(&self) -> Option { T::atom_count(*self) } - fn atom_iter(&self) -> Option { - T::atom_iter(*self) + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { + T::visit(*self, v) } fn as_any(&self) -> Option<&dyn std::any::Any> { None diff --git a/lib/src/space/module.rs b/lib/src/space/module.rs new file mode 100644 index 000000000..bf657ad2e --- /dev/null +++ b/lib/src/space/module.rs @@ -0,0 +1,91 @@ +use super::*; + +use std::fmt::Debug; + +pub struct ModuleSpace { + main: Box, + deps: Vec, +} + +impl Display for ModuleSpace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.main, f) + } +} + +impl Debug for ModuleSpace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.main, f) + } +} + +impl ModuleSpace { + pub fn new(space: T) -> Self { + Self { main: Box::new(space), deps: Vec::new() } + } + + pub fn query(&self, query: &Atom) -> BindingsSet { + let mut results = self.main.query(query); + for dep in &self.deps { + if let Some(space) = dep.borrow().as_any() { + if let Some(space) = space.downcast_ref::() { + results.extend(space.query_no_deps(query)); + } else { + panic!("Only ModuleSpace is expected inside dependencies collection"); + } + } else { + panic!("Cannot get space as Any inside ModuleSpace dependencies: {}", dep); + } + } + results + } + + fn query_no_deps(&self, query: &Atom) -> BindingsSet { + self.main.query(query) + } + + pub fn add_dep(&mut self, space: DynSpace) { + self.deps.push(space) + } + + pub fn deps(&self) -> &Vec { + &self.deps + } +} + +impl Space for ModuleSpace { + fn common(&self) -> FlexRef { + self.main.common() + } + fn query(&self, query: &Atom) -> BindingsSet { + ModuleSpace::query(self, query) + } + fn atom_count(&self) -> Option { + self.main.atom_count() + } + fn visit(&self, v: &mut dyn SpaceVisitor) -> Result<(), ()> { + self.main.visit(v) + } + fn as_any(&self) -> Option<&dyn std::any::Any> { + Some(self) + } + fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + Some(self) + } +} + +impl SpaceMut for ModuleSpace { + fn add(&mut self, atom: Atom) { + self.main.add(atom) + } + fn remove(&mut self, atom: &Atom) -> bool { + self.main.remove(atom) + } + fn replace(&mut self, from: &Atom, to: Atom) -> bool { + self.main.replace(from, to) + } + fn as_space<'a>(&self) -> &(dyn Space + 'a) { + self + } +} + diff --git a/python/tests/scripts/f1_imports.metta b/python/tests/scripts/f1_imports.metta index b5c887f5d..e72bcf724 100644 --- a/python/tests/scripts/f1_imports.metta +++ b/python/tests/scripts/f1_imports.metta @@ -3,14 +3,16 @@ ; are separate modules, but in no python mode there is no Python stdlib ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -; Even at the very beginning of the script `(get-atoms &self)` -; returns two atoms. One is from the imported stdlib, and the other -; is corelib, which was a dependency of stdlib that has been promoted -; These atoms are both wrapped spaces, as is `&self` +; At the very beginning of the script `(get-atoms &self)` +; returns no atoms. In fact it imports one or two libraries: +; corelib is a necessary core library and stdlib if Python is enabled. +; But these libraries are considered to be a part of the space content. +; TODO: if get-deps is implemented we could check it returns corelib and stdlib ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -!(assertEqual +!(assertEqualToResult ((let $x (get-atoms &self) (get-type $x))) - (superpose (((get-type &self)) ((get-type &self))))) + ()) + ; stdlib is already loaded !(assertEqual @@ -36,16 +38,11 @@ (= (is-space $atom) (let* (($type (get-type $atom)) ($space (get-type &self))) (== $type $space))) -; It's first atom is a space +; It is not returned by get-atoms as well +; TODO: after implementing get-deps we can check new dependency is added !(assertEqual (let $x (collapse (get-atoms &m)) (contains $x is-space)) - True) - -; FIXME: It is the `stdlib` atom but equality check doesn't work -;!(import! &stdlib top:stdlib) -;!(assertEqual - ;(let $x (collapse (get-atoms &m)) (car-atom $x)) - ;&stdlib) + False) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -61,8 +58,6 @@ (g 3)) ; Importing the same space into `&self` should break nothing -; TODO? If stdlib space would be in `&m`, which should check that it is not -; there anymore since in should be removed after importing it to `&self` !(import! &self f1_moduleA) ; Now indirectly imported `g` works and `f` fully works @@ -75,10 +70,6 @@ ; - moduleA itself, which is the same as &m ; - moduleC imported by moduleA and removed from A after its import to &self -; Check whether atom is &m -(: is-m (-> Atom Bool)) -(= (is-m $atom) (== $atom &m)) - ; Assert that the &self space contains the same space as &m, which we imported from moduleA ; TODO: Comparing spaces like this doesn't work because the source module is the same, but a specialized ; clone of the dependent space without transitive-dependencies was created during the import process. @@ -86,18 +77,22 @@ ; spaces will work again. But, In my opinion comparing spaces is not a good way to check to see if a ; module has been loaded. I believe a better solution is accessor operations for loaded & imported modules ; +; Check whether atom is &m +;(: is-m (-> Atom Bool)) +;(= (is-m $atom) (== $atom &m)) ;!(assertEqual -; (let $a (collapse (get-atoms &self)) (contains $a is-m)) +; (let $a (collapse (get-deps &self)) (contains $a is-m)) ; True) ; Check that the &self space contains the corelib child space ; Note: corelib doesn't import any modules into itself, so no space copy is needed -!(import! &corelib top:corelib) -(: is-corelib (-> Atom Bool)) -(= (is-corelib $atom) (== $atom &corelib)) -!(assertEqual - (let $a (collapse (get-atoms &self)) (contains $a is-corelib)) - True) +; TODO: this should be possible after implementing get-deps +;!(import! &corelib top:corelib) +;(: is-corelib (-> Atom Bool)) +;(= (is-corelib $atom) (== $atom &corelib)) +;!(assertEqual + ;(let $a (collapse (get-deps &self)) (contains $a is-corelib)) + ;True) ; Let's check that `if` from stdlib is not duplicated and gives only one result !(assertEqual