diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 241026c73..cf5cf24a2 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,6 +14,7 @@ rand = "0.8.5" bitset = "0.1.2" dyn-fmt = "0.4.0" itertools = "0.13.0" +unescaper = "0.1.5" # pkg_mgmt deps xxhash-rust = {version="0.8.7", features=["xxh3"], optional=true } diff --git a/lib/src/metta/runner/environment.rs b/lib/src/metta/runner/environment.rs index e75e66653..a47bb2235 100644 --- a/lib/src/metta/runner/environment.rs +++ b/lib/src/metta/runner/environment.rs @@ -455,9 +455,6 @@ fn git_catalog_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Result let refresh_time = refresh_time.ok_or_else(|| format!("Error in environment.metta. \"refreshTime\" property required for #gitCatalog"))? .parse::().map_err(|e| format!("Error in environment.metta. Error parsing \"refreshTime\": {e}"))?; - let catalog_name = crate::metta::runner::str::strip_quotes(catalog_name); - let catalog_url = crate::metta::runner::str::strip_quotes(catalog_url); - let mut managed_remote_catalog = LocalCatalog::new(caches_dir, catalog_name).unwrap(); let remote_catalog = GitCatalog::new(caches_dir, env.fs_mod_formats.clone(), catalog_name, catalog_url, refresh_time).unwrap(); managed_remote_catalog.push_upstream_catalog(Box::new(remote_catalog)); @@ -474,7 +471,6 @@ fn include_path_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Resul None => return Err(format!("Error in environment.metta. #includePath missing path value")) }; let path = <&crate::SymbolAtom>::try_from(path_atom)?.name(); - let path = crate::metta::runner::str::strip_quotes(path); //TODO-FUTURE: In the future we may want to replace dyn-fmt with strfmt, and do something a // little bit nicer than this diff --git a/lib/src/metta/runner/stdlib/debug.rs b/lib/src/metta/runner/stdlib/debug.rs index 88cc3f1d3..934c0de01 100644 --- a/lib/src/metta/runner/stdlib/debug.rs +++ b/lib/src/metta/runner/stdlib/debug.rs @@ -5,9 +5,11 @@ use crate::space::*; use crate::common::collections::{VecDisplay, Equality, DefaultEquality}; use crate::common::assert::compare_vec_no_order; use crate::atom::matcher::atoms_are_equivalent; -use crate::metta::runner::stdlib::{grounded_op, atom_to_string, regex, interpret_no_error, unit_result}; +use crate::metta::runner::stdlib::{grounded_op, regex, interpret_no_error, unit_result}; use crate::metta::runner::bool::*; +use crate::metta::runner::str::atom_to_string; + use std::convert::TryInto; fn assert_results_equal(actual: &Vec, expected: &Vec) -> Result, ExecError> { diff --git a/lib/src/metta/runner/stdlib/mod.rs b/lib/src/metta/runner/stdlib/mod.rs index 8a38a631e..a309e759e 100644 --- a/lib/src/metta/runner/stdlib/mod.rs +++ b/lib/src/metta/runner/stdlib/mod.rs @@ -17,7 +17,6 @@ use crate::metta::*; use crate::metta::text::{Tokenizer, SExprParser}; use crate::common::shared::Shared; use crate::metta::runner::{Metta, RunContext, ModuleLoader}; -use super::str::*; use regex::Regex; @@ -47,18 +46,6 @@ pub(crate) fn regex(regex: &str) -> Regex { Regex::new(regex).unwrap() } -pub fn atom_to_string(atom: &Atom) -> String { - match atom { - Atom::Grounded(gnd) if gnd.type_() == ATOM_TYPE_STRING => { - let mut s = gnd.to_string(); - s.remove(0); - s.pop(); - s - }, - _ => atom.to_string(), - } -} - // TODO: remove hiding errors completely after making it possible passing // them to the user pub fn interpret_no_error(space: DynSpace, expr: &Atom) -> Result, String> { diff --git a/lib/src/metta/runner/stdlib/module.rs b/lib/src/metta/runner/stdlib/module.rs index 817aded00..a237b44cf 100644 --- a/lib/src/metta/runner/stdlib/module.rs +++ b/lib/src/metta/runner/stdlib/module.rs @@ -4,8 +4,8 @@ use crate::metta::*; use crate::metta::text::Tokenizer; use crate::common::shared::Shared; use crate::metta::runner::{Metta, RunContext, ResourceKey}; -use crate::metta::runner::str::*; use super::{grounded_op, regex, unit_result}; +use crate::metta::runner::str::expect_string_like_atom; use regex::Regex; @@ -68,20 +68,13 @@ impl CustomExecute for ImportOp { let arg_error = || ExecError::from("import! expects a destination &space and a module name argument"); let dest_arg = args.get(0).ok_or_else(arg_error)?; - let mod_name_atom = args.get(1).ok_or_else(arg_error)?; - - // TODO: replace Symbol by grounded String? - let mod_name = match mod_name_atom { - Atom::Symbol(mod_name) => mod_name.name(), - _ => return Err("import! expects a module name as the first argument".into()) - }; - let mod_name = strip_quotes(mod_name); + let mod_name = args.get(1).and_then(expect_string_like_atom).ok_or_else(arg_error)?; // Load the module into the runner, or get the ModId if it's already loaded //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute` let ctx_ref = self.context.lock().unwrap().last().unwrap().clone(); let mut context = ctx_ref.lock().unwrap(); - let mod_id = context.load_module(mod_name)?; + let mod_id = context.load_module(&mod_name)?; // Import the module, as per the behavior described above match dest_arg { @@ -136,19 +129,12 @@ impl Grounded for IncludeOp { impl CustomExecute for IncludeOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("include expects a module name argument"); - let mod_name_atom = args.get(0).ok_or_else(arg_error)?; - - // TODO: replace Symbol by grounded String? - let mod_name = match mod_name_atom { - Atom::Symbol(mod_name) => mod_name.name(), - _ => return Err(arg_error()) - }; - let mod_name = strip_quotes(mod_name); + let mod_name = args.get(0).and_then(expect_string_like_atom).ok_or_else(arg_error)?; //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute` let ctx_ref = self.context.lock().unwrap().last().unwrap().clone(); let mut context = ctx_ref.lock().unwrap(); - let program_buf = context.load_resource_from_module(mod_name, ResourceKey::MainMettaSrc)?; + let program_buf = context.load_resource_from_module(&mod_name, ResourceKey::MainMettaSrc)?; // Interpret the loaded MeTTa S-Expression text let program_text = String::from_utf8(program_buf) @@ -199,20 +185,13 @@ impl Grounded for ModSpaceOp { impl CustomExecute for ModSpaceOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = "mod-space! expects a module name argument"; - let mod_name_atom = args.get(0).ok_or_else(|| ExecError::from(arg_error))?; - - // TODO: replace Symbol by grounded String? - let mod_name = match mod_name_atom { - Atom::Symbol(mod_name) => mod_name.name(), - _ => {return Err(ExecError::from(arg_error))} - }; - let mod_name = strip_quotes(mod_name); + let mod_name = args.get(0).and_then(expect_string_like_atom).ok_or_else(|| ExecError::from(arg_error))?; // Load the module into the runner, or get the ModId if it's already loaded //TODO: Remove this hack to access the RunContext, when it's part of the arguments to `execute` let ctx_ref = self.context.lock().unwrap().last().unwrap().clone(); let mut context = ctx_ref.lock().unwrap(); - let mod_id = context.load_module(mod_name)?; + let mod_id = context.load_module(&mod_name)?; let space = Atom::gnd(context.metta().module_space(mod_id)); Ok(vec![space]) diff --git a/lib/src/metta/runner/stdlib/package.rs b/lib/src/metta/runner/stdlib/package.rs index b70a5f5e8..d4f0cc3bd 100644 --- a/lib/src/metta/runner/stdlib/package.rs +++ b/lib/src/metta/runner/stdlib/package.rs @@ -6,7 +6,7 @@ use crate::metta::runner::{Metta, RunContext, git_catalog::ModuleGitLocation, mod_name_from_url, pkg_mgmt::UpdateMode}; -use crate::metta::runner::str::*; +use crate::metta::runner::str::expect_string_like_atom; /// Provides a way to access [Metta::load_module_at_path] from within MeTTa code #[derive(Clone, Debug)] @@ -35,14 +35,8 @@ impl Grounded for RegisterModuleOp { impl CustomExecute for RegisterModuleOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = "register-module! expects a file system path; use quotes if needed"; - let path_arg_atom = args.get(0).ok_or_else(|| ExecError::from(arg_error))?; + let path = args.get(0).and_then(expect_string_like_atom).ok_or_else(|| ExecError::from(arg_error))?; - let path = match path_arg_atom { - Atom::Symbol(path_arg) => path_arg.name(), - Atom::Grounded(g) => g.downcast_ref::().ok_or_else(|| ExecError::from(arg_error))?.as_str(), - _ => return Err(arg_error.into()), - }; - let path = strip_quotes(path); let path = std::path::PathBuf::from(path); // Load the module from the path @@ -90,19 +84,12 @@ impl Grounded for GitModuleOp { impl CustomExecute for GitModuleOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = "git-module! expects a URL; use quotes if needed"; - let url_arg_atom = args.get(0).ok_or_else(|| ExecError::from(arg_error))?; + let url = args.get(0).and_then(expect_string_like_atom).ok_or_else(|| ExecError::from(arg_error))?; // TODO: When we figure out how to address varargs, it will be nice to take an optional branch name - let url = match url_arg_atom { - Atom::Symbol(url_arg) => url_arg.name(), - Atom::Grounded(g) => g.downcast_ref::().ok_or_else(|| ExecError::from(arg_error))?.as_str(), - _ => return Err(arg_error.into()), - }; - let url = strip_quotes(url); - // TODO: Depending on what we do with `register-module!`, we might want to let the // caller provide an optional mod_name here too, rather than extracting it from the url - let mod_name = match mod_name_from_url(url) { + let mod_name = match mod_name_from_url(&url) { Some(mod_name) => mod_name, None => return Err(ExecError::from("git-module! error extracting module name from URL")) }; diff --git a/lib/src/metta/runner/stdlib/string.rs b/lib/src/metta/runner/stdlib/string.rs index 5185caa02..9fe9e8745 100644 --- a/lib/src/metta/runner/stdlib/string.rs +++ b/lib/src/metta/runner/stdlib/string.rs @@ -2,7 +2,7 @@ use crate::*; use crate::metta::*; use crate::metta::text::Tokenizer; use crate::metta::runner::str::*; -use super::{grounded_op, atom_to_string, unit_result, regex}; +use super::{grounded_op, unit_result, regex}; use std::convert::TryInto; diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index 82ab90ca8..5ab197342 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -2,6 +2,7 @@ use crate::*; use crate::common::collections::ImmutableString; use crate::serial; use crate::atom::serial::ConvertingSerializer; +use unescaper; /// String type pub const ATOM_TYPE_STRING : Atom = sym!("String"); @@ -47,26 +48,10 @@ impl Grounded for Str { impl std::fmt::Display for Str { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "\"{}\"", self.0) + write!(f, "{:?}", self.0.as_str()) } } -/// A utility function to return the part of a string in between starting and ending quotes -pub fn strip_quotes(src: &str) -> &str { - if let Some(first) = src.chars().next() { - if first == '"' { - if let Some(last) = src.chars().last() { - if last == '"' { - if src.len() > 1 { - return &src[1..src.len()-1] - } - } - } - } - } - src -} - #[derive(Default)] struct StrSerializer { value: Option, @@ -84,3 +69,53 @@ impl serial::ConvertingSerializer for StrSerializer { self.value } } + +pub fn atom_to_string(atom: &Atom) -> String { + match atom { + Atom::Grounded(gnd) if gnd.type_() == ATOM_TYPE_STRING => + // TODO: get string from internal representation using + // serialization like we do for Number + unescape(&atom.to_string()).unwrap(), + _ => atom.to_string(), + } +} + +pub fn unescape(str: &str) -> unescaper::Result { + unescaper::unescape(str).map(|mut s| { + s.remove(0); + s.pop(); + s + }) +} + +pub(crate) fn expect_string_like_atom(atom: &Atom) -> Option { + match atom { + Atom::Symbol(_) | Atom::Grounded(_) => Some(atom_to_string(atom)), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn str_display_escape() { + let s = Str::from_str("\\ \" \' \n \r \t \x1b abc"); + assert_eq!(r#""\\ \" ' \n \r \t \u{1b} abc""#, s.to_string()); + } + + #[test] + fn str_unescape() { + let s = unescape(r#""\\ \" ' \n \r \t \u{1b} abc""#); + assert_eq!("\\ \" \' \n \r \t \x1b abc", s.unwrap()); + } + + #[test] + fn test_atom_to_string() { + let atom = Atom::gnd(Str::from_str("A\nB")); + assert_eq!("A\nB", atom_to_string(&atom)); + let atom = Atom::sym(r#""AB""#); + assert_eq!(r#""AB""#, atom_to_string(&atom)); + } +} diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 166aef264..dbc19caf2 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -279,7 +279,8 @@ def __repr__(self): # Overwrite Python default representation of a string to use # double quotes instead of single quotes. if isinstance(self.content, str): - return f'"{self.content}"' + newstr = repr(self.content)[1:-1].translate(str.maketrans({'"' : r'\"'})) + return f'"{newstr}"' # Use default representation for everything else return repr(self.content) if self.id is None else self.id diff --git a/python/hyperon/stdlib.py b/python/hyperon/stdlib.py index bd5124f71..2f3cabcea 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -48,6 +48,16 @@ def match_(self, atom): return [{"matched_pattern": S(pattern)}] return [] +def parseImpl(atom, run_context): + try: + s = atom.get_object().content + if type(s) != str: + raise IncorrectArgumentError() + return [SExprParser(repr(s)[1:-1]).parse(run_context.tokenizer())] + except Exception as e: + raise IncorrectArgumentError() + + @register_atoms(pass_metta=True) def text_ops(run_context): """Add text operators @@ -61,10 +71,9 @@ def text_ops(run_context): """ - reprAtom = OperationAtom('repr', lambda a: [ValueAtom(repr(a))], + reprAtom = OperationAtom('repr', lambda a: [ValueAtom(repr(a), 'String')], ['Atom', 'String'], unwrap=False) - parseAtom = OperationAtom('parse', lambda s: [SExprParser(str(s)[1:-1]).parse(run_context.tokenizer())], - ['String', 'Atom'], unwrap=False) + parseAtom = OperationAtom('parse', lambda s: parseImpl(s, run_context), ['String', 'Atom'], unwrap=False) stringToCharsAtom = OperationAtom('stringToChars', lambda s: [E(*[ValueAtom(Char(c)) for c in str(s)[1:-1]])], ['String', 'Atom'], unwrap=False) charsToStringAtom = OperationAtom('charsToString', lambda a: [ValueAtom("".join([str(c)[1:-1] for c in a.get_children()]))], diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index 166715a2a..1fa706fc8 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -369,7 +369,6 @@ def test_char_vs_string(self): self.assertEqualMettaRunnerResults(metta.run("!(get-type 'A')"), [[S('Char')]]) self.assertEqualMettaRunnerResults(metta.run('!(get-type "A")'), [[S('String')]]) - class SomeObject(): def __init__(self): diff --git a/python/tests/test_grounded_type.py b/python/tests/test_grounded_type.py index 8cf13110d..0e8b95f72 100644 --- a/python/tests/test_grounded_type.py +++ b/python/tests/test_grounded_type.py @@ -44,6 +44,10 @@ def test_higher_func(self): self.assertEqual(metta.run("!((curry_num plus 1) 2)"), metta.run("! 3")) + def test_string_repr(self): + metta = MeTTa(env_builder=Environment.test_env()) + self.assertEqual(metta.run('!(repr "A")')[0][0].get_object(), ValueObject("\"A\"")) + def test_meta_types(self): metta = MeTTa(env_builder=Environment.test_env()) ### Basic functional types diff --git a/python/tests/test_stdlib.py b/python/tests/test_stdlib.py index 6bad14ccc..ab2ff68ff 100644 --- a/python/tests/test_stdlib.py +++ b/python/tests/test_stdlib.py @@ -20,7 +20,7 @@ def test_text_ops(self): # [[(V("X"))]]) self.assertEqualMettaRunnerResults(metta.run('!(parse "\\"A\\"")'), - [[(ValueAtom("A"))]]) + [[(ValueAtom("A"))]]) #self.assertEqualMettaRunnerResults(metta.run('!(parse "(func (Cons $x (Cons $xs $xss))) ")'), # [[E(S("func"), E(S("Cons"), V("x"), E(S("Cons"), V("xs"), V("xss"))))]]) diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 7bbd573d7..87344da1d 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -38,7 +38,8 @@ pub mod metta_interface_mod { use pyo3::prelude::*; use pyo3::types::{PyTuple, PyString, PyBool, PyList, PyDict}; use hyperon::common::collections::VecDisplay; - use super::{strip_quotes, exec_state_prepare, exec_state_should_break}; + use super::{exec_state_prepare, exec_state_should_break}; + use hyperon::metta::runner::str::unescape; /// Load the hyperon module, and get the "__version__" attribute pub fn get_hyperonpy_version() -> Result { @@ -238,7 +239,10 @@ pub mod metta_interface_mod { } else { match result.downcast::() { Ok(result_list) => { - Some(result_list.iter().map(|atom| strip_quotes(&atom.to_string()).to_string()).collect()) + Some(result_list.into_iter() + // String atom is expected as a value + .map(|atom| unescape(&atom.to_string()).unwrap()) + .collect()) }, Err(_) => None } @@ -258,7 +262,8 @@ pub mod metta_interface_mod { Ok(if result.is_none() { None } else { - Some(strip_quotes(&result.to_string()).to_string()) + // String atom is expected as a value + Some(unescape(&result.to_string()).unwrap()) }) }).unwrap() } @@ -300,7 +305,6 @@ pub mod metta_interface_mod { } } } - } /// The "no python" path involves a reimplementation of all of the MeTTa interface points calling MeTTa @@ -320,7 +324,8 @@ pub mod metta_interface_mod { use hyperon::Atom; use hyperon::metta::runner::{Metta, RunnerState, Environment, EnvBuilder}; use hyperon::common::collections::VecDisplay; - use super::{strip_quotes, exec_state_prepare, exec_state_should_break}; + use super::{exec_state_prepare, exec_state_should_break}; + use hyperon::metta::runner::str::atom_to_string; pub use hyperon::metta::text::SyntaxNodeType as SyntaxNodeType; @@ -439,7 +444,7 @@ pub mod metta_interface_mod { pub fn get_config_string(&mut self, config_name: &str) -> Option { let atom = self.get_config_atom(config_name)?; //TODO: We need to do atom type checking here - Some(strip_quotes(&atom.to_string()).to_string()) + Some(atom_to_string(&atom)) } pub fn get_config_expr_vec(&mut self, config_name: &str) -> Option> { @@ -449,7 +454,7 @@ pub mod metta_interface_mod { .into_iter() .map(|atom| { //TODO: We need to do atom type checking here - strip_quotes(&atom.to_string()).to_string() + atom_to_string(&atom) }) .collect()) } else { @@ -461,20 +466,5 @@ pub mod metta_interface_mod { None //TODO. Make this work when I have reliable value atom bridging } } - } -pub fn strip_quotes(src: &str) -> &str { - if let Some(first) = src.chars().next() { - if first == '"' { - if let Some(last) = src.chars().last() { - if last == '"' { - if src.len() > 1 { - return &src[1..src.len()-1] - } - } - } - } - } - src -}