From 5f505533d816bc474d3224843f552e7f8c928329 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Sat, 28 Dec 2024 08:19:21 +0300 Subject: [PATCH 01/10] python's repr fixed --- python/hyperon/atoms.py | 3 ++- python/hyperon/stdlib.py | 2 +- python/tests/test_examples.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 166aef264..65053e138 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 = self.content.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..da6410708 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -61,7 +61,7 @@ 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) diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index 166715a2a..ca5f993ee 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -368,6 +368,7 @@ def test_char_vs_string(self): self.assertEqual(repr(metta.run('!("A")')), '[[("A")]]') self.assertEqualMettaRunnerResults(metta.run("!(get-type 'A')"), [[S('Char')]]) self.assertEqualMettaRunnerResults(metta.run('!(get-type "A")'), [[S('String')]]) + self.assertEqual(metta.run('!(repr "A")')[0][0].get_object(), ValueObject("\"A\"")) class SomeObject(): From 64933ea38126ea0f7057e58bbc992af65c4e1c62 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Sat, 28 Dec 2024 11:03:48 +0300 Subject: [PATCH 02/10] fixes after Vitaly's review --- python/tests/test_examples.py | 2 -- python/tests/test_grounded_type.py | 4 ++++ python/tests/test_stdlib.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index ca5f993ee..1fa706fc8 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -368,8 +368,6 @@ def test_char_vs_string(self): self.assertEqual(repr(metta.run('!("A")')), '[[("A")]]') self.assertEqualMettaRunnerResults(metta.run("!(get-type 'A')"), [[S('Char')]]) self.assertEqualMettaRunnerResults(metta.run('!(get-type "A")'), [[S('String')]]) - self.assertEqual(metta.run('!(repr "A")')[0][0].get_object(), ValueObject("\"A\"")) - class SomeObject(): 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..3afa9f7ef 100644 --- a/python/tests/test_stdlib.py +++ b/python/tests/test_stdlib.py @@ -19,8 +19,8 @@ def test_text_ops(self): #self.assertEqualMettaRunnerResults(metta.run('!(parse "$X")'), # [[(V("X"))]]) - self.assertEqualMettaRunnerResults(metta.run('!(parse "\\"A\\"")'), - [[(ValueAtom("A"))]]) + self.assertEqualMettaRunnerResults(repr(metta.run('!(parse "\\"A\\"")')), + '[[\\"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"))))]]) From fadf15f8bd20f4657081e52bea1e7edd95d88694 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Sat, 28 Dec 2024 12:11:04 +0300 Subject: [PATCH 03/10] Escape string atom before displaying it Use snailquote::unescape to fix REPL output when manipulating strings represented as atoms. --- lib/src/metta/runner/str.rs | 2 +- repl/Cargo.toml | 1 + repl/src/metta_shim.rs | 32 ++++++++++++-------------------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index 82ab90ca8..a97c16ea0 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -47,7 +47,7 @@ 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()) } } diff --git a/repl/Cargo.toml b/repl/Cargo.toml index f47ffa235..b2dbbea7e 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -14,6 +14,7 @@ signal-hook = "0.3.17" pyo3 = { version = "0.19.2", features = ["auto-initialize"], optional = true } pep440_rs = { version = "0.3.11", optional = true } hyperon = { workspace = true, optional = true } #TODO: We can only link Hyperon directly or through Python, but not both at the same time. The right fix is to allow HyperonPy to be built within Hyperon, See https://github.com/trueagi-io/hyperon-experimental/issues/283 +snailquote = "0.3.1" [[bin]] name = "metta-repl" diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 7bbd573d7..18ab9158f 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 snailquote::unescape; /// Load the hyperon module, and get the "__version__" attribute pub fn get_hyperonpy_version() -> Result { @@ -238,7 +239,9 @@ 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() + .map(|atom| unescape(&atom.to_string()).unwrap()) + .collect()) }, Err(_) => None } @@ -258,7 +261,7 @@ pub mod metta_interface_mod { Ok(if result.is_none() { None } else { - Some(strip_quotes(&result.to_string()).to_string()) + Some(unescape(&result.to_string()).unwrap()) }) }).unwrap() } @@ -320,7 +323,7 @@ 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}; pub use hyperon::metta::text::SyntaxNodeType as SyntaxNodeType; @@ -439,7 +442,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_into_string(atom)) } pub fn get_config_expr_vec(&mut self, config_name: &str) -> Option> { @@ -449,7 +452,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_into_string(atom) }) .collect()) } else { @@ -462,19 +465,8 @@ pub mod metta_interface_mod { } } -} - -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] - } - } - } - } + pub fn atom_into_string(atom: Atom) -> String { + snailquote::unescape(&atom.to_string()).unwrap() } - src } + From f117584ca13d2d10d7c7f13a6b4106dbcce96a80 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Thu, 9 Jan 2025 12:12:14 +0300 Subject: [PATCH 04/10] current state of PR --- lib/Cargo.toml | 1 + lib/src/metta/runner/stdlib/debug.rs | 8 +++++--- lib/src/metta/runner/stdlib/string.rs | 8 ++++---- lib/src/metta/runner/str.rs | 5 +++++ python/tests/test_stdlib.py | 4 ++-- repl/src/metta_shim.rs | 5 +---- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 241026c73..59ad8d078 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" +snailquote = "0.3.1" # pkg_mgmt deps xxhash-rust = {version="0.8.7", features=["xxh3"], optional=true } diff --git a/lib/src/metta/runner/stdlib/debug.rs b/lib/src/metta/runner/stdlib/debug.rs index ac3fed559..8faba6623 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_into_string; + use std::convert::TryInto; fn assert_results_equal(actual: &Vec, expected: &Vec) -> Result, ExecError> { @@ -93,10 +95,10 @@ impl Grounded for PrintAlternativesOp { impl CustomExecute for PrintAlternativesOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("print-alternatives! expects format string as a first argument and expression as a second argument"); - let atom = atom_to_string(args.get(0).ok_or_else(arg_error)?); + let atom = atom_into_string(args.get(0).ok_or_else(arg_error)?.clone()); let args = TryInto::<&ExpressionAtom>::try_into(args.get(1).ok_or_else(arg_error)?)?; let args: Vec = args.children().iter() - .map(|atom| atom_to_string(atom)) + .map(|atom| atom_into_string(atom.clone())) .collect(); println!("{} {}:", args.len(), atom); args.iter().for_each(|arg| println!(" {}", arg)); diff --git a/lib/src/metta/runner/stdlib/string.rs b/lib/src/metta/runner/stdlib/string.rs index 5185caa02..c095bcb88 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; @@ -25,7 +25,7 @@ impl CustomExecute for PrintlnOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("println! expects single atom as an argument"); let atom = args.get(0).ok_or_else(arg_error)?; - println!("{}", atom_to_string(atom)); + println!("{}", atom_into_string(atom.clone())); unit_result() } } @@ -50,10 +50,10 @@ impl Grounded for FormatArgsOp { impl CustomExecute for FormatArgsOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("format-args expects format string as a first argument and expression as a second argument"); - let format = atom_to_string(args.get(0).ok_or_else(arg_error)?); + let format = atom_into_string(args.get(0).ok_or_else(arg_error)?.clone()); let args = TryInto::<&ExpressionAtom>::try_into(args.get(1).ok_or_else(arg_error)?)?; let args: Vec = args.children().iter() - .map(|atom| atom_to_string(atom)) + .map(|atom| atom_into_string(atom.clone())) .collect(); let res = format.format(args.as_slice()); Ok(vec![Atom::gnd(Str::from_string(res))]) diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index a97c16ea0..11dfc34d2 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 snailquote::unescape; /// String type pub const ATOM_TYPE_STRING : Atom = sym!("String"); @@ -84,3 +85,7 @@ impl serial::ConvertingSerializer for StrSerializer { self.value } } + +pub fn atom_into_string(atom: Atom) -> String { + unescape(&atom.to_string()).unwrap() +} \ No newline at end of file diff --git a/python/tests/test_stdlib.py b/python/tests/test_stdlib.py index 3afa9f7ef..2d97394f1 100644 --- a/python/tests/test_stdlib.py +++ b/python/tests/test_stdlib.py @@ -19,8 +19,8 @@ def test_text_ops(self): #self.assertEqualMettaRunnerResults(metta.run('!(parse "$X")'), # [[(V("X"))]]) - self.assertEqualMettaRunnerResults(repr(metta.run('!(parse "\\"A\\"")')), - '[[\\"A\\"]]') + self.assertEqualMettaRunnerResults(metta.run('!(parse "\\"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 18ab9158f..775d368f6 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -324,6 +324,7 @@ pub mod metta_interface_mod { use hyperon::metta::runner::{Metta, RunnerState, Environment, EnvBuilder}; use hyperon::common::collections::VecDisplay; use super::{exec_state_prepare, exec_state_should_break}; + use hyperon::metta::runner::str::atom_into_string; pub use hyperon::metta::text::SyntaxNodeType as SyntaxNodeType; @@ -464,9 +465,5 @@ pub mod metta_interface_mod { None //TODO. Make this work when I have reliable value atom bridging } } - - pub fn atom_into_string(atom: Atom) -> String { - snailquote::unescape(&atom.to_string()).unwrap() - } } From 342adccafeb72a16f61d43563effc604ceab5a32 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Thu, 9 Jan 2025 12:52:35 +0300 Subject: [PATCH 05/10] some fixes after conversation with Vitaly --- python/hyperon/atoms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 65053e138..dbc19caf2 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -279,7 +279,7 @@ def __repr__(self): # Overwrite Python default representation of a string to use # double quotes instead of single quotes. if isinstance(self.content, str): - newstr = self.content.translate(str.maketrans({'"' : r'\"'})) + newstr = repr(self.content)[1:-1].translate(str.maketrans({'"' : r'\"'})) return f'"{newstr}"' # Use default representation for everything else From a834490fa9b3dd4b2ea506843e6a6c32a33484d0 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Fri, 10 Jan 2025 09:34:43 +0300 Subject: [PATCH 06/10] fixes after Vitaly's review --- lib/src/metta/runner/stdlib/debug.rs | 6 +++--- lib/src/metta/runner/stdlib/mod.rs | 13 ------------- lib/src/metta/runner/stdlib/string.rs | 6 +++--- lib/src/metta/runner/str.rs | 2 +- python/hyperon/stdlib.py | 3 +++ python/tests/test_stdlib.py | 2 +- repl/src/metta_shim.rs | 6 +++--- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/lib/src/metta/runner/stdlib/debug.rs b/lib/src/metta/runner/stdlib/debug.rs index 8faba6623..5b06a9f0a 100644 --- a/lib/src/metta/runner/stdlib/debug.rs +++ b/lib/src/metta/runner/stdlib/debug.rs @@ -8,7 +8,7 @@ use crate::atom::matcher::atoms_are_equivalent; use crate::metta::runner::stdlib::{grounded_op, regex, interpret_no_error, unit_result}; use crate::metta::runner::bool::*; -use crate::metta::runner::str::atom_into_string; +use crate::metta::runner::str::atom_to_string; use std::convert::TryInto; @@ -95,10 +95,10 @@ impl Grounded for PrintAlternativesOp { impl CustomExecute for PrintAlternativesOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("print-alternatives! expects format string as a first argument and expression as a second argument"); - let atom = atom_into_string(args.get(0).ok_or_else(arg_error)?.clone()); + let atom = atom_to_string(args.get(0).ok_or_else(arg_error)?); let args = TryInto::<&ExpressionAtom>::try_into(args.get(1).ok_or_else(arg_error)?)?; let args: Vec = args.children().iter() - .map(|atom| atom_into_string(atom.clone())) + .map(|atom| atom_to_string(atom)) .collect(); println!("{} {}:", args.len(), atom); args.iter().for_each(|arg| println!(" {}", arg)); 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/string.rs b/lib/src/metta/runner/stdlib/string.rs index c095bcb88..9fe9e8745 100644 --- a/lib/src/metta/runner/stdlib/string.rs +++ b/lib/src/metta/runner/stdlib/string.rs @@ -25,7 +25,7 @@ impl CustomExecute for PrintlnOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("println! expects single atom as an argument"); let atom = args.get(0).ok_or_else(arg_error)?; - println!("{}", atom_into_string(atom.clone())); + println!("{}", atom_to_string(atom)); unit_result() } } @@ -50,10 +50,10 @@ impl Grounded for FormatArgsOp { impl CustomExecute for FormatArgsOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("format-args expects format string as a first argument and expression as a second argument"); - let format = atom_into_string(args.get(0).ok_or_else(arg_error)?.clone()); + let format = atom_to_string(args.get(0).ok_or_else(arg_error)?); let args = TryInto::<&ExpressionAtom>::try_into(args.get(1).ok_or_else(arg_error)?)?; let args: Vec = args.children().iter() - .map(|atom| atom_into_string(atom.clone())) + .map(|atom| atom_to_string(atom)) .collect(); let res = format.format(args.as_slice()); Ok(vec![Atom::gnd(Str::from_string(res))]) diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index 11dfc34d2..438720ea2 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -86,6 +86,6 @@ impl serial::ConvertingSerializer for StrSerializer { } } -pub fn atom_into_string(atom: Atom) -> String { +pub fn atom_to_string(atom: &Atom) -> String { unescape(&atom.to_string()).unwrap() } \ No newline at end of file diff --git a/python/hyperon/stdlib.py b/python/hyperon/stdlib.py index da6410708..5ad902c34 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -48,6 +48,9 @@ def match_(self, atom): return [{"matched_pattern": S(pattern)}] return [] +def parseImpl(): + + @register_atoms(pass_metta=True) def text_ops(run_context): """Add text operators diff --git a/python/tests/test_stdlib.py b/python/tests/test_stdlib.py index 2d97394f1..ab2ff68ff 100644 --- a/python/tests/test_stdlib.py +++ b/python/tests/test_stdlib.py @@ -19,7 +19,7 @@ def test_text_ops(self): #self.assertEqualMettaRunnerResults(metta.run('!(parse "$X")'), # [[(V("X"))]]) - self.assertEqualMettaRunnerResults(metta.run('!(parse "\\"A"\\")'), + self.assertEqualMettaRunnerResults(metta.run('!(parse "\\"A\\"")'), [[(ValueAtom("A"))]]) #self.assertEqualMettaRunnerResults(metta.run('!(parse "(func (Cons $x (Cons $xs $xss))) ")'), diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 775d368f6..9c8252aa8 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -324,7 +324,7 @@ pub mod metta_interface_mod { use hyperon::metta::runner::{Metta, RunnerState, Environment, EnvBuilder}; use hyperon::common::collections::VecDisplay; use super::{exec_state_prepare, exec_state_should_break}; - use hyperon::metta::runner::str::atom_into_string; + use hyperon::metta::runner::str::atom_to_string; pub use hyperon::metta::text::SyntaxNodeType as SyntaxNodeType; @@ -443,7 +443,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(atom_into_string(atom)) + Some(atom_to_string(&atom)) } pub fn get_config_expr_vec(&mut self, config_name: &str) -> Option> { @@ -453,7 +453,7 @@ pub mod metta_interface_mod { .into_iter() .map(|atom| { //TODO: We need to do atom type checking here - atom_into_string(atom) + atom_to_string(&atom) }) .collect()) } else { From e888e45ddff645be53992946483518fe8a5f2f4f Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 13 Jan 2025 19:53:17 +0300 Subject: [PATCH 07/10] Check Python string is passed as an input to parse function --- python/hyperon/stdlib.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/hyperon/stdlib.py b/python/hyperon/stdlib.py index 5ad902c34..2f3cabcea 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -48,7 +48,14 @@ def match_(self, atom): return [{"matched_pattern": S(pattern)}] return [] -def parseImpl(): +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) @@ -66,8 +73,7 @@ def text_ops(run_context): 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()]))], From 9417b4d2109e5bfa127e5e2c250fea09f05c9bb3 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 13 Jan 2025 19:54:58 +0300 Subject: [PATCH 08/10] Parse \u{...} escape sequences properly --- lib/Cargo.toml | 2 +- lib/src/metta/runner/str.rs | 29 +++++++++++++++++++++++++++-- repl/Cargo.toml | 1 - repl/src/metta_shim.rs | 5 ++--- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 59ad8d078..cf5cf24a2 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,7 +14,7 @@ rand = "0.8.5" bitset = "0.1.2" dyn-fmt = "0.4.0" itertools = "0.13.0" -snailquote = "0.3.1" +unescaper = "0.1.5" # pkg_mgmt deps xxhash-rust = {version="0.8.7", features=["xxh3"], optional=true } diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index 438720ea2..e3b5e0d02 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -2,7 +2,7 @@ use crate::*; use crate::common::collections::ImmutableString; use crate::serial; use crate::atom::serial::ConvertingSerializer; -use snailquote::unescape; +use unescaper; /// String type pub const ATOM_TYPE_STRING : Atom = sym!("String"); @@ -88,4 +88,29 @@ impl serial::ConvertingSerializer for StrSerializer { pub fn atom_to_string(atom: &Atom) -> String { unescape(&atom.to_string()).unwrap() -} \ No newline at end of file +} + +pub fn unescape(str: &str) -> unescaper::Result { + unescaper::unescape(str).map(|mut s| { + s.remove(0); + s.pop(); + s + }) +} + +#[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()); + } +} diff --git a/repl/Cargo.toml b/repl/Cargo.toml index b2dbbea7e..f47ffa235 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -14,7 +14,6 @@ signal-hook = "0.3.17" pyo3 = { version = "0.19.2", features = ["auto-initialize"], optional = true } pep440_rs = { version = "0.3.11", optional = true } hyperon = { workspace = true, optional = true } #TODO: We can only link Hyperon directly or through Python, but not both at the same time. The right fix is to allow HyperonPy to be built within Hyperon, See https://github.com/trueagi-io/hyperon-experimental/issues/283 -snailquote = "0.3.1" [[bin]] name = "metta-repl" diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 9c8252aa8..530cc33cc 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -39,7 +39,7 @@ pub mod metta_interface_mod { use pyo3::types::{PyTuple, PyString, PyBool, PyList, PyDict}; use hyperon::common::collections::VecDisplay; use super::{exec_state_prepare, exec_state_should_break}; - use snailquote::unescape; + use hyperon::metta::runner::str::unescape; /// Load the hyperon module, and get the "__version__" attribute pub fn get_hyperonpy_version() -> Result { @@ -240,7 +240,7 @@ pub mod metta_interface_mod { match result.downcast::() { Ok(result_list) => { Some(result_list.into_iter() - .map(|atom| unescape(&atom.to_string()).unwrap()) + .map(|atom| { unescape(&atom.to_string()).unwrap() }) .collect()) }, Err(_) => None @@ -303,7 +303,6 @@ pub mod metta_interface_mod { } } } - } /// The "no python" path involves a reimplementation of all of the MeTTa interface points calling MeTTa From 91ad9138dccc22a0b5697379e0ea111065c4c948 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 14 Jan 2025 12:31:36 +0300 Subject: [PATCH 09/10] Fix atom_to_string() to properly work with atoms other than string --- lib/src/metta/runner/str.rs | 16 +++++++++++++++- repl/src/metta_shim.rs | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/str.rs b/lib/src/metta/runner/str.rs index e3b5e0d02..8dbef1adc 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -87,7 +87,13 @@ impl serial::ConvertingSerializer for StrSerializer { } pub fn atom_to_string(atom: &Atom) -> String { - unescape(&atom.to_string()).unwrap() + 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 { @@ -113,4 +119,12 @@ mod tests { 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/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 530cc33cc..87344da1d 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -240,7 +240,8 @@ pub mod metta_interface_mod { match result.downcast::() { Ok(result_list) => { Some(result_list.into_iter() - .map(|atom| { unescape(&atom.to_string()).unwrap() }) + // String atom is expected as a value + .map(|atom| unescape(&atom.to_string()).unwrap()) .collect()) }, Err(_) => None @@ -261,6 +262,7 @@ pub mod metta_interface_mod { Ok(if result.is_none() { None } else { + // String atom is expected as a value Some(unescape(&result.to_string()).unwrap()) }) }).unwrap() From 18831747c1dc240ba8670ee44d763a877fb306d3 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 14 Jan 2025 13:16:40 +0300 Subject: [PATCH 10/10] Remove strip_quotes() function Use function to extract string from atom instead. --- lib/src/metta/runner/environment.rs | 4 --- lib/src/metta/runner/stdlib/module.rs | 35 ++++++-------------------- lib/src/metta/runner/stdlib/package.rs | 21 +++------------- lib/src/metta/runner/str.rs | 23 ++++++----------- 4 files changed, 18 insertions(+), 65 deletions(-) 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/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/str.rs b/lib/src/metta/runner/str.rs index 8dbef1adc..5ab197342 100644 --- a/lib/src/metta/runner/str.rs +++ b/lib/src/metta/runner/str.rs @@ -52,22 +52,6 @@ impl std::fmt::Display for 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, @@ -104,6 +88,13 @@ pub fn unescape(str: &str) -> unescaper::Result { }) } +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::*;