From 897a89a634685fa109a7f7319812a8c653e10064 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Thu, 31 Oct 2024 11:33:41 +0300 Subject: [PATCH 01/10] random-float and random-int added --- lib/src/metta/runner/stdlib.metta | 14 +++++ lib/src/metta/runner/stdlib.rs | 71 +++++++++++++++++++++++ lib/src/metta/runner/stdlib_minimal.metta | 14 +++++ lib/src/metta/runner/stdlib_minimal.rs | 10 ++++ 4 files changed, 109 insertions(+) diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index 2139af922..341d6edee 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -422,6 +422,20 @@ (@param "Index"))) (@return "Atom from an expression in the place defined by index. Error if index is out of bounds of an expression")) +(@doc random-int + (@desc "Returns random int number from range defined by two numbers (first and second argument)") + (@params ( + (@param "Range start") + (@param "Range end"))) + (@return "Random int number from defined range")) + +(@doc random-float + (@desc "Returns random float number from range defined by two numbers (first and second argument)") + (@params ( + (@param "Range start") + (@param "Range end"))) + (@return "Random float number from defined range")) + (@doc println! (@desc "Prints a line of text to the console") (@params ( diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 0aee17415..67cb4af63 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -21,6 +21,7 @@ use std::cell::RefCell; use std::fmt::Display; use std::collections::HashMap; use regex::Regex; +use rand::Rng; use super::arithmetics::*; use super::string::*; @@ -1253,6 +1254,64 @@ impl CustomExecute for SubtractionAtomOp { } } +#[derive(Clone, Debug)] +pub struct RandomIntOp {} + +grounded_op!(RandomIntOp, "random-int"); + +impl Grounded for RandomIntOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER]) + } + + fn as_execute(&self) -> Option<&dyn CustomExecute> { + Some(self) + } +} + +impl CustomExecute for RandomIntOp { + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("random-int expects two arguments: number (start) and number (end)"); + let start = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; + let end = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; + let range = Into::::into(start)..Into::::into(end); + if range.is_empty() { + return Err(ExecError::from("Wrong range")); + } + let mut rng = rand::thread_rng(); + Ok(vec![Atom::value(rng.gen_range::(range))]) + } +} + +#[derive(Clone, Debug)] +pub struct RandomFloatOp {} + +grounded_op!(RandomFloatOp, "random-float"); + +impl Grounded for RandomFloatOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER]) + } + + fn as_execute(&self) -> Option<&dyn CustomExecute> { + Some(self) + } +} + +impl CustomExecute for RandomFloatOp { + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("random-float expects two arguments: number (start) and number (end)"); + let start = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; + let end = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; + let range = Into::::into(start)..Into::::into(end); + if range.is_empty() { + return Err(ExecError::from("Wrong range")); + } + let mut rng = rand::thread_rng(); + Ok(vec![Atom::value(rng.gen_range::(range))]) + } +} + /// The internal `non_minimal_only_stdlib` module contains code that is never used by the minimal stdlib #[cfg(feature = "old_interpreter")] mod non_minimal_only_stdlib { @@ -1766,6 +1825,10 @@ mod non_minimal_only_stdlib { tref.register_token(regex(r"cons-atom"), move |_| { cons_atom_op.clone() }); let index_atom_op = Atom::gnd(IndexAtomOp{}); tref.register_token(regex(r"index-atom"), move |_| { index_atom_op.clone() }); + let random_int_op = Atom::gnd(RandomIntOp{}); + tref.register_token(regex(r"random-int"), move |_| { random_int_op.clone() }); + let random_float_op = Atom::gnd(RandomFloatOp{}); + tref.register_token(regex(r"random-float"), move |_| { random_float_op.clone() }); let println_op = Atom::gnd(PrintlnOp{}); tref.register_token(regex(r"println!"), move |_| { println_op.clone() }); let format_args_op = Atom::gnd(FormatArgsOp{}); @@ -2061,6 +2124,14 @@ mod tests { assert_eq!(res, Err(ExecError::from("Index is out of bounds"))); } + #[test] + fn random_op() { + let res = RandomIntOp{}.execute(&mut vec![expr!({Number::Integer(2)}), expr!({Number::Integer(-2)})]); + assert_eq!(res, Err(ExecError::from("Wrong range"))); + let res = RandomFloatOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(0)})]); + assert_eq!(res, Err(ExecError::from("Wrong range"))); + } + #[test] fn bind_new_space_op() { let tokenizer = Shared::new(Tokenizer::new()); diff --git a/lib/src/metta/runner/stdlib_minimal.metta b/lib/src/metta/runner/stdlib_minimal.metta index e4d43d89a..68e2f56ca 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -79,6 +79,20 @@ (@param "Index"))) (@return "Atom from an expression in the place defined by index. Error if index is out of bounds")) +(@doc random-int + (@desc "Returns random int number from range defined by two numbers (first and second argument)") + (@params ( + (@param "Range start") + (@param "Range end"))) + (@return "Random int number from defined range")) + +(@doc random-float + (@desc "Returns random float number from range defined by two numbers (first and second argument)") + (@params ( + (@param "Range start") + (@param "Range end"))) + (@return "Random float number from defined range")) + (@doc collapse-bind (@desc "Evaluates minimal MeTTa operation (first argument) and returns an expression which contains all alternative evaluations in a form (Atom Bindings). Bindings are represented in a form of a grounded atom.") (@params ( diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index ff81ba839..cdc0caa70 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -434,6 +434,10 @@ pub fn register_common_tokens(tref: &mut Tokenizer, _tokenizer: Shared Date: Fri, 1 Nov 2024 08:26:41 +0300 Subject: [PATCH 02/10] fixes after Vitaly's review --- lib/src/metta/runner/stdlib.rs | 38 ++++++++++++++++++-------- lib/src/metta/runner/stdlib_minimal.rs | 13 +++++++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 67cb4af63..ec075f306 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1254,6 +1254,11 @@ impl CustomExecute for SubtractionAtomOp { } } + +//TODO: In the current version of rand it is possible for rust to hang if range end's value is too +// big. In future releases (0.9+) of rand signature of sample_single will be changed and it will be +// possible to use match construction to cover overflow and other errors. So after library will be +// upgraded RandomInt and RandomFloat codes should be altered. #[derive(Clone, Debug)] pub struct RandomIntOp {} @@ -1272,14 +1277,14 @@ impl Grounded for RandomIntOp { impl CustomExecute for RandomIntOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("random-int expects two arguments: number (start) and number (end)"); - let start = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; - let end = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; - let range = Into::::into(start)..Into::::into(end); + let start: i64 = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?.into(); + let end: i64 = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?.into(); + let range = start..end; if range.is_empty() { - return Err(ExecError::from("Wrong range")); + return Err(ExecError::from("Range is empty")); } let mut rng = rand::thread_rng(); - Ok(vec![Atom::value(rng.gen_range::(range))]) + Ok(vec![Atom::gnd(Number::Integer(rng.gen_range(range)))]) } } @@ -1301,14 +1306,14 @@ impl Grounded for RandomFloatOp { impl CustomExecute for RandomFloatOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("random-float expects two arguments: number (start) and number (end)"); - let start = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; - let end = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?; - let range = Into::::into(start)..Into::::into(end); + let start: f64 = AsPrimitive::from_atom(args.get(0).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?.into(); + let end: f64 = AsPrimitive::from_atom(args.get(1).ok_or_else(arg_error)?).as_number().ok_or_else(arg_error)?.into(); + let range = start..end; if range.is_empty() { - return Err(ExecError::from("Wrong range")); + return Err(ExecError::from("Range is empty")); } let mut rng = rand::thread_rng(); - Ok(vec![Atom::value(rng.gen_range::(range))]) + Ok(vec![Atom::gnd(Number::Float(rng.gen_range(range)))]) } } @@ -2126,10 +2131,19 @@ mod tests { #[test] fn random_op() { + let res = RandomIntOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]); + let range = 0..5; + let res_i64: i64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap()).as_number().unwrap().into(); + assert!(range.contains(&res_i64)); let res = RandomIntOp{}.execute(&mut vec![expr!({Number::Integer(2)}), expr!({Number::Integer(-2)})]); - assert_eq!(res, Err(ExecError::from("Wrong range"))); + assert_eq!(res, Err(ExecError::from("Range is empty"))); + + let res = RandomFloatOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]); + let range = 0.0..5.0; + let res_f64: f64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap()).as_number().unwrap().into(); + assert!(range.contains(&res_f64)); let res = RandomFloatOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(0)})]); - assert_eq!(res, Err(ExecError::from("Wrong range"))); + assert_eq!(res, Err(ExecError::from("Range is empty"))); } #[test] diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index cdc0caa70..be6125608 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -629,8 +629,17 @@ mod tests { #[test] fn metta_random() { - assert_eq!(run_program(&format!("!(random-int 0 0)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomIntOp{} } {Number::Integer(0)} {Number::Integer(0)}) "Wrong range")]])); - assert_eq!(run_program(&format!("!(random-float 0 -5)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomFloatOp{} } {Number::Integer(0)} {Number::Integer(-5)}) "Wrong range")]])); + let res = run_program(&format!("!(random-int 0 5)")); + let range = 0..5; + let res_i64: i64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap().get(0).unwrap()).as_number().unwrap().into(); + assert!(range.contains(&res_i64)); + assert_eq!(run_program(&format!("!(random-int 0 0)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomIntOp{} } {Number::Integer(0)} {Number::Integer(0)}) "Range is empty")]])); + + let res = run_program(&format!("!(random-float 0 5)")); + let range = 0.0..5.0; + let res_f64: f64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap().get(0).unwrap()).as_number().unwrap().into(); + assert!(range.contains(&res_f64)); + assert_eq!(run_program(&format!("!(random-float 0 -5)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomFloatOp{} } {Number::Integer(0)} {Number::Integer(-5)}) "Range is empty")]])); } #[test] From 49e671e732380362f4f2fceb2d433aac82d590ae Mon Sep 17 00:00:00 2001 From: Innokenty Date: Fri, 1 Nov 2024 08:35:47 +0300 Subject: [PATCH 03/10] better tests for stdlib_minimal.rs --- lib/src/metta/runner/stdlib_minimal.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index be6125608..92a8cb704 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -629,16 +629,9 @@ mod tests { #[test] fn metta_random() { - let res = run_program(&format!("!(random-int 0 5)")); - let range = 0..5; - let res_i64: i64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap().get(0).unwrap()).as_number().unwrap().into(); - assert!(range.contains(&res_i64)); + assert_eq!(run_program(&format!("!(chain (eval (random-int 0 5)) $rint (and (>= $rint 0) (< $rint 5)))")), Ok(vec![vec![expr!({Bool(true)})]])); assert_eq!(run_program(&format!("!(random-int 0 0)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomIntOp{} } {Number::Integer(0)} {Number::Integer(0)}) "Range is empty")]])); - - let res = run_program(&format!("!(random-float 0 5)")); - let range = 0.0..5.0; - let res_f64: f64 = AsPrimitive::from_atom(res.unwrap().get(0).unwrap().get(0).unwrap()).as_number().unwrap().into(); - assert!(range.contains(&res_f64)); + assert_eq!(run_program(&format!("!(chain (eval (random-float 0.0 5.0)) $rfloat (and (>= $rfloat 0.0) (< $rfloat 5.0)))")), Ok(vec![vec![expr!({Bool(true)})]])); assert_eq!(run_program(&format!("!(random-float 0 -5)")), Ok(vec![vec![expr!("Error" ({ stdlib::RandomFloatOp{} } {Number::Integer(0)} {Number::Integer(-5)}) "Range is empty")]])); } From 54314ba78f6d71762da2d500525e2f5d9e742ac8 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 1 Nov 2024 10:47:48 +0300 Subject: [PATCH 04/10] Add additional info about rand usage rework in future --- lib/src/metta/runner/stdlib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index ec075f306..31ed06af1 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1259,6 +1259,7 @@ impl CustomExecute for SubtractionAtomOp { // big. In future releases (0.9+) of rand signature of sample_single will be changed and it will be // possible to use match construction to cover overflow and other errors. So after library will be // upgraded RandomInt and RandomFloat codes should be altered. +// see comment https://github.com/trueagi-io/hyperon-experimental/pull/791#discussion_r1824355414 #[derive(Clone, Debug)] pub struct RandomIntOp {} From dd00140c584c10aca64b07b65f29f19704384ec5 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Tue, 5 Nov 2024 07:48:08 +0300 Subject: [PATCH 05/10] size-atom added --- lib/src/metta/runner/stdlib.rs | 34 ++++++++++++++++++++++++++ lib/src/metta/runner/stdlib_minimal.rs | 8 ++++++ 2 files changed, 42 insertions(+) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 0aee17415..4184ce4f4 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1160,6 +1160,30 @@ impl CustomExecute for IntersectionAtomOp { } } +#[derive(Clone, Debug)] +pub struct SizeAtomOp {} + +grounded_op!(SizeAtomOp, "size-atom"); + +impl Grounded for SizeAtomOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_NUMBER]) + } + + fn as_execute(&self) -> Option<&dyn CustomExecute> { + Some(self) + } +} + +impl CustomExecute for SizeAtomOp { + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("size-atom expects one argument: expression"); + let children = TryInto::<&ExpressionAtom>::try_into(args.get(0).ok_or_else(arg_error)?)?.children(); + let size = children.len(); + Ok(vec![Atom::gnd(Number::Integer(size as i64))]) + } +} + #[derive(Clone, Debug)] pub struct IndexAtomOp {} @@ -1764,6 +1788,8 @@ mod non_minimal_only_stdlib { tref.register_token(regex(r"cdr-atom"), move |_| { cdr_atom_op.clone() }); let cons_atom_op = Atom::gnd(ConsAtomOp{}); tref.register_token(regex(r"cons-atom"), move |_| { cons_atom_op.clone() }); + let size_atom_op = Atom::gnd(SizeAtomOp{}); + tref.register_token(regex(r"size-atom"), move |_| { size_atom_op.clone() }); let index_atom_op = Atom::gnd(IndexAtomOp{}); tref.register_token(regex(r"index-atom"), move |_| { index_atom_op.clone() }); let println_op = Atom::gnd(PrintlnOp{}); @@ -2053,6 +2079,14 @@ mod tests { assert_eq!(res, vec![expr!(("A" "F") ("B" "C") "D")]); } + #[test] + fn size_atom_op() { + let res = SizeAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Integer(3)} {Number::Integer(2)} {Number::Integer(1)})]).expect("No result returned"); + assert_eq!(res, vec![expr!({Number::Integer(5)})]); + let res = SizeAtomOp{}.execute(&mut vec![expr!()]).expect("No result returned"); + assert_eq!(res, vec![expr!({Number::Integer(0)})]); + } + #[test] fn index_atom_op() { let res = IndexAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Integer(3)} {Number::Integer(2)} {Number::Integer(1)}), expr!({Number::Integer(2)})]).expect("No result returned"); diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index ff81ba839..4d48a7e03 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -432,6 +432,8 @@ pub fn register_common_tokens(tref: &mut Tokenizer, _tokenizer: Shared Date: Tue, 5 Nov 2024 07:51:42 +0300 Subject: [PATCH 06/10] documentation for size atom added --- lib/src/metta/runner/stdlib.metta | 6 ++++++ lib/src/metta/runner/stdlib_minimal.metta | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index 2139af922..ad5ca9944 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -415,6 +415,12 @@ (@param "Tail of an expression"))) (@return "New expression consists of two input arguments")) +(@doc size-atom + (@desc "Returns size of an expression (first argument)") + (@params ( + (@param "Expression"))) + (@return "Size of an expression")) + (@doc index-atom (@desc "Returns atom from an expression (first argument) using index (second argument) or error if index is out of bounds") (@params ( diff --git a/lib/src/metta/runner/stdlib_minimal.metta b/lib/src/metta/runner/stdlib_minimal.metta index e4d43d89a..3cc56c9ad 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -72,6 +72,12 @@ (@return "Deconsed expression")) (: decons-atom (-> Expression Expression)) +(@doc size-atom + (@desc "Returns size of an expression (first argument)") + (@params ( + (@param "Expression"))) + (@return "Size of an expression")) + (@doc index-atom (@desc "Returns atom from an expression (first argument) using index (second argument) or error if index is out of bounds") (@params ( From 9eb027b3c5105a93ea86697ed203ab6d12cab742 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Thu, 7 Nov 2024 11:01:40 +0300 Subject: [PATCH 07/10] min/max -atom functions added --- lib/src/metta/runner/stdlib.metta | 12 ++++ lib/src/metta/runner/stdlib.rs | 88 +++++++++++++++++++++++ lib/src/metta/runner/stdlib_minimal.metta | 12 ++++ lib/src/metta/runner/stdlib_minimal.rs | 18 +++++ 4 files changed, 130 insertions(+) diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index ad5ca9944..d05f0fcd0 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -415,6 +415,18 @@ (@param "Tail of an expression"))) (@return "New expression consists of two input arguments")) +(@doc min-atom + (@desc "Returns atom with min value in the expression (first argument). Only numbers allowed") + (@params ( + (@param "Expression"))) + (@return "Min value in the expression. Error if expression contains non-numeric value or is empty")) + +(@doc max-atom + (@desc "Returns atom with max value in the expression (first argument). Only numbers allowed") + (@params ( + (@param "Expression"))) + (@return "Max value in the expression. Error if expression contains non-numeric value or is empty")) + (@doc size-atom (@desc "Returns size of an expression (first argument)") (@params ( diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 4184ce4f4..2a4e1f401 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1160,6 +1160,70 @@ impl CustomExecute for IntersectionAtomOp { } } +#[derive(Clone, Debug)] +pub struct MaxAtomOp {} + +grounded_op!(MaxAtomOp, "max-atom"); + +impl Grounded for MaxAtomOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + } + + fn as_execute(&self) -> Option<&dyn CustomExecute> { + Some(self) + } +} + +impl CustomExecute for MaxAtomOp { + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("max-atom expects one argument: expression"); + let children = TryInto::<&ExpressionAtom>::try_into(args.get(0).ok_or_else(arg_error)?)?.children(); + for x in children.iter() { + match AsPrimitive::from_atom(x).as_number() { + None => Err(ExecError::from("Only numbers allowed in expression")), + _ => Ok({}), + }? + }; + match children.into_iter().map(|x| Into::::into(AsPrimitive::from_atom(x).as_number().unwrap())).reduce(f64::max) { + Some(max) => Ok(vec![Atom::gnd(Number::Float(max))]), + None => Err(ExecError::from("Empty expression")), + } + } +} + +#[derive(Clone, Debug)] +pub struct MinAtomOp {} + +grounded_op!(MinAtomOp, "min-atom"); + +impl Grounded for MinAtomOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + } + + fn as_execute(&self) -> Option<&dyn CustomExecute> { + Some(self) + } +} + +impl CustomExecute for MinAtomOp { + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("min-atom expects one argument: expression"); + let children = TryInto::<&ExpressionAtom>::try_into(args.get(0).ok_or_else(arg_error)?)?.children(); + for x in children.iter() { + match AsPrimitive::from_atom(x).as_number() { + None => Err(ExecError::from("Only numbers allowed in expression")), + _ => Ok({}), + }? + }; + match children.into_iter().map(|x| Into::::into(AsPrimitive::from_atom(x).as_number().unwrap())).reduce(f64::min) { + Some(min) => Ok(vec![Atom::gnd(Number::Float(min))]), + None => Err(ExecError::from("Empty expression")), + } + } +} + #[derive(Clone, Debug)] pub struct SizeAtomOp {} @@ -1788,6 +1852,10 @@ mod non_minimal_only_stdlib { tref.register_token(regex(r"cdr-atom"), move |_| { cdr_atom_op.clone() }); let cons_atom_op = Atom::gnd(ConsAtomOp{}); tref.register_token(regex(r"cons-atom"), move |_| { cons_atom_op.clone() }); + let max_atom_op = Atom::gnd(MaxAtomOp{}); + tref.register_token(regex(r"max-atom"), move |_| { max_atom_op.clone() }); + let min_atom_op = Atom::gnd(MinAtomOp{}); + tref.register_token(regex(r"min-atom"), move |_| { min_atom_op.clone() }); let size_atom_op = Atom::gnd(SizeAtomOp{}); tref.register_token(regex(r"size-atom"), move |_| { size_atom_op.clone() }); let index_atom_op = Atom::gnd(IndexAtomOp{}); @@ -2087,6 +2155,26 @@ mod tests { assert_eq!(res, vec![expr!({Number::Integer(0)})]); } + #[test] + fn min_atom_op() { + let res = MinAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Float(5.5)})]).expect("No result returned"); + assert_eq!(res, vec![expr!({Number::Integer(4)})]); + let res = MinAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} "A")]); + assert_eq!(res, Err(ExecError::from("Only numbers allowed in expression"))); + let res = MinAtomOp{}.execute(&mut vec![expr!()]); + assert_eq!(res, Err(ExecError::from("Empty expression"))); + } + + #[test] + fn max_atom_op() { + let res = MaxAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Float(5.5)})]).expect("No result returned"); + assert_eq!(res, vec![expr!({Number::Float(5.5)})]); + let res = MaxAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} "A")]); + assert_eq!(res, Err(ExecError::from("Only numbers allowed in expression"))); + let res = MaxAtomOp{}.execute(&mut vec![expr!()]); + assert_eq!(res, Err(ExecError::from("Empty expression"))); + } + #[test] fn index_atom_op() { let res = IndexAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Integer(3)} {Number::Integer(2)} {Number::Integer(1)}), expr!({Number::Integer(2)})]).expect("No result returned"); diff --git a/lib/src/metta/runner/stdlib_minimal.metta b/lib/src/metta/runner/stdlib_minimal.metta index 3cc56c9ad..10ce57e01 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -72,6 +72,18 @@ (@return "Deconsed expression")) (: decons-atom (-> Expression Expression)) +(@doc min-atom + (@desc "Returns atom with min value in the expression (first argument). Only numbers allowed") + (@params ( + (@param "Expression"))) + (@return "Min value in the expression. Error if expression contains non-numeric value or is empty")) + +(@doc max-atom + (@desc "Returns atom with max value in the expression (first argument). Only numbers allowed") + (@params ( + (@param "Expression"))) + (@return "Max value in the expression. Error if expression contains non-numeric value or is empty")) + (@doc size-atom (@desc "Returns size of an expression (first argument)") (@params ( diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index 4d48a7e03..e9fcb1f3f 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -432,6 +432,10 @@ pub fn register_common_tokens(tref: &mut Tokenizer, _tokenizer: Shared Date: Thu, 7 Nov 2024 19:08:58 +0300 Subject: [PATCH 08/10] Fix native interface of the Python serializer Fix names of the functions inside pybind11 macro stub. --- python/hyperon/atoms.py | 11 +++++++---- python/hyperonpy.cpp | 4 ++-- python/tests/test_grounded_type.py | 13 +++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 81ce3ed3b..6bc88aa80 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -177,13 +177,16 @@ def _priv_gnd_get_object(atom): return SpaceRef._from_cspace(hp.atom_get_space(atom.catom)) elif typ == S('Bool') or typ == S('Number'): converter = ConvertingSerializer() - hp.atom_gnd_serialize(atom.catom, converter) - if converter.value is None: - raise RuntimeError(f"Could not convert atom {atom}") + try: + res = hp.atom_gnd_serialize(atom.catom, converter) + except Exception as e: + raise RuntimeError(f"Could not convert atom {atom} to Python value, exception caught: {e}") + if res != SerialResult.OK or converter.value is None: + raise RuntimeError(f"Could not convert atom {atom} to Python value") else: return ValueObject(converter.value) else: - raise TypeError(f"Cannot get_object of unsupported non-C {atom}") + raise TypeError(f"Cannot get Python object of unsupported non-C atom {atom}") def G(object, type=AtomType.UNDEFINED): diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index 9587b00f4..2eece4d12 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -256,11 +256,11 @@ struct PySerializer : public Serializer { } serial_result_t serialize_int(py::int_ v) override { - PYBIND11_OVERRIDE_PURE(serial_result_t, Serializer, serialize_longlong, v); + PYBIND11_OVERRIDE_PURE(serial_result_t, Serializer, serialize_int, v); } serial_result_t serialize_float(py::float_ v) override { - PYBIND11_OVERRIDE_PURE(serial_result_t, Serializer, serialize_double, v); + PYBIND11_OVERRIDE_PURE(serial_result_t, Serializer, serialize_float, v); } }; diff --git a/python/tests/test_grounded_type.py b/python/tests/test_grounded_type.py index 3a6c65463..9c0beb012 100644 --- a/python/tests/test_grounded_type.py +++ b/python/tests/test_grounded_type.py @@ -138,5 +138,18 @@ def test_undefined_operation_type(self): self.assertNotEqual(metta.parse_single("untop").get_grounded_type(), metta.parse_single("untyped").get_grounded_type()) + def test_conversion_between_rust_and_python(self): + self.maxDiff = None + metta = MeTTa(env_builder=Environment.test_env()) + integer = metta.run('!(+ 1 (random-int 4 5))', flat=True)[0].get_object() + self.assertEqual(integer, ValueObject(5)) + float = metta.run('!(+ 1.0 (random-float 4 5))', flat=True)[0].get_object() + self.assertTrue(float.value >= 5.0 and float.value < 6) + bool = metta.run('!(not (flip))', flat=True)[0].get_object() + self.assertTrue(bool.value or not bool.value) + false = metta.run('!(not True)', flat=True)[0].get_object() + self.assertEquals(false, ValueObject(False)) + + if __name__ == "__main__": unittest.main() From 09529609bc24de75fed62052898de322cb66f42f Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 7 Nov 2024 19:12:48 +0300 Subject: [PATCH 09/10] Show grounded atom conversion exceptions if any Previous implementation hides conversion exception thrown from get_object() method. It makes debug difficult. Handle attempts to call get_object() on non-grounded atoms by separate condition. All other errors are thrown to the upper stack frames. --- python/hyperon/atoms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 81ce3ed3b..6594a0c83 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -329,9 +329,9 @@ def unwrap_args(atoms): except: raise NoReduceError() continue - try: + if hasattr(a, 'get_object'): args.append(a.get_object().content) - except: + else: # NOTE: # Currently, applying grounded operations to pure atoms is not reduced. # If we want, we can raise an exception, or form an error expression instead, From bbc8cca0010beb35b403b928361f3dc3184eaf6d Mon Sep 17 00:00:00 2001 From: Innokenty Date: Fri, 8 Nov 2024 10:29:24 +0300 Subject: [PATCH 10/10] Vitaly's fixes --- lib/src/metta/runner/stdlib.metta | 4 +- lib/src/metta/runner/stdlib.rs | 50 ++++++++++++----------- lib/src/metta/runner/stdlib_minimal.metta | 4 +- lib/src/metta/runner/stdlib_minimal.rs | 4 +- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index d05f0fcd0..7a0537bb6 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -418,13 +418,13 @@ (@doc min-atom (@desc "Returns atom with min value in the expression (first argument). Only numbers allowed") (@params ( - (@param "Expression"))) + (@param "Expression which contains atoms of Number type"))) (@return "Min value in the expression. Error if expression contains non-numeric value or is empty")) (@doc max-atom (@desc "Returns atom with max value in the expression (first argument). Only numbers allowed") (@params ( - (@param "Expression"))) + (@param "Expression which contains atoms of Number type"))) (@return "Max value in the expression. Error if expression contains non-numeric value or is empty")) (@doc size-atom diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 2a4e1f401..5ec1481e9 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1167,7 +1167,7 @@ grounded_op!(MaxAtomOp, "max-atom"); impl Grounded for MaxAtomOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_NUMBER]) } fn as_execute(&self) -> Option<&dyn CustomExecute> { @@ -1179,16 +1179,17 @@ impl CustomExecute for MaxAtomOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("max-atom expects one argument: expression"); let children = TryInto::<&ExpressionAtom>::try_into(args.get(0).ok_or_else(arg_error)?)?.children(); - for x in children.iter() { - match AsPrimitive::from_atom(x).as_number() { - None => Err(ExecError::from("Only numbers allowed in expression")), - _ => Ok({}), - }? - }; - match children.into_iter().map(|x| Into::::into(AsPrimitive::from_atom(x).as_number().unwrap())).reduce(f64::max) { - Some(max) => Ok(vec![Atom::gnd(Number::Float(max))]), - None => Err(ExecError::from("Empty expression")), - } + if children.is_empty() { + Err(ExecError::from("Empty expression")) + } else { + children.into_iter().fold(Ok(f64::NEG_INFINITY), |res, x| { + match (res, AsPrimitive::from_atom(x).as_number()) { + (res @ Err(_), _) => res, + (_, None) => Err(ExecError::from("Only numbers are allowed in expression")), + (Ok(max), Some(x)) => Ok(f64::max(max, x.into())), + } + }) + }.map(|max| vec![Atom::gnd(Number::Float(max))]) } } @@ -1199,7 +1200,7 @@ grounded_op!(MinAtomOp, "min-atom"); impl Grounded for MinAtomOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_NUMBER]) } fn as_execute(&self) -> Option<&dyn CustomExecute> { @@ -1211,16 +1212,17 @@ impl CustomExecute for MinAtomOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("min-atom expects one argument: expression"); let children = TryInto::<&ExpressionAtom>::try_into(args.get(0).ok_or_else(arg_error)?)?.children(); - for x in children.iter() { - match AsPrimitive::from_atom(x).as_number() { - None => Err(ExecError::from("Only numbers allowed in expression")), - _ => Ok({}), - }? - }; - match children.into_iter().map(|x| Into::::into(AsPrimitive::from_atom(x).as_number().unwrap())).reduce(f64::min) { - Some(min) => Ok(vec![Atom::gnd(Number::Float(min))]), - None => Err(ExecError::from("Empty expression")), - } + if children.is_empty() { + Err(ExecError::from("Empty expression")) + } else { + children.into_iter().fold(Ok(f64::INFINITY), |res, x| { + match (res, AsPrimitive::from_atom(x).as_number()) { + (res @ Err(_), _) => res, + (_, None) => Err(ExecError::from("Only numbers are allowed in expression")), + (Ok(min), Some(x)) => Ok(f64::min(min, x.into())), + } + }) + }.map(|min| vec![Atom::gnd(Number::Float(min))]) } } @@ -2160,7 +2162,7 @@ mod tests { let res = MinAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Float(5.5)})]).expect("No result returned"); assert_eq!(res, vec![expr!({Number::Integer(4)})]); let res = MinAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} "A")]); - assert_eq!(res, Err(ExecError::from("Only numbers allowed in expression"))); + assert_eq!(res, Err(ExecError::from("Only numbers are allowed in expression"))); let res = MinAtomOp{}.execute(&mut vec![expr!()]); assert_eq!(res, Err(ExecError::from("Empty expression"))); } @@ -2170,7 +2172,7 @@ mod tests { let res = MaxAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} {Number::Float(5.5)})]).expect("No result returned"); assert_eq!(res, vec![expr!({Number::Float(5.5)})]); let res = MaxAtomOp{}.execute(&mut vec![expr!({Number::Integer(5)} {Number::Integer(4)} "A")]); - assert_eq!(res, Err(ExecError::from("Only numbers allowed in expression"))); + assert_eq!(res, Err(ExecError::from("Only numbers are allowed in expression"))); let res = MaxAtomOp{}.execute(&mut vec![expr!()]); assert_eq!(res, Err(ExecError::from("Empty expression"))); } diff --git a/lib/src/metta/runner/stdlib_minimal.metta b/lib/src/metta/runner/stdlib_minimal.metta index 10ce57e01..08793ba91 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -75,13 +75,13 @@ (@doc min-atom (@desc "Returns atom with min value in the expression (first argument). Only numbers allowed") (@params ( - (@param "Expression"))) + (@param "Expression which contains atoms of Number type"))) (@return "Min value in the expression. Error if expression contains non-numeric value or is empty")) (@doc max-atom (@desc "Returns atom with max value in the expression (first argument). Only numbers allowed") (@params ( - (@param "Expression"))) + (@param "Expression which contains atoms of Number type"))) (@return "Max value in the expression. Error if expression contains non-numeric value or is empty")) (@doc size-atom diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index e9fcb1f3f..a3141ad9f 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -633,14 +633,14 @@ mod tests { fn metta_min_atom() { assert_eq!(run_program(&format!("!(min-atom (5 4 5.5))")), Ok(vec![vec![expr!({Number::Integer(4)})]])); assert_eq!(run_program(&format!("!(min-atom ())")), Ok(vec![vec![expr!("Error" ({ stdlib::MinAtomOp{} } ()) "Empty expression")]])); - assert_eq!(run_program(&format!("!(min-atom (3 A B 5))")), Ok(vec![vec![expr!("Error" ({ stdlib::MinAtomOp{} } ({Number::Integer(3)} "A" "B" {Number::Integer(5)})) "Only numbers allowed in expression")]])); + assert_eq!(run_program(&format!("!(min-atom (3 A B 5))")), Ok(vec![vec![expr!("Error" ({ stdlib::MinAtomOp{} } ({Number::Integer(3)} "A" "B" {Number::Integer(5)})) "Only numbers are allowed in expression")]])); } #[test] fn metta_max_atom() { assert_eq!(run_program(&format!("!(max-atom (5 4 5.5))")), Ok(vec![vec![expr!({Number::Float(5.5)})]])); assert_eq!(run_program(&format!("!(max-atom ())")), Ok(vec![vec![expr!("Error" ({ stdlib::MaxAtomOp{} } ()) "Empty expression")]])); - assert_eq!(run_program(&format!("!(max-atom (3 A B 5))")), Ok(vec![vec![expr!("Error" ({ stdlib::MaxAtomOp{} } ({Number::Integer(3)} "A" "B" {Number::Integer(5)})) "Only numbers allowed in expression")]])); + assert_eq!(run_program(&format!("!(max-atom (3 A B 5))")), Ok(vec![vec![expr!("Error" ({ stdlib::MaxAtomOp{} } ({Number::Integer(3)} "A" "B" {Number::Integer(5)})) "Only numbers are allowed in expression")]])); } #[test]