From 897a89a634685fa109a7f7319812a8c653e10064 Mon Sep 17 00:00:00 2001 From: Innokenty Date: Thu, 31 Oct 2024 11:33:41 +0300 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 {}