Skip to content

Commit

Permalink
Merge pull request #791 from DaddyWesker/random_numbers
Browse files Browse the repository at this point in the history
random-float and random-int added
  • Loading branch information
vsbogd authored Nov 7, 2024
2 parents 6547067 + 54314ba commit 054b66d
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 0 deletions.
14 changes: 14 additions & 0 deletions lib/src/metta/runner/stdlib.metta
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
86 changes: 86 additions & 0 deletions lib/src/metta/runner/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -1253,6 +1254,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<Vec<Atom>, 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<Vec<Atom>, 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 {
Expand Down Expand Up @@ -1766,6 +1831,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{});
Expand Down Expand Up @@ -2061,6 +2130,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());
Expand Down
14 changes: 14 additions & 0 deletions lib/src/metta/runner/stdlib_minimal.metta
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
12 changes: 12 additions & 0 deletions lib/src/metta/runner/stdlib_minimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ pub fn register_common_tokens(tref: &mut Tokenizer, _tokenizer: Shared<Tokenizer
tref.register_token(regex(r"match"), move |_| { match_op.clone() });
let index_atom_op = Atom::gnd(stdlib::IndexAtomOp{});
tref.register_token(regex(r"index-atom"), move |_| { index_atom_op.clone() });
let random_int_op = Atom::gnd(stdlib::RandomIntOp{});
tref.register_token(regex(r"random-int"), move |_| { random_int_op.clone() });
let random_float_op = Atom::gnd(stdlib::RandomFloatOp{});
tref.register_token(regex(r"random-float"), move |_| { random_float_op.clone() });
let mod_space_op = Atom::gnd(stdlib::ModSpaceOp::new(metta.clone()));
tref.register_token(regex(r"mod-space!"), move |_| { mod_space_op.clone() });
let print_mods_op = Atom::gnd(stdlib::PrintModsOp::new(metta.clone()));
Expand Down Expand Up @@ -623,6 +627,14 @@ mod tests {
assert_eq!(run_program(&format!("!(index-atom (A B C D E) 5)")), Ok(vec![vec![expr!("Error" ({ stdlib::IndexAtomOp{} } ("A" "B" "C" "D" "E") {Number::Integer(5)}) "Index is out of bounds")]]));
}

#[test]
fn metta_random() {
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")]]));
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)) )))");
Expand Down

0 comments on commit 054b66d

Please sign in to comment.