Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python's repr fixed #828

Merged
merged 14 commits into from
Jan 14, 2025
Merged
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
4 changes: 0 additions & 4 deletions lib/src/metta/runner/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u64>().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));
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/src/metta/runner/stdlib/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Atom>, expected: &Vec<Atom>) -> Result<Vec<Atom>, ExecError> {
Expand Down
13 changes: 0 additions & 13 deletions lib/src/metta/runner/stdlib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Vec<Atom>, String> {
Expand Down
35 changes: 7 additions & 28 deletions lib/src/metta/runner/stdlib/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -136,19 +129,12 @@ impl Grounded for IncludeOp {
impl CustomExecute for IncludeOp {
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, 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)
Expand Down Expand Up @@ -199,20 +185,13 @@ impl Grounded for ModSpaceOp {
impl CustomExecute for ModSpaceOp {
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, 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])
Expand Down
21 changes: 4 additions & 17 deletions lib/src/metta/runner/stdlib/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -35,14 +35,8 @@ impl Grounded for RegisterModuleOp {
impl CustomExecute for RegisterModuleOp {
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, 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::<Str>().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
Expand Down Expand Up @@ -90,19 +84,12 @@ impl Grounded for GitModuleOp {
impl CustomExecute for GitModuleOp {
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, 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::<Str>().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"))
};
Expand Down
2 changes: 1 addition & 1 deletion lib/src/metta/runner/stdlib/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
69 changes: 52 additions & 17 deletions lib/src/metta/runner/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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<Str>,
Expand All @@ -84,3 +69,53 @@ impl serial::ConvertingSerializer<Str> 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<String> {
unescaper::unescape(str).map(|mut s| {
s.remove(0);
s.pop();
s
})
}

pub(crate) fn expect_string_like_atom(atom: &Atom) -> Option<String> {
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));
}
}
3 changes: 2 additions & 1 deletion python/hyperon/atoms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions python/hyperon/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()]))],
Expand Down
1 change: 0 additions & 1 deletion python/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions python/tests/test_grounded_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion python/tests/test_stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))))]])
Expand Down
Loading
Loading