diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index d05f0fcd0..5bf3e94ca 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -440,6 +440,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 2a4e1f401..3184fec84 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::*; @@ -1341,6 +1342,70 @@ 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. +// see comment https://github.com/trueagi-io/hyperon-experimental/pull/791#discussion_r1824355414 +#[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: 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("Range is empty")); + } + let mut rng = rand::thread_rng(); + Ok(vec![Atom::gnd(Number::Integer(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: 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("Range is empty")); + } + let mut rng = rand::thread_rng(); + Ok(vec![Atom::gnd(Number::Float(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 { @@ -1860,6 +1925,10 @@ mod non_minimal_only_stdlib { 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 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{}); @@ -2183,6 +2252,23 @@ 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(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("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("Range is empty"))); + } + #[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 10ce57e01..a1cb5f5b9 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -97,6 +97,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 e9fcb1f3f..de3cf2c38 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -440,6 +440,10 @@ pub fn register_common_tokens(tref: &mut Tokenizer, _tokenizer: Shared= $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")]])); + 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")]])); + } + #[test] fn metta_switch() { let result = run_program("!(eval (switch (A $b) ( (($a B) ($b $a)) ((B C) (C B)) )))");