From 73a3c3d8ca96e0c53dee9eb5ab2f680ede5113ee Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 17 Aug 2023 21:30:22 +0300 Subject: [PATCH 01/27] Fix links to the minimal MeTTa code examples --- doc/minimal-metta.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/minimal-metta.md b/doc/minimal-metta.md index 32d3f4df0..a6b339b34 100644 --- a/doc/minimal-metta.md +++ b/doc/minimal-metta.md @@ -137,7 +137,7 @@ Reduce in loop until result is calculated: (eval (reduce $res $var $templ)) )))) ``` -[Link](https://github.com/vsbogd/hyperon-experimental/blob/f64bf92edf632a538aa6277b6048dbd418924435/lib/src/metta/runner/stdlib.rs#L1199-L1263) +[Link](https://github.com/trueagi-io/hyperon-experimental/blob/27861e63af1417df4780d9314eaf2e8a3b5cde06/lib/src/metta/runner/stdlib2.rs#L234-L302) to the full code of the interpreter in MeTTa (not finished yet). # Properties @@ -146,7 +146,7 @@ to the full code of the interpreter in MeTTa (not finished yet). The following program implements a Turing machine using the minimal MeTTa instruction set (the full code of the example can be found -[here](https://github.com/vsbogd/hyperon-experimental/blob/f64bf92edf632a538aa6277b6048dbd418924435/lib/src/metta/interpreter2.rs#L526-L566)): +[here](https://github.com/trueagi-io/hyperon-experimental/blob/27861e63af1417df4780d9314eaf2e8a3b5cde06/lib/src/metta/interpreter2.rs#L628-L669)): ```metta (= (tm $rule $state $tape) From a447234320f34cf2b69cc5dd74ec592b3914e817 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 1 Aug 2023 14:31:25 +0300 Subject: [PATCH 02/27] Add grounded atom case into test_return_bad_type_error --- lib/src/metta/runner/stdlib2.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index fc21494d5..929b87b0f 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -569,20 +569,38 @@ mod tests { assert_eq_metta_results!(run_program(program), Ok(vec![vec![expr!("a")]])); } + static ID_NUM: &Operation = &Operation{ + name: "id_num", + execute: |_, args| { + let arg_error = || ExecError::from("id_num expects one argument: number"); + let num = args.get(0).ok_or_else(arg_error)?; + Ok(vec![num.clone()]) + }, + typ: "(-> Number Number)", + }; + #[test] fn test_return_bad_type_error() { - let program = " + let program1 = " (: myAtom myType) (: id_a (-> A A)) (= (id_a $a) $a) !(eval (interpret (id_a myAtom) %Undefined% &self)) - ;!(id_num myatom) "; - assert_eq!(run_program(program), + let metta = new_metta_rust(); + metta.tokenizer().borrow_mut().register_token(Regex::new("id_num").unwrap(), + |_| Atom::gnd(ID_NUM)); + + assert_eq!(metta.run(&mut SExprParser::new(program1)), + Ok(vec![vec![expr!("Error" "myAtom" "BadType")]])); + + let program2 = " + !(eval (interpret (id_num myAtom) %Undefined% &self)) + "; + + assert_eq!(metta.run(&mut SExprParser::new(program2)), Ok(vec![vec![expr!("Error" "myAtom" "BadType")]])); - //assert_eq!(interpret(&space, &expr!({ID_NUM} "myAtom")), - //Ok(vec![Atom::expr([ERROR_SYMBOL, sym!("myAtom"), BAD_TYPE_SYMBOL])])); } } From b7cac6f08fa325c4df689258ee6e7ec646174312 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 8 Sep 2023 14:38:45 +0300 Subject: [PATCH 03/27] Add feature to enable minimal MeTTa during compilation --- lib/Cargo.toml | 3 +++ lib/src/metta/interpreter.rs | 1 + lib/src/metta/mod.rs | 1 + lib/src/metta/runner/mod.rs | 29 ++++++++++++++++++++++++----- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 2511626a5..fd9b3eb35 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,3 +16,6 @@ smallvec = "1.10.0" name = "hyperon" path = "src/lib.rs" crate-type = ["lib"] + +[features] +minimal = [] diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index f26b893ad..d6ff5658b 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -88,6 +88,7 @@ pub struct InterpreterState<'a, T: SpaceRef<'a>> { impl<'a, T: SpaceRef<'a>> InterpreterState<'a, T> { /// INTERNAL USE ONLY. Create an InterpreterState that is ready to yield results + #[cfg(not(feature = "minimal"))] pub(crate) fn new_finished(_space: T, results: Vec) -> Self { Self { step_result: StepResult::Return(results.into_iter().map(|atom| InterpretedAtom(atom, Bindings::new())).collect()), diff --git a/lib/src/metta/mod.rs b/lib/src/metta/mod.rs index 29090cbd1..967ea6ba1 100644 --- a/lib/src/metta/mod.rs +++ b/lib/src/metta/mod.rs @@ -2,6 +2,7 @@ pub mod text; pub mod interpreter; +#[cfg(feature = "minimal")] pub mod interpreter2; pub mod types; pub mod runner; diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index 7a1ecf1dd..e7539c9ef 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -10,15 +10,20 @@ use std::rc::Rc; use std::path::PathBuf; use std::collections::HashMap; + +#[cfg(not(feature = "minimal"))] pub mod stdlib; +#[cfg(not(feature = "minimal"))] use super::interpreter::{interpret, interpret_init, interpret_step, InterpreterState}; +#[cfg(not(feature = "minimal"))] use stdlib::*; -// Uncomment three lines below and comment three lines above to -// switch to the minimal MeTTa version -//pub mod stdlib2; -//use super::interpreter2::{interpret, interpret_init, interpret_step, InterpreterState}; -//use stdlib2::*; +#[cfg(feature = "minimal")] +pub mod stdlib2; +#[cfg(feature = "minimal")] +use super::interpreter2::{interpret, interpret_init, interpret_step, InterpreterState}; +#[cfg(feature = "minimal")] +use stdlib2::*; mod arithmetics; @@ -219,6 +224,8 @@ impl Metta { InterpreterState::new_finished(self.space().clone(), vec![atom]) }, Ok(atom) => { + #[cfg(feature = "minimal")] + let atom = wrap_atom_by_metta_interpreter(self, atom); interpret_init(self.space().clone(), &atom) }, }); @@ -236,7 +243,11 @@ impl Metta { Ok(()) } + // TODO: this method is deprecated and should be removed after switching + // to the minimal MeTTa pub fn evaluate_atom(&self, atom: Atom) -> Result, String> { + #[cfg(feature = "minimal")] + let atom = wrap_atom_by_metta_interpreter(self, atom); match self.type_check(atom) { Err(atom) => Ok(vec![atom]), Ok(atom) => interpret(self.space(), &atom), @@ -260,6 +271,14 @@ impl Metta { } +#[cfg(feature = "minimal")] +fn wrap_atom_by_metta_interpreter(runner: &Metta, atom: Atom) -> Atom { + let space = Atom::gnd(runner.space().clone()); + let interpret = Atom::expr([Atom::sym("interpret"), atom, ATOM_TYPE_UNDEFINED, space]); + let eval = Atom::expr([EVAL_SYMBOL, interpret]); + eval +} + impl<'a> RunnerState<'a> { fn new() -> Self { Self { From 0de5ab6782a5fabddb993e438bddabe8e134de91 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 10 Aug 2023 11:45:37 +0300 Subject: [PATCH 04/27] Rename match to unify to eliminate clash with current MeTTa code --- doc/minimal-metta.md | 40 ++++++++++++++++----------------- lib/src/metta/interpreter2.rs | 26 ++++++++++----------- lib/src/metta/mod.rs | 2 +- lib/src/metta/runner/stdlib2.rs | 30 ++++++++++++++++++------- 4 files changed, 56 insertions(+), 42 deletions(-) diff --git a/doc/minimal-metta.md b/doc/minimal-metta.md index a6b339b34..09b29a7db 100644 --- a/doc/minimal-metta.md +++ b/doc/minimal-metta.md @@ -89,9 +89,9 @@ as a first argument: nested in such a case only the most nested `chain` instruction is executed during the interpretation step. -## match +## unify -Conditioning on the results can be done using `match` operation `(match +Conditioning on the results can be done using `unify` operation `(unify )`. This operation matches `` with a ``. If match is successful then it returns `` atom and merges bindings of the original `` to resulting variable bindings. If matching is not successful @@ -117,22 +117,22 @@ Switch implementation: (= (switch $atom $cases) (chain (decons $cases) $list (eval (switch-internal $atom $list)))) (= (switch-internal $atom (($pattern $template) $tail)) - (match $atom $pattern $template (eval (switch $atom $tail)))) + (unify $atom $pattern $template (eval (switch $atom $tail)))) ``` Reduce in loop until result is calculated: ```metta (= (subst $atom $var $templ) - (match $atom $var $templ + (unify $atom $var $templ (Error (subst $atom $var $templ) \"subst expects a variable as a second argument\") )) (= (reduce $atom $var $templ) (chain (eval $atom) $res - (match $res (Error $a $m) + (unify $res (Error $a $m) (Error $a $m) - (match $res Empty + (unify $res Empty (eval (subst $atom $var $templ)) (eval (reduce $res $var $templ)) )))) ``` @@ -150,11 +150,11 @@ instruction set (the full code of the example can be found ```metta (= (tm $rule $state $tape) - (match $state HALT + (unify $state HALT $tape (chain (eval (read $tape)) $char (chain (eval ($rule $state $char)) $res - (match $res ($next-state $next-char $dir) + (unify $res ($next-state $next-char $dir) (chain (eval (move $tape $next-char $dir)) $next-tape (eval (tm $rule $next-state $next-tape)) ) (Error (tm $rule $state $tape) \"Incorrect state\") ))))) @@ -165,13 +165,13 @@ instruction set (the full code of the example can be found (= (move ($head $hole $tail) $char L) (chain (cons $char $head) $next-head (chain (decons $tail) $list - (match $list ($next-hole $next-tail) + (unify $list ($next-hole $next-tail) ($next-head $next-hole $next-tail) ($next-head 0 ()) )))) (= (move ($head $hole $tail) $char R) (chain (cons $char $tail) $next-tail (chain (decons $head) $list - (match $list ($next-hole $next-head) + (unify $list ($next-hole $next-head) ($next-head $next-hole $next-tail) (() 0 $next-tail) )))) ``` @@ -181,7 +181,7 @@ instruction set (the full code of the example can be found One difference from MOPS [1] is that the minimal instruction set allows relatively easy write deterministic programs and non-determinism is injected only via matching and evaluation. `Query` and `Chain` from MOPS are very -similar to `eval`. `Transform` is very similar to `match`. `chain` has no +similar to `eval`. `Transform` is very similar to `unify`. `chain` has no analogue in MOPS, it is used to make deterministic computations. `cons`/`decons` to some extent are analogues of `AtomAdd`/`AtomRemove` in a sense that they can be used to change the state. @@ -204,11 +204,11 @@ The version of the `reduce` written using `return` will look like the following: ```metta (= (reduce $atom $var $templ) (chain $atom $res - (match $res (return (Error $a $m)) + (unify $res (return (Error $a $m)) (return (Error $a $m)) - (match $res (return Empty) + (unify $res (return Empty) (subst $atom $var $templ) - (match $res (return $val) + (unify $res (return $val) (subst $val $var $templ) (reduce $res $var $templ) ))))) ``` @@ -224,9 +224,9 @@ into the template. It can make program even more compact: ```metta (= (reduce $atom $var $templ) (chain $atom $res - (match $res (Error $a $m) + (unify $res (Error $a $m) (Error $a $m) - (match $res Empty + (unify $res Empty (subst $atom $var $templ) (reduce $res $var $templ) )))) ``` @@ -245,7 +245,7 @@ Each instruction in a minimal instruction set is a complete function. Nevertheless `Empty` allows defining partial functions in MeTTa. For example partial `if` can be defined as follows: ```metta -(= (if $condition $then) (match $condition True $then Empty)) +(= (if $condition $then) (unify $condition True $then Empty)) ``` # Future work @@ -261,11 +261,11 @@ Making atomspace out of implicit context could make import semantics more straightforward. In the current implementation of the minimal instruction set it was needed to explicitly pass the atomspace to the interpreter because otherwise grounded `get-type` function didn't work properly.It also could allow -defining `eval` via `match` which minimizes the number of instructions and +defining `eval` via `unify` which minimizes the number of instructions and allows defining `eval` in a MeTTa program itself. Which in turn allows defining different versions of `eval` to program different kinds of chaining. -Nevertheless defining `eval` through `match` requires rework of the grounded -functions interface to allow calling them by executing `match` instructions. +Nevertheless defining `eval` through `unify` requires rework of the grounded +functions interface to allow calling them by executing `unify` instructions. Which is an interesting direction to follow. ## Scope of variables diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index 8545cc1ce..df3c47fb4 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -187,7 +187,7 @@ fn is_embedded_op(atom: &Atom) -> bool { match expr { Some([op, ..]) => *op == EVAL_SYMBOL || *op == CHAIN_SYMBOL - || *op == MATCH_SYMBOL + || *op == UNIFY_SYMBOL || *op == CONS_SYMBOL || *op == DECONS_SYMBOL, _ => false, @@ -228,11 +228,11 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre }, } }, - Some([op, args @ ..]) if *op == MATCH_SYMBOL => { + Some([op, args @ ..]) if *op == UNIFY_SYMBOL => { match args { [atom, pattern, then, else_] => match_(bindings, atom, pattern, then, else_), _ => { - let error: String = format!("expected: ({} ), found: {}", MATCH_SYMBOL, atom); + let error: String = format!("expected: ({} ), found: {}", UNIFY_SYMBOL, atom); vec![InterpretedAtom(error_atom(atom, error), bindings)] }, } @@ -560,21 +560,21 @@ mod tests { #[test] fn interpret_atom_match_incorrect_args() { - assert_eq!(interpret_atom(&space(""), atom("(match a p t e o)", bind!{})), - vec![InterpretedAtom(expr!("Error" ("match" "a" "p" "t" "e" "o") "expected: (match ), found: (match a p t e o)"), bind!{})]); - assert_eq!(interpret_atom(&space(""), atom("(match a p t)", bind!{})), - vec![InterpretedAtom(expr!("Error" ("match" "a" "p" "t") "expected: (match ), found: (match a p t)"), bind!{})]); + assert_eq!(interpret_atom(&space(""), atom("(unify a p t e o)", bind!{})), + vec![InterpretedAtom(expr!("Error" ("unify" "a" "p" "t" "e" "o") "expected: (unify ), found: (unify a p t e o)"), bind!{})]); + assert_eq!(interpret_atom(&space(""), atom("(unify a p t)", bind!{})), + vec![InterpretedAtom(expr!("Error" ("unify" "a" "p" "t") "expected: (unify ), found: (unify a p t)"), bind!{})]); } #[test] fn interpret_atom_match_then() { - let result = interpret_atom(&space(""), atom("(match (A $b) ($a B) ($a $b) Empty)", bind!{})); + let result = interpret_atom(&space(""), atom("(unify (A $b) ($a B) ($a $b) Empty)", bind!{})); assert_eq!(result, vec![atom("(A B)", bind!{})]); } #[test] fn interpret_atom_match_else() { - let result = interpret_atom(&space(""), atom("(match (A $b C) ($a B D) ($a $b) Empty)", bind!{})); + let result = interpret_atom(&space(""), atom("(unify (A $b C) ($a B D) ($a $b) Empty)", bind!{})); assert_eq!(result, vec![atom("Empty", bind!{})]); } @@ -640,11 +640,11 @@ mod tests { fn metta_turing_machine() { let space = space(" (= (tm $rule $state $tape) - (match $state HALT + (unify $state HALT $tape (chain (eval (read $tape)) $char (chain (eval ($rule $state $char)) $res - (match $res ($next-state $next-char $dir) + (unify $res ($next-state $next-char $dir) (chain (eval (move $tape $next-char $dir)) $next-tape (eval (tm $rule $next-state $next-tape)) ) (Error (tm $rule $state $tape) \"Incorrect state\") ))))) @@ -655,13 +655,13 @@ mod tests { (= (move ($head $hole $tail) $char L) (chain (cons $char $head) $next-head (chain (decons $tail) $list - (match $list ($next-hole $next-tail) + (unify $list ($next-hole $next-tail) ($next-head $next-hole $next-tail) ($next-head 0 ()) )))) (= (move ($head $hole $tail) $char R) (chain (cons $char $tail) $next-tail (chain (decons $head) $list - (match $list ($next-hole $next-head) + (unify $list ($next-hole $next-head) ($next-head $next-hole $next-tail) (() 0 $next-tail) )))) diff --git a/lib/src/metta/mod.rs b/lib/src/metta/mod.rs index 967ea6ba1..d0170babb 100644 --- a/lib/src/metta/mod.rs +++ b/lib/src/metta/mod.rs @@ -35,7 +35,7 @@ pub const NO_VALID_ALTERNATIVES : Atom = sym!("NoValidAlternatives"); pub const EMPTY_SYMBOL : Atom = sym!("Empty"); pub const EVAL_SYMBOL : Atom = sym!("eval"); pub const CHAIN_SYMBOL : Atom = sym!("chain"); -pub const MATCH_SYMBOL : Atom = sym!("match"); +pub const UNIFY_SYMBOL : Atom = sym!("unify"); pub const DECONS_SYMBOL : Atom = sym!("decons"); pub const CONS_SYMBOL : Atom = sym!("cons"); diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 929b87b0f..cebd172e5 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -174,7 +174,7 @@ pub static METTA_CODE: &'static str = " (= (if-decons $atom $head $tail $then $else) (eval (if-non-empty-expression $atom (chain (decons $atom) $list - (match $list ($head $tail) $then $else) ) + (unify $list ($head $tail) $then $else) ) $else ))) (= (if-empty $atom $then $else) @@ -198,10 +198,13 @@ pub static METTA_CODE: &'static str = " (= (switch $atom $cases) (chain (decons $cases) $list (eval (switch-internal $atom $list)))) (= (switch-internal $atom (($pattern $template) $tail)) - (match $atom $pattern $template (eval (switch $atom $tail)))) + (unify $atom $pattern $template (eval (switch $atom $tail)))) + +; FIXME: subst and reduce are not used in interpreter implementation +; we could remove them (= (subst $atom $var $templ) - (match $atom $var $templ + (unify $atom $var $templ (Error (subst $atom $var $templ) \"subst expects a variable as a second argument\") )) @@ -212,6 +215,10 @@ pub static METTA_CODE: &'static str = " (eval (subst $atom $var $templ)) (eval (reduce $res $var $templ)) )))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; MeTTa interpreter implementation ; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (= (type-cast $atom $type $space) (chain (eval (get-type $atom $space)) $actual-type (eval (switch ($actual-type $type) @@ -227,7 +234,7 @@ pub static METTA_CODE: &'static str = " ( (($_ Expression) (chain (eval (car $type)) $head - (match $head -> True False) )) + (unify $head -> True False) )) ($_ False) ))))) (= (interpret $atom $type $space) @@ -246,7 +253,7 @@ pub static METTA_CODE: &'static str = " (eval (if-decons $atom $op $args (chain (eval (get-type $op $space)) $op-type (chain (eval (is-function $op-type)) $is-func - (match $is-func True + (unify $is-func True (chain (eval (interpret-func $atom $op-type $space)) $reduced-atom (eval (call $reduced-atom $type $space)) ) (chain (eval (interpret-tuple $atom $space)) $reduced-atom @@ -265,8 +272,8 @@ pub static METTA_CODE: &'static str = " (Error $expr \"Non-empty expression atom is expected\") ))) (= (interpret-args $atom $args $arg-types $space) - (match $args () - (match $arg-types ($ret) () (Error $atom BadType)) + (unify $args () + (unify $arg-types ($ret) () (Error $atom BadType)) (eval (if-decons $args $head $tail (eval (if-decons $arg-types $head-type $tail-types (chain (eval (interpret $head $head-type $space)) $reduced-head @@ -286,7 +293,7 @@ pub static METTA_CODE: &'static str = " (cons $head $reduced-tail) )))) (= (interpret-tuple $atom $space) - (match $atom () + (unify $atom () $atom (eval (if-decons $atom $head $tail (chain (eval (interpret $head %Undefined% $space)) $rhead @@ -300,6 +307,13 @@ pub static METTA_CODE: &'static str = " (eval (if-error $result $result (eval (interpret $result $type $space)) )))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Standard library written in MeTTa ; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(: match (-> Atom Atom Atom %Undefined%)) +(= (match $space $pattern $template) + (unify $pattern $space $template Empty)) "; #[cfg(test)] From f08949e56b980dad272f1af36659a97b666bdf0b Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 10 Aug 2023 11:48:51 +0300 Subject: [PATCH 05/27] Add hacky Assert...Op implementation This implementation calls interpret() function inside directly which is not a minimal MeTTa way. The proper way is to implement it in MeTTa using pure interpret() and collapse() function. Less proper way is to return `(interpret $atom %Undefined% )` from grounded Assert...Op operation. But current implementation is added to quickly migrate to the minimal MeTTa interpreter. --- lib/src/metta/mod.rs | 2 + lib/src/metta/runner/stdlib2.rs | 151 +++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/lib/src/metta/mod.rs b/lib/src/metta/mod.rs index d0170babb..94a82e634 100644 --- a/lib/src/metta/mod.rs +++ b/lib/src/metta/mod.rs @@ -33,6 +33,8 @@ pub const NOT_REDUCIBLE_SYMBOL : Atom = sym!("NotReducible"); pub const NO_VALID_ALTERNATIVES : Atom = sym!("NoValidAlternatives"); pub const EMPTY_SYMBOL : Atom = sym!("Empty"); +pub const VOID_SYMBOL : Atom = sym!("Void"); + pub const EVAL_SYMBOL : Atom = sym!("eval"); pub const CHAIN_SYMBOL : Atom = sym!("chain"); pub const UNIFY_SYMBOL : Atom = sym!("unify"); diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index cebd172e5..ca04a88f3 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -5,9 +5,12 @@ use crate::metta::*; use crate::metta::text::Tokenizer; use crate::metta::runner::Metta; use crate::metta::types::{get_atom_types, get_meta_type}; +use crate::common::assert::vec_eq_no_order; +use crate::metta::runner::interpret; use std::fmt::Display; use regex::Regex; +use std::convert::TryInto; use super::arithmetics::*; @@ -97,6 +100,105 @@ impl Grounded for IfEqualOp { } } +// TODO: remove hiding errors completely after making it possible passing +// them to the user +fn interpret_no_error(space: DynSpace, expr: &Atom) -> Result, String> { + let expr = Atom::expr([EVAL_SYMBOL, Atom::expr([Atom::sym("interpret"), expr.clone(), ATOM_TYPE_UNDEFINED, Atom::gnd(space.clone())])]); + let result = interpret(space, &expr); + log::debug!("interpret_no_error: interpretation expr: {}, result {:?}", expr, result); + match result { + Ok(result) => Ok(result), + Err(_) => Ok(vec![]), + } +} + +fn assert_results_equal(actual: &Vec, expected: &Vec, atom: &Atom) -> Result, ExecError> { + log::debug!("assert_results_equal: actual: {:?}, expected: {:?}, actual atom: {:?}", actual, expected, atom); + let report = format!("\nExpected: {:?}\nGot: {:?}", expected, actual); + match vec_eq_no_order(actual.iter(), expected.iter()) { + Ok(()) => Ok(vec![VOID_SYMBOL]), + Err(diff) => Err(ExecError::Runtime(format!("{}\n{}", report, diff))) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct AssertEqualOp { + space: DynSpace, +} + +impl AssertEqualOp { + pub fn new(space: DynSpace) -> Self { + Self{ space } + } +} + +impl Display for AssertEqualOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "assertEqual") + } +} + +impl Grounded for AssertEqualOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + log::debug!("AssertEqualOp::execute: {:?}", args); + let arg_error = || ExecError::from("assertEqual expects two atoms as arguments: actual and expected"); + let actual_atom = args.get(0).ok_or_else(arg_error)?; + let expected_atom = args.get(1).ok_or_else(arg_error)?; + + let actual = interpret_no_error(self.space.clone(), actual_atom)?; + let expected = interpret_no_error(self.space.clone(), expected_atom)?; + + assert_results_equal(&actual, &expected, actual_atom) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct AssertEqualToResultOp { + space: DynSpace, +} + +impl AssertEqualToResultOp { + pub fn new(space: DynSpace) -> Self { + Self{ space } + } +} + +impl Display for AssertEqualToResultOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "assertEqualToResult") + } +} + +impl Grounded for AssertEqualToResultOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + log::debug!("AssertEqualToResultOp::execute: {:?}", args); + let arg_error = || ExecError::from("assertEqualToResult expects two atoms as arguments: actual and expected"); + let actual_atom = args.get(0).ok_or_else(arg_error)?; + let expected = TryInto::<&ExpressionAtom>::try_into(args.get(1).ok_or_else(arg_error)?) + .map_err(|_| arg_error())? + .children(); + + let actual = interpret_no_error(self.space.clone(), actual_atom)?; + + assert_results_equal(&actual, expected, actual_atom) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} fn regex(regex: &str) -> Regex { Regex::new(regex).unwrap() @@ -115,11 +217,15 @@ pub fn register_common_tokens(metta: &Metta) { } pub fn register_runner_tokens(metta: &Metta) { - let _space = metta.space(); + let space = metta.space(); let tokenizer = metta.tokenizer(); let mut tref = tokenizer.borrow_mut(); + let assert_equal_op = Atom::gnd(AssertEqualOp::new(space.clone())); + tref.register_token(regex(r"assertEqual"), move |_| { assert_equal_op.clone() }); + let assert_equal_to_result_op = Atom::gnd(AssertEqualToResultOp::new(space.clone())); + tref.register_token(regex(r"assertEqualToResult"), move |_| { assert_equal_to_result_op.clone() }); // &self should be updated // TODO: adding &self might be done not by stdlib, but by MeTTa itself. // TODO: adding &self introduces self referencing and thus prevents space @@ -480,6 +586,49 @@ mod tests { assert_eq!(run_program("!(eval (interpret () SomeType &self))"), Ok(vec![vec![expr!(())]])); } + #[test] + fn metta_assert_equal_op() { + let metta = new_metta_rust(); + let assert = AssertEqualOp::new(metta.space().clone()); + let program = " + (= (foo $x) $x) + (= (bar $x) $x) + "; + assert_eq!(metta.run(&mut SExprParser::new(program)), Ok(vec![])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqual (foo A) (bar A))")), Ok(vec![ + vec![VOID_SYMBOL], + ])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqual (foo A) (bar B))")), Ok(vec![ + vec![expr!("Error" ({assert.clone()} ("foo" "A") ("bar" "B")) "\nExpected: [B]\nGot: [A]\nMissed result: B")], + ])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqual (foo A) Empty)")), Ok(vec![ + vec![expr!("Error" ({assert.clone()} ("foo" "A") "Empty") "\nExpected: []\nGot: [A]\nExcessive result: A")] + ])); + } + + #[test] + fn metta_assert_equal_to_result_op() { + let metta = new_metta_rust(); + let assert = AssertEqualToResultOp::new(metta.space().clone()); + let program = " + (= (foo) A) + (= (foo) B) + (= (bar) C) + (= (baz) D) + (= (baz) D) + "; + assert_eq!(metta.run(&mut SExprParser::new(program)), Ok(vec![])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqualToResult (foo) (A B))")), Ok(vec![ + vec![VOID_SYMBOL], + ])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqualToResult (bar) (A))")), Ok(vec![ + vec![expr!("Error" ({assert.clone()} ("bar") ("A")) "\nExpected: [A]\nGot: [C]\nMissed result: A")], + ])); + assert_eq!(metta.run(&mut SExprParser::new("!(assertEqualToResult (baz) (D))")), Ok(vec![ + vec![expr!("Error" ({assert.clone()} ("baz") ("D")) "\nExpected: [D]\nGot: [D, D]\nExcessive result: D")] + ])); + } + #[test] fn test_frog_reasoning() { From 664d15ac87005a74bb3ee2183c31cccdf7986875 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 10 Aug 2023 12:06:43 +0300 Subject: [PATCH 06/27] Check for Void returned by Assert...Op instead of empty result Add standard atoms collection into C API and use it in Python API to check the equality with predefined atom. --- c/src/metta.rs | 8 ++++++++ python/hyperon/atoms.py | 4 ++++ python/hyperonpy.cpp | 6 ++++++ python/tests/test_run_metta.py | 4 ++-- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/c/src/metta.rs b/c/src/metta.rs index b26066460..186b07018 100644 --- a/c/src/metta.rs +++ b/c/src/metta.rs @@ -275,6 +275,14 @@ pub extern "C" fn sexpr_parser_parse( /// #[no_mangle] pub extern "C" fn ATOM_TYPE_GROUNDED_SPACE() -> atom_t { rust_type_atom::().into() } +/// @brief Creates a Symbol atom for the special MeTTa symbol used to indicate empty results in +/// case expressions. +/// @ingroup metta_language_group +/// @return The `atom_t` representing the Void atom +/// @note The returned `atom_t` must be freed with `atom_free()` +/// +#[no_mangle] pub extern "C" fn VOID_SYMBOL() -> atom_t { hyperon::metta::VOID_SYMBOL.into() } + /// @brief Checks whether Atom `atom` has Type `typ` in context of `space` /// @ingroup metta_language_group /// @param[in] space A pointer to the `space_t` representing the space context in which to perform the check diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 68cf13caa..7a8786fe1 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -91,6 +91,10 @@ class AtomType: GROUNDED = Atom._from_catom(hp.CAtomType.GROUNDED) GROUNDED_SPACE = Atom._from_catom(hp.CAtomType.GROUNDED_SPACE) +class Atoms: + + VOID = Atom._from_catom(hp.CAtoms.VOID) + class GroundedAtom(Atom): def __init__(self, catom): diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index 35a60f738..392214288 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -426,6 +426,7 @@ struct CSExprParser { }; struct CAtomType {}; +struct CAtoms {}; PYBIND11_MODULE(hyperonpy, m) { m.doc() = "Python API of the Hyperon library"; @@ -690,6 +691,11 @@ PYBIND11_MODULE(hyperonpy, m) { return atoms; }, "Get types of the given atom"); +#define ADD_SYMBOL(t, d) .def_property_readonly_static(#t, [](py::object) { return CAtom(t ## _SYMBOL()); }, d " atom type") + + py::class_(m, "CAtoms") + ADD_SYMBOL(VOID, "Void"); + py::class_(m, "CMetta").def(py::init(&cmetta_from_inner_ptr_as_int)); m.def("metta_new", [](CSpace space, CTokenizer tokenizer, char const* cwd) { return CMetta(metta_new(space.ptr(), tokenizer.ptr(), cwd)); diff --git a/python/tests/test_run_metta.py b/python/tests/test_run_metta.py index 9bffb4981..5361559d3 100644 --- a/python/tests/test_run_metta.py +++ b/python/tests/test_run_metta.py @@ -1,6 +1,6 @@ import unittest -from hyperon import MeTTa +from hyperon import MeTTa, Atoms from test_common import HyperonTestCase from pathlib import Path @@ -72,7 +72,7 @@ def test_comments(self): def process_exceptions(self, results): for result in results: - self.assertEqual(result, []) + self.assertEqual(result, [Atoms.VOID]) def test_scripts(self): self.process_exceptions(MeTTa().import_file(f"{pwd}/scripts/a1_symbols.metta")) From 612ccde6eb702941195a7d3bac1a8706e67153cb Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 10 Aug 2023 12:42:00 +0300 Subject: [PATCH 07/27] Properly handle empty result for the function call and tuple call In case of function call empty result means the function is not defined on the passed arguments. Thus the whole branch is removed from the plan. In case of tuple call empty result means there is no function which matches this tuple. Thus original tuple is returned as a result. --- lib/src/metta/runner/stdlib2.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index ca04a88f3..7b75055cc 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -361,9 +361,14 @@ pub static METTA_CODE: &'static str = " (chain (eval (is-function $op-type)) $is-func (unify $is-func True (chain (eval (interpret-func $atom $op-type $space)) $reduced-atom - (eval (call $reduced-atom $type $space)) ) + ; When function is called then empty result means it is not defined + ; on such arguments and thus should not be called. In such case + ; the whole branch is removed from interpreter plan. + (eval (call $reduced-atom $type $space Empty)) ) (chain (eval (interpret-tuple $atom $space)) $reduced-atom - (eval (call $reduced-atom $type $space)) )))) + ; When tuple is called then empty result means this tuple is not a + ; function call and should be returned as is. + (eval (call $reduced-atom $type $space $reduced-atom)) )))) (eval (type-cast $atom $type $space)) ))) (= (interpret-func $expr $type $space) @@ -403,15 +408,18 @@ pub static METTA_CODE: &'static str = " $atom (eval (if-decons $atom $head $tail (chain (eval (interpret $head %Undefined% $space)) $rhead - (chain (eval (interpret-tuple $tail $space)) $rtail - (cons $rhead $rtail) )) + (eval (if-empty $rhead Empty + (chain (eval (interpret-tuple $tail $space)) $rtail + (eval (if-empty $rtail Empty + (cons $rhead $rtail) )))))) (Error (interpret-tuple $atom $space) \"Non-empty expression atom is expected as an argument\") )))) -(= (call $atom $type $space) - (chain (eval $atom) $result - (eval (if-empty $result $atom - (eval (if-error $result $result - (eval (interpret $result $type $space)) )))))) +(= (call $atom $type $space $on-empty) + (eval (if-error $atom $atom + (chain (eval $atom) $result + (eval (if-empty $result $on-empty + (eval (if-error $result $result + (eval (interpret $result $type $space)) )))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Standard library written in MeTTa ; From 6d95a612472ec0b82986a825b8b4aec79ad7179e Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 10 Aug 2023 14:50:03 +0300 Subject: [PATCH 08/27] Add superpose, collapse and let into minimal MeTTa standard library --- lib/src/metta/runner/stdlib2.rs | 140 ++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 7b75055cc..10914a38b 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -200,6 +200,87 @@ impl Grounded for AssertEqualToResultOp { } } +#[derive(Clone, PartialEq, Debug)] +pub struct SuperposeOp { + space: DynSpace, +} + +impl SuperposeOp { + fn new(space: DynSpace) -> Self { + Self{ space } + } +} + +impl Display for SuperposeOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "superpose") + } +} + +impl Grounded for SuperposeOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_UNDEFINED]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("superpose expects single expression as an argument"); + let atom = args.get(0).ok_or_else(arg_error)?; + let expr = TryInto::<&ExpressionAtom>::try_into(atom).map_err(|_| arg_error())?; + + let mut superposed = Vec::new(); + for atom in expr.children() { + match interpret_no_error(self.space.clone(), atom) { + Ok(results) => { superposed.extend(results); }, + Err(message) => { return Err(format!("Error: {}", message).into()) }, + } + } + Ok(superposed) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct CollapseOp { + space: DynSpace, +} + +impl CollapseOp { + pub fn new(space: DynSpace) -> Self { + Self{ space } + } +} + +impl Display for CollapseOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "collapse") + } +} + +impl Grounded for CollapseOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("collapse expects single executable atom as an argument"); + let atom = args.get(0).ok_or_else(arg_error)?; + + // TODO: Calling interpreter inside the operation is not too good + // Could it be done via StepResult? + let result = interpret_no_error(self.space.clone(), atom)?; + + Ok(vec![Atom::expr(result)]) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} + + fn regex(regex: &str) -> Regex { Regex::new(regex).unwrap() } @@ -226,6 +307,10 @@ pub fn register_runner_tokens(metta: &Metta) { tref.register_token(regex(r"assertEqual"), move |_| { assert_equal_op.clone() }); let assert_equal_to_result_op = Atom::gnd(AssertEqualToResultOp::new(space.clone())); tref.register_token(regex(r"assertEqualToResult"), move |_| { assert_equal_to_result_op.clone() }); + let superpose_op = Atom::gnd(SuperposeOp::new(space.clone())); + tref.register_token(regex(r"superpose"), move |_| { superpose_op.clone() }); + let collapse_op = Atom::gnd(CollapseOp::new(space.clone())); + tref.register_token(regex(r"collapse"), move |_| { collapse_op.clone() }); // &self should be updated // TODO: adding &self might be done not by stdlib, but by MeTTa itself. // TODO: adding &self introduces self referencing and thus prevents space @@ -428,6 +513,10 @@ pub static METTA_CODE: &'static str = " (: match (-> Atom Atom Atom %Undefined%)) (= (match $space $pattern $template) (unify $pattern $space $template Empty)) + +(: let (-> Atom %Undefined% Atom Atom)) +(= (let $pattern $atom $template) + (unify $atom $pattern $template Empty)) "; #[cfg(test)] @@ -436,6 +525,8 @@ mod tests { use crate::metta::runner::new_metta_rust; use crate::matcher::atoms_are_equivalent; + use std::convert::TryFrom; + fn run_program(program: &str) -> Result>, String> { let metta = new_metta_rust(); metta.run(&mut SExprParser::new(program)) @@ -637,6 +728,55 @@ mod tests { ])); } + #[test] + fn metta_superpose() { + assert_eq_metta_results!(run_program("!(superpose (red yellow green))"), + Ok(vec![vec![expr!("red"), expr!("yellow"), expr!("green")]])); + let program = " + (= (foo) FOO) + (= (bar) BAR) + !(superpose ((foo) (bar) BAZ)) + "; + assert_eq_metta_results!(run_program(program), + Ok(vec![vec![expr!("FOO"), expr!("BAR"), expr!("BAZ")]])); + } + + #[test] + fn metta_collapse() { + let program = " + (= (color) red) + (= (color) green) + (= (color) blue) + !(collapse (color)) + "; + let result = run_program(program).expect("Successful result is expected"); + assert_eq!(result.len(), 1); + let result = result.get(0).unwrap(); + assert_eq!(result.len(), 1); + let result = result.get(0).unwrap(); + let actual = <&ExpressionAtom>::try_from(result) + .expect("Expression atom is expected").children(); + assert_eq_no_order!(actual, vec![expr!("red"), expr!("green"), expr!("blue")]); + } + + #[test] + fn metta_let() { + let result = run_program("!(let (P A $b) (P $a B) (P $b $a))"); + assert_eq!(result, Ok(vec![vec![expr!("P" "B" "A")]])); + let result = run_program(" + (= (foo) (P A B)) + !(let (P A $b) (foo) (P $b A)) + "); + assert_eq!(result, Ok(vec![vec![expr!("P" "B" "A")]])); + let result = run_program(" + (= (foo) (P A B)) + !(let (foo) (P A $b) (P $b A)) + "); + assert_eq!(result, Ok(vec![vec![]])); + let result = run_program("!(let (P A $b) (P B C) (P C B))"); + assert_eq!(result, Ok(vec![vec![]])); + } + #[test] fn test_frog_reasoning() { From 953c0f061cec2afb129becf0a6d50e094cae8cbb Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 18 Aug 2023 09:17:57 +0300 Subject: [PATCH 09/27] Add let* implementation into minimal MeTTa standard library --- lib/src/metta/runner/stdlib2.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 10914a38b..42aab5e13 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -517,6 +517,13 @@ pub static METTA_CODE: &'static str = " (: let (-> Atom %Undefined% Atom Atom)) (= (let $pattern $atom $template) (unify $atom $pattern $template Empty)) + +(: let* (-> Expression Atom Atom)) +(= (let* $pairs $template) + (eval (if-decons $pairs ($pattern $atom) $tail + (let $pattern $atom (let* $tail $template)) + $template ))) + "; #[cfg(test)] @@ -777,6 +784,18 @@ mod tests { assert_eq!(result, Ok(vec![vec![]])); } + #[test] + fn metta_let_var() { + let result = run_program("!(let* () result)"); + assert_eq!(result, Ok(vec![vec![expr!("result")]])); + let result = run_program("!(let* ( ((P A $b) (P $a B)) ) (P $b $a))"); + assert_eq!(result, Ok(vec![vec![expr!("P" "B" "A")]])); + let result = run_program("!(let* ( ((P $a) (P A)) ((P B) (P $b)) ) (P $b $a))"); + assert_eq!(result, Ok(vec![vec![expr!("P" "B" "A")]])); + let result = run_program("!(let* ( ((P $a) (P A)) ((P B) (P C)) ) (P $b $a))"); + assert_eq!(result, Ok(vec![vec![]])); + } + #[test] fn test_frog_reasoning() { From 00d73a48f279ad0de716bd29bbee70a53133d5d5 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 17 Aug 2023 21:20:00 +0300 Subject: [PATCH 10/27] Return NotReducible instead of Empty when function is not defined This corresponds to the core functionality implemented in a previous version of the interpreter. When function definition cannot be found the atom is returned as is. Two major use-cases are type constructors (which are usually have no definition only type declaration) and partially defined functions (which return results on some arguments and stay non-reducible on others). --- doc/minimal-metta.md | 38 +++++++++++++++++++---------- lib/src/metta/interpreter2.rs | 42 +++++++++++++++++++++------------ lib/src/metta/runner/stdlib2.rs | 34 +++++++++++++------------- 3 files changed, 70 insertions(+), 44 deletions(-) diff --git a/doc/minimal-metta.md b/doc/minimal-metta.md index 09b29a7db..d03ea9c2d 100644 --- a/doc/minimal-metta.md +++ b/doc/minimal-metta.md @@ -32,16 +32,25 @@ allowed developing the first stable version with less effort (see `eval` and `Return`). If an instruction returns the atom which is not from the minimal set it is not interpreted further and returned as a part of the final result. -## Error/Empty +## Error/Empty/NotReducible/Void There are atoms which can be returned to designate a special situation in a code: - `(Error )` means the interpretation is finished with error; - `Empty` means the corresponding branch of the evaluation returned no results, such result is not returned among other results when interpreting is - finished. - -Both these atoms are not interpreted further as they are not a part of the -minimal set of instructions. + finished; +- `NotReducible` can be returned by `eval` in order to designate the situation + when function can not be reduced further; for example it can happen when code + tries to call a type constructor (which has no definition), partially defined + function (with argument values which are not handled), or grounded function + which returns `NotReducible` explicitly; this atom is introduced to separate + the situations when atom should be returned "as is" from `Empty` when atom + should be removed from results; +- `Void` is a unit result which is mainly used by functions with side effects + which has no meaningful value to return. + +These atoms are not interpreted further as they are not a part of the minimal +set of instructions. ## eval @@ -58,15 +67,14 @@ the evaluation in this case. A grounded function can have side effects as well. In both cases bindings of the `eval`'s argument are merged to the bindings of the result. -Atomspace search can bring the list of results which can be empty. When search -returns no results then `Empty` atom is a result of the instruction. Grounded +Atomspace search can bring the list of results which is empty. When search +returns no results then `NotReducible` atom is a result of the instruction. Grounded function can return a list of atoms, empty result, `Error()` or `NoReduce` result. The result of the instruction for a special values are the following: -- empty result returns `Empty` atom; +- empty result returns `Void` atom; - `Error()` returns `(Error )` atom; -- `NoReduce` returns `` atom which is not evaluated further - because it is not an operation from the minimal set. +- `NoReduce` returns `NotReducible` atom. ## chain @@ -115,7 +123,9 @@ Switch implementation: ```metta (= (switch $atom $cases) - (chain (decons $cases) $list (eval (switch-internal $atom $list)))) + (chain (decons $cases) $list + (chain (eval (switch-internal $atom $list)) $res + (unify $res NotReducible Empty $res) ))) (= (switch-internal $atom (($pattern $template) $tail)) (unify $atom $pattern $template (eval (switch $atom $tail)))) ``` @@ -130,11 +140,13 @@ Reduce in loop until result is calculated: (= (reduce $atom $var $templ) (chain (eval $atom) $res + (unify $res Empty + Empty (unify $res (Error $a $m) (Error $a $m) - (unify $res Empty + (unify $res NotReducible (eval (subst $atom $var $templ)) - (eval (reduce $res $var $templ)) )))) + (eval (reduce $res $var $templ)) ))))) ``` [Link](https://github.com/trueagi-io/hyperon-experimental/blob/27861e63af1417df4780d9314eaf2e8a3b5cde06/lib/src/metta/runner/stdlib2.rs#L234-L302) diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index df3c47fb4..efaa04baa 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -278,8 +278,12 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre result } -fn return_empty() -> Atom { - EMPTY_SYMBOL +fn return_unit() -> Atom { + VOID_SYMBOL +} + +fn return_not_reducible() -> Atom { + NOT_REDUCIBLE_SYMBOL } fn error_atom(atom: Atom, err: String) -> Atom { @@ -296,7 +300,16 @@ fn eval<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec { if results.is_empty() { - vec![InterpretedAtom(return_empty(), bindings)] + // TODO: This is an open question how to interpret empty results + // which are returned by grounded function. There is no + // case to return empty result for now. If alternative + // should be remove from plan Empty is a proper result. + // If grounded atom returns no value Void should be returned. + // NotReducible or Exec::NoReduce can be returned to + // let a caller know that function is not defined on a + // passed input data. Thus we can interpreter empty result + // by any way we like. + vec![InterpretedAtom(return_unit(), bindings)] } else { results.into_iter() .map(|atom| InterpretedAtom(atom, bindings.clone())) @@ -305,11 +318,10 @@ fn eval<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec vec![InterpretedAtom(error_atom(atom, err), bindings)], - // TODO: NoReduce should also be available for processing - // on MeTTa code level, to allow override code behavior in - // case when grounded expression cannot be reduced. Err(ExecError::NoReduce) => - vec![InterpretedAtom(return_atom(atom), bindings)], + // TODO: we could remove ExecError::NoReduce and explicitly + // return NOT_REDUCIBLE_SYMBOL from the grounded function instead. + vec![InterpretedAtom(return_not_reducible(), bindings)], } }, _ => query(space, atom, bindings), @@ -321,7 +333,7 @@ fn query<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec Date: Fri, 18 Aug 2023 09:35:43 +0300 Subject: [PATCH 11/27] Fix runner unit tests after switching to minimal MeTTa --- lib/src/metta/runner/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index e7539c9ef..c418a2fb7 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -387,7 +387,7 @@ mod tests { !(foo) "; - let metta = Metta::new(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new())); + let metta = new_metta_rust(); metta.tokenizer().borrow_mut().register_token(Regex::new("error").unwrap(), |_| Atom::gnd(ErrorOp{})); let result = metta.run(&mut SExprParser::new(program)); @@ -438,7 +438,7 @@ mod tests { !(empty) "; - let metta = Metta::new(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new())); + let metta = new_metta_rust(); metta.tokenizer().borrow_mut().register_token(Regex::new("empty").unwrap(), |_| Atom::gnd(ReturnAtomOp(expr!()))); let result = metta.run(&mut SExprParser::new(program)); From 482e87ab5422204d3024b0d642beca3a2bc1856a Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 18 Aug 2023 13:13:22 +0300 Subject: [PATCH 12/27] Add suggestions for pattern matching syntax improvements --- doc/minimal-metta.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/minimal-metta.md b/doc/minimal-metta.md index d03ea9c2d..1461d3767 100644 --- a/doc/minimal-metta.md +++ b/doc/minimal-metta.md @@ -294,6 +294,32 @@ such possibility the additional instruction is needed. It could mark a set of results as joined and when their evaluation is finished would assemble them into an expression. +## Special matching syntax + +Sometimes it is convenient to change the semantics of the matching within a +pattern. Some real examples are provided below. One possible way to extend +matching syntax is embrace atoms by expressions with matching modifier on a +first position. For instance `(: )` could apply `` rule to +match the ``. How to eliminate interference of this syntax with symbol +atoms used by programmers is an open question. + +### Syntax to match atom by equality + +In many situations we need to check that atom is equal to some symbol. `unify` +doesn't work well in such cases because when checked atom is a variable it is +matched with anything (for instance `(unify $x Empty then else)` returns +`then`). It would be convenient to have a special syntax to match the atom by +equality. For instance `(unify (:= Empty) then else)` should match +`` with pattern only when `` is `Empty`. + +### Syntax to match part of the expression + +We could have a specific syntax which would allow matching part of the +expressions. For example such syntax could be used to match head and tail of +the expression without using `cons`/`decons`. Another example is matching part +of the expression with some gap, i.e. `(A ... D ...)` could match `(A B C D E)` +atom. + # Links 1. Lucius Gregory Meredith, Ben Goertzel, Jonathan Warrell, and Adam From 9b7e3bb133bf340baec324f704dfaef1259ed875 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 18 Aug 2023 13:31:25 +0300 Subject: [PATCH 13/27] Fix MeTTa test script b5_*.metta by adding incorrect type result --- python/tests/scripts/b5_types_prelim.metta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tests/scripts/b5_types_prelim.metta b/python/tests/scripts/b5_types_prelim.metta index 17b4fa0ae..1ae2c7648 100644 --- a/python/tests/scripts/b5_types_prelim.metta +++ b/python/tests/scripts/b5_types_prelim.metta @@ -55,7 +55,7 @@ ; The following will be accepted by the interpreter !(assertEqualToResult (Add Z Ten) - ((Add Z Ten))) + ((Add Z Ten) (Error Ten BadType))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; From 9c7e1660dbea32e5a512ea4dc67a8f0769b08bbd Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 18 Aug 2023 15:06:24 +0300 Subject: [PATCH 14/27] Fix type-checking logic for the function returned type Use if-equal to check equality of atoms instead of matching as matching doesn't work for variable types. Add return type checking step into interpret-args function. --- lib/src/metta/runner/stdlib2.rs | 86 +++++++++++++++------- python/tests/scripts/b5_types_prelim.metta | 2 +- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index dcf8ff4d3..75ef9c907 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -416,14 +416,14 @@ pub static METTA_CODE: &'static str = " ; MeTTa interpreter implementation ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(= (match-types $type1 $type2 $then $else) + (eval (if-equal $type1 %Undefined% $then + (eval (if-equal $type2 %Undefined% $then + (unify $type1 $type2 $then $else) ))))) + (= (type-cast $atom $type $space) (chain (eval (get-type $atom $space)) $actual-type - (eval (switch ($actual-type $type) - ( - ((%Undefined% $_) $atom) - (($_ %Undefined%) $atom) - (($type $_) $atom) - ($_ (Error $atom BadType)) ))))) + (eval (match-types $actual-type $type $atom (Error $atom BadType))) )) (= (is-function $type) (chain (eval (get-metatype $type)) $meta @@ -436,56 +436,56 @@ pub static METTA_CODE: &'static str = " (= (interpret $atom $type $space) (chain (eval (get-metatype $atom)) $meta - (eval (switch ($type $meta) - ( - ((Atom $_meta) $atom) - (($meta $meta) $atom) - (($_type Variable) $atom) - - (($_type Symbol) (eval (type-cast $atom $type $space))) - (($_type Grounded) (eval (type-cast $atom $type $space))) - (($_type Expression) (eval (interpret-expression $atom $type $space))) ))))) + (eval (if-equal $type Atom $atom + (eval (if-equal $type $meta $atom + (eval (switch ($type $meta) + ( + (($_type Variable) $atom) + (($_type Symbol) (eval (type-cast $atom $type $space))) + (($_type Grounded) (eval (type-cast $atom $type $space))) + (($_type Expression) (eval (interpret-expression $atom $type $space))) ))))))))) (= (interpret-expression $atom $type $space) (eval (if-decons $atom $op $args (chain (eval (get-type $op $space)) $op-type (chain (eval (is-function $op-type)) $is-func (unify $is-func True - (chain (eval (interpret-func $atom $op-type $space)) $reduced-atom + (chain (eval (interpret-func $atom $op-type $type $space)) $reduced-atom (eval (call $reduced-atom $type $space)) ) (chain (eval (interpret-tuple $atom $space)) $reduced-atom (eval (call $reduced-atom $type $space)) )))) (eval (type-cast $atom $type $space)) ))) -(= (interpret-func $expr $type $space) +(= (interpret-func $expr $type $ret-type $space) (eval (if-decons $expr $op $args (chain (eval (interpret $op $type $space)) $reduced-op (eval (return-on-error $reduced-op (eval (if-decons $type $arrow $arg-types - (chain (eval (interpret-args $expr $args $arg-types $space)) $reduced-args + (chain (eval (interpret-args $expr $args $arg-types $ret-type $space)) $reduced-args (eval (return-on-error $reduced-args (cons $reduced-op $reduced-args) ))) (Error $type \"Function type expected\") ))))) (Error $expr \"Non-empty expression atom is expected\") ))) -(= (interpret-args $atom $args $arg-types $space) +(= (interpret-args $atom $args $arg-types $ret-type $space) (unify $args () - (unify $arg-types ($ret) () (Error $atom BadType)) + (chain (eval (car $arg-types)) $actual-ret-type + (eval (match-types $actual-ret-type $ret-type () (Error $atom BadType)))) (eval (if-decons $args $head $tail (eval (if-decons $arg-types $head-type $tail-types (chain (eval (interpret $head $head-type $space)) $reduced-head ; check that head was changed otherwise Error or Empty in the head ; can be just an argument which is passed by intention (eval (if-equal $reduced-head $head - (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $space)) + (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $ret-type $space)) (eval (return-on-error $reduced-head - (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $space)) ))))) + (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $ret-type $space)) ))))) (Error $atom BadType) )) (Error (interpret-atom $atom $args $arg-types $space) \"Non-empty expression atom is expected\") )))) -(= (interpret-args-tail $atom $head $args-tail $args-tail-types $space) - (chain (eval (interpret-args $atom $args-tail $args-tail-types $space)) $reduced-tail +(= (interpret-args-tail $atom $head $args-tail $args-tail-types $ret-type $space) + (chain (eval (interpret-args $atom $args-tail $args-tail-types $ret-type $space)) $reduced-tail (eval (return-on-error $reduced-tail (cons $head $reduced-tail) )))) @@ -675,7 +675,43 @@ mod tests { } #[test] - fn metta_interpret_children() { + fn metta_interpret_single_atom_as_variable_type() { + let result = run_program(" + (: S Int) + !(chain (eval (interpret S $t &self)) $res (: $res $t)) + "); + assert_eq!(result, Ok(vec![vec![expr!(":" "S" "Int")]])); + } + + #[test] + fn metta_interpret_func() { + let result = run_program(" + (: a T) + (: foo (-> T T)) + (= (foo $x) $x) + (= (bar $x) $x) + !(eval (interpret (foo (bar a)) %Undefined% &self)) + "); + assert_eq!(result, Ok(vec![vec![expr!("a")]])); + let result = run_program(" + (: b B) + (: foo (-> T T)) + (= (foo $x) $x) + !(eval (interpret (foo b) %Undefined% &self)) + "); + assert_eq!(result, Ok(vec![vec![expr!("Error" "b" "BadType")]])); + let result = run_program(" + (: Nil (List $t)) + (: Z Nat) + (: S (-> Nat Nat)) + (: Cons (-> $t (List $t) (List $t))) + !(eval (interpret (Cons S (Cons Z Nil)) %Undefined% &self)) + "); + assert_eq!(result, Ok(vec![vec![expr!("Error" ("Cons" "Z" "Nil") "BadType")]])); + } + + #[test] + fn metta_interpret_tuple() { assert_eq!(run_program("!(eval (interpret-tuple () &self))"), Ok(vec![vec![expr!(())]])); assert_eq!(run_program("!(eval (interpret-tuple (a) &self))"), Ok(vec![vec![expr!(("a"))]])); assert_eq!(run_program("!(eval (interpret-tuple (a b) &self))"), Ok(vec![vec![expr!(("a" "b"))]])); diff --git a/python/tests/scripts/b5_types_prelim.metta b/python/tests/scripts/b5_types_prelim.metta index 1ae2c7648..b912436d3 100644 --- a/python/tests/scripts/b5_types_prelim.metta +++ b/python/tests/scripts/b5_types_prelim.metta @@ -84,7 +84,7 @@ ; This list is badly typed, because S and Z are not the same type !(assertEqualToResult (Cons S (Cons Z Nil)) - ((Error Z BadType))) + ((Error (Cons Z Nil) BadType))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; From ac3257fc06e3f4ca6eecc2c94e0c407db59a80ae Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 21 Aug 2023 16:17:35 +0300 Subject: [PATCH 15/27] Fix test, don't add space atom into itself --- python/tests/test_custom_space.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tests/test_custom_space.py b/python/tests/test_custom_space.py index b16321465..46b85d97c 100644 --- a/python/tests/test_custom_space.py +++ b/python/tests/test_custom_space.py @@ -132,7 +132,6 @@ def test_match_nested_custom_space(self): space_atom = G(nested) runner = MeTTa() - runner.space().add_atom(space_atom) runner.tokenizer().register_token("nested", lambda token: space_atom) result = runner.run("!(match nested (A $x) $x)") From 9554813d6124bb4fade110086788067bbecc4b73 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 21 Aug 2023 17:48:04 +0300 Subject: [PATCH 16/27] Fix GetTypeOp and SuperposeOp after changing semantics of results Now empty vector returned from GroundedAtom means value of the unit type Void. Thus returning empty collection should be interpreted as Empty. --- lib/src/metta/runner/stdlib2.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 75ef9c907..1de745ba4 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -33,7 +33,12 @@ impl Grounded for GetTypeOp { let atom = args.get(0).ok_or_else(arg_error)?; let space = args.get(1).ok_or_else(arg_error)?; let space = Atom::as_gnd::(space).ok_or("match expects a space as the first argument")?; - Ok(get_atom_types(space, atom)) + let types = get_atom_types(space, atom); + if types.is_empty() { + Ok(vec![EMPTY_SYMBOL]) + } else { + Ok(types) + } } fn match_(&self, other: &Atom) -> MatchResultIter { @@ -227,14 +232,18 @@ impl Grounded for SuperposeOp { let atom = args.get(0).ok_or_else(arg_error)?; let expr = TryInto::<&ExpressionAtom>::try_into(atom).map_err(|_| arg_error())?; - let mut superposed = Vec::new(); - for atom in expr.children() { - match interpret_no_error(self.space.clone(), atom) { - Ok(results) => { superposed.extend(results); }, - Err(message) => { return Err(format!("Error: {}", message).into()) }, + if expr.children().is_empty() { + Ok(vec![EMPTY_SYMBOL]) + } else { + let mut superposed = Vec::new(); + for atom in expr.children() { + match interpret_no_error(self.space.clone(), atom) { + Ok(results) => { superposed.extend(results); }, + Err(message) => { return Err(format!("Error: {}", message).into()) }, + } } + Ok(superposed) } - Ok(superposed) } fn match_(&self, other: &Atom) -> MatchResultIter { @@ -567,7 +576,7 @@ mod tests { assert_eq_no_order!(get_type_op.execute(&mut vec![expr!("f" "42"), expr!({space.clone()})]).unwrap(), vec![sym!("String")]); assert_eq_no_order!(get_type_op.execute(&mut vec![expr!("f" "\"test\""), expr!({space.clone()})]).unwrap(), - Vec::::new()); + vec![EMPTY_SYMBOL]); } From 97cbe2b872852acf76e7eeb104812a1bc0c37626 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 21 Aug 2023 18:01:53 +0300 Subject: [PATCH 17/27] Add case operation implementation --- lib/src/metta/runner/stdlib2.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 1de745ba4..c42fb10a4 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -535,6 +535,9 @@ pub static METTA_CODE: &'static str = " (let $pattern $atom (let* $tail $template)) $template ))) +(: case (-> %Undefined% Expression Atom)) +(= (case $atom $cases) (switch $atom $cases)) + "; #[cfg(test)] From c4d31e01c504e483969316fc2a5fe185a7437d0d Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 21 Aug 2023 18:21:51 +0300 Subject: [PATCH 18/27] Fix type-cast to allow meta type checking --- lib/src/metta/runner/stdlib2.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index c42fb10a4..5e5acc1f6 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -428,11 +428,15 @@ pub static METTA_CODE: &'static str = " (= (match-types $type1 $type2 $then $else) (eval (if-equal $type1 %Undefined% $then (eval (if-equal $type2 %Undefined% $then - (unify $type1 $type2 $then $else) ))))) + (eval (if-equal $type1 Atom $then + (eval (if-equal $type2 Atom $then + (unify $type1 $type2 $then $else) ))))))))) (= (type-cast $atom $type $space) (chain (eval (get-type $atom $space)) $actual-type - (eval (match-types $actual-type $type $atom (Error $atom BadType))) )) + (chain (eval (get-metatype $atom)) $meta + (eval (if-equal $type $meta $atom + (eval (match-types $actual-type $type $atom (Error $atom BadType))) ))))) (= (is-function $type) (chain (eval (get-metatype $type)) $meta @@ -651,6 +655,13 @@ mod tests { assert_eq!(run_program("!(eval (type-cast a B &self))"), Ok(vec![vec![expr!("a")]])); assert_eq!(run_program("!(eval (type-cast 42 Number &self))"), Ok(vec![vec![expr!({Number::Integer(42)})]])); assert_eq!(run_program("!(eval (type-cast 42 %Undefined% &self))"), Ok(vec![vec![expr!({Number::Integer(42)})]])); + assert_eq!(run_program("(: a A) !(eval (type-cast a Atom &self))"), Ok(vec![vec![expr!("a")]])); + assert_eq!(run_program("(: a A) !(eval (type-cast a Symbol &self))"), Ok(vec![vec![expr!("a")]])); + assert_eq!(run_program("!(eval (type-cast 42 Grounded &self))"), Ok(vec![vec![expr!({Number::Integer(42)})]])); + assert_eq!(run_program("!(eval (type-cast () Expression &self))"), Ok(vec![vec![expr!()]])); + assert_eq!(run_program("!(eval (type-cast (a b) Expression &self))"), Ok(vec![vec![expr!("a" "b")]])); + assert_eq!(run_program("!(eval (type-cast $v Variable &self))"), Ok(vec![vec![expr!(v)]])); + assert_eq!(run_program("(: a A) (: b B) !(eval (type-cast (a b) (A B) &self))"), Ok(vec![vec![expr!("a" "b")]])); } #[test] From 3105066d1e11fc91da976f5ade85122e98701ee8 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 21 Aug 2023 19:01:22 +0300 Subject: [PATCH 19/27] Add types to the MeTTa interpreter written in MeTTa --- lib/src/metta/runner/stdlib2.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 5e5acc1f6..e47ee01ae 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -365,39 +365,41 @@ pub static METTA_CODE: &'static str = " (: Error (-> Atom Atom ErrorType)) +(: if-non-empty-expression (-> Atom Atom Atom Atom)) (= (if-non-empty-expression $atom $then $else) (chain (eval (get-metatype $atom)) $type (eval (if-equal $type Expression (eval (if-equal $atom () $else $then)) $else )))) +(: if-decons (-> Atom Variable Variable Atom Atom Atom)) (= (if-decons $atom $head $tail $then $else) (eval (if-non-empty-expression $atom (chain (decons $atom) $list (unify $list ($head $tail) $then $else) ) $else ))) +(: if-empty (-> Atom Atom Atom Atom)) (= (if-empty $atom $then $else) (eval (if-equal $atom Empty $then $else))) +(: if-not-reducible (-> Atom Atom Atom Atom)) (= (if-not-reducible $atom $then $else) (eval (if-equal $atom NotReducible $then $else))) +(: if-error (-> Atom Atom Atom Atom)) (= (if-error $atom $then $else) (eval (if-decons $atom $head $_ (eval (if-equal $head Error $then $else)) $else ))) +(: return-on-error (-> Atom Atom Atom)) (= (return-on-error $atom $then) (eval (if-empty $atom Empty (eval (if-error $atom $atom $then ))))) -(= (car $atom) - (eval (if-decons $atom $head $_ - $head - (Error (car $atom) \"car expects a non-empty expression as an argument\") ))) - +(: switch (-> %Undefined% Expression Atom)) (= (switch $atom $cases) (chain (decons $cases) $list (chain (eval (switch-internal $atom $list)) $res @@ -408,11 +410,13 @@ pub static METTA_CODE: &'static str = " ; FIXME: subst and reduce are not used in interpreter implementation ; we could remove them +(: subst (-> Atom Variable Atom Atom)) (= (subst $atom $var $templ) (unify $atom $var $templ (Error (subst $atom $var $templ) \"subst expects a variable as a second argument\") )) +(: reduce (-> Atom Variable Atom Atom)) (= (reduce $atom $var $templ) (chain (eval $atom) $res (eval (if-empty $res Empty @@ -542,6 +546,18 @@ pub static METTA_CODE: &'static str = " (: case (-> %Undefined% Expression Atom)) (= (case $atom $cases) (switch $atom $cases)) +(: car (-> Expression Atom)) +(= (car $atom) + (eval (if-decons $atom $head $_ + $head + (Error (car $atom) \"car expects a non-empty expression as an argument\") ))) + +(: cdr (-> Expression Expression)) +(= (cdr $atom) + (eval (if-decons $atom $_ $tail + $tail + (Error (cdr $atom) \"cdr expects a non-empty expression as an argument\") ))) + "; #[cfg(test)] From 8043f595cf8bced9e43cb7352f01d637c5ce853a Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 23 Aug 2023 06:32:41 +0300 Subject: [PATCH 20/27] Add benchmark for the minimal MeTTa chain operation --- lib/benches/interpreter2.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/benches/interpreter2.rs diff --git a/lib/benches/interpreter2.rs b/lib/benches/interpreter2.rs new file mode 100644 index 000000000..140a84138 --- /dev/null +++ b/lib/benches/interpreter2.rs @@ -0,0 +1,29 @@ +#![feature(test)] + +extern crate test; + +use test::Bencher; + +use hyperon::*; +use hyperon::space::grounding::*; +use hyperon::metta::interpreter2::*; +use hyperon::metta::*; + +fn chain_atom(size: isize) -> Atom { + let mut atom = Atom::expr([CHAIN_SYMBOL, Atom::sym("A"), Atom::var("x"), Atom::var("x")]); + for _i in (1..size).step_by(1) { + atom = Atom::expr([CHAIN_SYMBOL, atom, Atom::var("x"), Atom::var("x")]) + } + atom +} + +#[bench] +fn chain_x100(bencher: &mut Bencher) { + let atom = chain_atom(100); + let expected = Ok(vec![expr!("A")]); + bencher.iter(|| { + let space = GroundingSpace::new(); + let res = interpret(space, &atom); + assert_eq!(res, expected); + }) +} From 5d8ea863f506e74361ee0cbecbd44ef2a65293af Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 23 Aug 2023 18:56:37 +0300 Subject: [PATCH 21/27] Fix iter_mut() unit tests --- lib/src/atom/iter.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/atom/iter.rs b/lib/src/atom/iter.rs index c3b487113..715ca4f75 100644 --- a/lib/src/atom/iter.rs +++ b/lib/src/atom/iter.rs @@ -130,17 +130,17 @@ mod test { #[test] fn atom_iter_mut_collect() { - assert_eq!(expr!("A").iter().collect::>(), vec![&expr!("A")]); - assert_eq!(expr!(a).iter().collect::>(), vec![&expr!(a)]); - assert_eq!(expr!({1}).iter().collect::>(), vec![&expr!({1})]); + assert_eq!(expr!("A").iter_mut().collect::>(), vec![&mut expr!("A")]); + assert_eq!(expr!(a).iter_mut().collect::>(), vec![&mut expr!(a)]); + assert_eq!(expr!({1}).iter_mut().collect::>(), vec![&mut expr!({1})]); } #[test] fn expr_iter_mut_collect() { - assert_eq!(expr!("A" a {1}).iter().collect::>(), - vec![&expr!("A"), &expr!(a), &expr!({1})]); - assert_eq!(expr!("A" (a {1})).iter().collect::>(), - vec![&expr!("A"), &expr!(a), &expr!({1})]); + assert_eq!(expr!("A" a {1}).iter_mut().collect::>(), + vec![&mut expr!("A"), &mut expr!(a), &mut expr!({1})]); + assert_eq!(expr!("A" (a {1})).iter_mut().collect::>(), + vec![&mut expr!("A"), &mut expr!(a), &mut expr!({1})]); } #[test] From d5b4e60fc0667563990fbd93cdce6865d958fe85 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 23 Aug 2023 20:29:49 +0300 Subject: [PATCH 22/27] Optimize chain minimal MeTTa operation Remove unnecessary cloning, bindings merge, bindings application. Add separate branch without cloning for the typical case with single result. --- lib/src/metta/interpreter2.rs | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index efaa04baa..67ee28e43 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -221,7 +221,13 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre }, Some([op, args @ ..]) if *op == CHAIN_SYMBOL => { match args { - [nested, Atom::Variable(var), templ] => chain(space, bindings, nested, var, templ), + [_nested, Atom::Variable(_var), _templ] => { + match atom_into_array(atom) { + Some([_, nested, Atom::Variable(var), templ]) => + chain(space, bindings, nested, var, templ), + _ => panic!("Unexpected state"), + } + }, _ => { let error: String = format!("expected: ({} (: Variable) ), found: {}", CHAIN_SYMBOL, atom); vec![InterpretedAtom(error_atom(atom, error), bindings)] @@ -348,23 +354,19 @@ fn query<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec>(space: T, bindings: Bindings, nested: &Atom, var: &VariableAtom, templ: &Atom) -> Vec { +fn chain<'a, T: SpaceRef<'a>>(space: T, bindings: Bindings, nested: Atom, var: VariableAtom, templ: Atom) -> Vec { if is_embedded_op(&nested) { - let result = interpret_atom_root(space, InterpretedAtom(nested.clone(), bindings.clone()), false); - result.into_iter() - .flat_map(|InterpretedAtom(r, b)| { - b.merge_v2(&bindings) - .into_iter() - .map(move |bindings| { - // TODO: we could eliminate bindings application here - // and below after pretty print for debug is ready, - // before that it is difficult to look at the plans - // with the variables and bindings separated. - let result = apply_bindings_to_atom(&r, &bindings); - InterpretedAtom(Atom::expr([CHAIN_SYMBOL, result, Atom::Variable(var.clone()), templ.clone()]), bindings) - }) - }) - .collect() + let mut result = interpret_atom_root(space, InterpretedAtom(nested, bindings), false); + if result.len() == 1 { + let InterpretedAtom(r, b) = result.pop().unwrap(); + vec![InterpretedAtom(Atom::expr([CHAIN_SYMBOL, r, Atom::Variable(var), templ]), b)] + } else { + result.into_iter() + .map(|InterpretedAtom(r, b)| { + InterpretedAtom(Atom::expr([CHAIN_SYMBOL, r, Atom::Variable(var.clone()), templ.clone()]), b) + }) + .collect() + } } else { let b = Bindings::new().add_var_binding_v2(var, nested).unwrap(); let result = apply_bindings_to_atom(&templ, &b); From 6332ba8eaec09468b123e8d709feb883d16e490e Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 8 Sep 2023 11:31:59 +0300 Subject: [PATCH 23/27] Implement pragma! in minimal MeTTa standard library --- lib/src/metta/runner/stdlib2.rs | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index e47ee01ae..67200a91a 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -7,10 +7,12 @@ use crate::metta::runner::Metta; use crate::metta::types::{get_atom_types, get_meta_type}; use crate::common::assert::vec_eq_no_order; use crate::metta::runner::interpret; +use crate::common::shared::Shared; use std::fmt::Display; use regex::Regex; use std::convert::TryInto; +use std::collections::HashMap; use super::arithmetics::*; @@ -289,6 +291,45 @@ impl Grounded for CollapseOp { } } +#[derive(Clone, PartialEq, Debug)] +pub struct PragmaOp { + settings: Shared>, +} + +impl PragmaOp { + pub fn new(settings: Shared>) -> Self { + Self{ settings } + } +} + +impl Display for PragmaOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "pragma!") + } +} + +impl Grounded for PragmaOp { + fn type_(&self) -> Atom { + ATOM_TYPE_UNDEFINED + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("pragma! expects key and value as arguments"); + let key = TryInto::<&SymbolAtom>::try_into(args.get(0).ok_or_else(arg_error)?) + .map_err(|_| "pragma! expects symbol atom as a key")?.name(); + let value = TryInto::<&SymbolAtom>::try_into(args.get(1).ok_or_else(arg_error)?) + .map_err(|_| "pragma! expects symbol atom as a value")?.name(); + + // TODO: add support for Grounded values when needed + self.settings.borrow_mut().insert(key.into(), value.into()); + + Ok(vec![]) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} fn regex(regex: &str) -> Regex { Regex::new(regex).unwrap() @@ -320,6 +361,8 @@ pub fn register_runner_tokens(metta: &Metta) { tref.register_token(regex(r"superpose"), move |_| { superpose_op.clone() }); let collapse_op = Atom::gnd(CollapseOp::new(space.clone())); tref.register_token(regex(r"collapse"), move |_| { collapse_op.clone() }); + let pragma_op = Atom::gnd(PragmaOp::new(metta.settings().clone())); + tref.register_token(regex(r"pragma!"), move |_| { pragma_op.clone() }); // &self should be updated // TODO: adding &self might be done not by stdlib, but by MeTTa itself. // TODO: adding &self introduces self referencing and thus prevents space From 78f3d61809ba13e553ace609edeb596dfb03307f Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 11 Sep 2023 19:24:42 +0300 Subject: [PATCH 24/27] Remove InterpreterContextRef which is not used anywhere --- lib/src/metta/interpreter2.rs | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index 67ee28e43..dd360e653 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -10,8 +10,6 @@ use crate::space::*; use crate::space::grounding::*; use crate::metta::*; -use std::ops::Deref; -use std::rc::Rc; use std::fmt::{Debug, Display, Formatter}; use std::convert::TryFrom; @@ -52,32 +50,16 @@ struct InterpreterContext<'a, T: SpaceRef<'a>> { phantom: PhantomData<&'a GroundingSpace>, } -struct InterpreterContextRef<'a, T: SpaceRef<'a>>(Rc>); - -impl<'a, T: SpaceRef<'a>> InterpreterContextRef<'a, T> { +impl<'a, T: SpaceRef<'a>> InterpreterContext<'a, T> { fn new(space: T) -> Self { - Self(Rc::new(InterpreterContext{ space, phantom: PhantomData })) - } -} - -impl<'a, T: SpaceRef<'a>> Deref for InterpreterContextRef<'a, T> { - type Target = InterpreterContext<'a, T>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, T: SpaceRef<'a>> Clone for InterpreterContextRef<'a, T> { - fn clone(&self) -> Self { - Self(Rc::clone(&self.0)) + Self{ space, phantom: PhantomData } } } pub struct InterpreterState<'a, T: SpaceRef<'a>> { plan: Vec, finished: Vec, - context: InterpreterContextRef<'a, T>, + context: InterpreterContext<'a, T>, } fn atom_as_slice(atom: &Atom) -> Option<&[Atom]> { @@ -97,7 +79,7 @@ impl<'a, T: SpaceRef<'a>> InterpreterState<'a, T> { Self { plan: vec![], finished: results, - context: InterpreterContextRef::new(space), + context: InterpreterContext::new(space), } } @@ -144,7 +126,7 @@ impl<'a, T: SpaceRef<'a>> std::fmt::Display for InterpreterState<'a, T> { /// * `space` - atomspace to query for interpretation /// * `expr` - atom to interpret pub fn interpret_init<'a, T: Space + 'a>(space: T, expr: &Atom) -> InterpreterState<'a, T> { - let context = InterpreterContextRef::new(space); + let context = InterpreterContext::new(space); InterpreterState { plan: vec![InterpretedAtom(expr.clone(), Bindings::new())], finished: vec![], From 4d4b37f68d71340aa591fcaa96a26b5ab889e8e1 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 12 Sep 2023 11:44:52 +0300 Subject: [PATCH 25/27] Rollback minimal MeTTa related changes in test This is to make tests for the old interpreter green again. --- python/tests/scripts/b5_types_prelim.metta | 4 ++-- python/tests/test_run_metta.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/tests/scripts/b5_types_prelim.metta b/python/tests/scripts/b5_types_prelim.metta index b912436d3..17b4fa0ae 100644 --- a/python/tests/scripts/b5_types_prelim.metta +++ b/python/tests/scripts/b5_types_prelim.metta @@ -55,7 +55,7 @@ ; The following will be accepted by the interpreter !(assertEqualToResult (Add Z Ten) - ((Add Z Ten) (Error Ten BadType))) + ((Add Z Ten))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -84,7 +84,7 @@ ; This list is badly typed, because S and Z are not the same type !(assertEqualToResult (Cons S (Cons Z Nil)) - ((Error (Cons Z Nil) BadType))) + ((Error Z BadType))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/python/tests/test_run_metta.py b/python/tests/test_run_metta.py index 5361559d3..e6030c090 100644 --- a/python/tests/test_run_metta.py +++ b/python/tests/test_run_metta.py @@ -72,7 +72,7 @@ def test_comments(self): def process_exceptions(self, results): for result in results: - self.assertEqual(result, [Atoms.VOID]) + self.assertEqual(result, []) def test_scripts(self): self.process_exceptions(MeTTa().import_file(f"{pwd}/scripts/a1_symbols.metta")) From 5f96b07aaaa3e72c47efc1deb83cb8e8b39b01a5 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 13 Sep 2023 10:22:44 +0300 Subject: [PATCH 26/27] Add REPL feature to enable minimal MeTTa --- lib/Cargo.toml | 1 + repl/Cargo.toml | 3 ++- repl/src/metta_shim.rs | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fd9b3eb35..0b40e6864 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,4 +18,5 @@ path = "src/lib.rs" crate-type = ["lib"] [features] +#default = ["minimal"] minimal = [] diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 60087eaf4..b787a6811 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -22,5 +22,6 @@ name = "metta" path = "src/main.rs" [features] -# default = ["python"] +# default = ["python", "minimal"] python = ["pyo3", "semver"] +minimal = ["hyperon/minimal"] diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 6bfadcebb..7efb6ef8b 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -8,7 +8,10 @@ use hyperon::space::*; use hyperon::space::grounding::GroundingSpace; use hyperon::metta::*; use hyperon::metta::runner::Metta; +#[cfg(not(feature = "minimal"))] use hyperon::metta::runner::stdlib::register_rust_tokens; +#[cfg(feature = "minimal")] +use hyperon::metta::runner::stdlib2::register_rust_tokens; use hyperon::metta::text::Tokenizer; use hyperon::metta::text::SExprParser; use hyperon::common::shared::Shared; @@ -385,4 +388,4 @@ mod py_mod_loading { match_by_equality(self, other) } } -} \ No newline at end of file +} From bc0ac59afb27919c14b9dea37537eb171c73b7be Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 13 Sep 2023 18:16:30 +0300 Subject: [PATCH 27/27] Fix check_space test which fails when minimal MeTTa enabled Test fails because stdlib is not loaded. In minimal MeTTa case it is critical because stdlib contains interpreter's implementation. --- c/tests/check_space.c | 1 + 1 file changed, 1 insertion(+) diff --git a/c/tests/check_space.c b/c/tests/check_space.c index 6c7d8c839..a808f2dd7 100644 --- a/c/tests/check_space.c +++ b/c/tests/check_space.c @@ -231,6 +231,7 @@ START_TEST (test_space_nested_in_atom) space_t runner_space = space_new_grounding_space(); tokenizer_t tokenizer = tokenizer_new(); metta_t runner = metta_new(&runner_space, &tokenizer, "."); + metta_load_module(&runner, "stdlib"); tokenizer_register_token(&tokenizer, "nested", &TOKEN_API_CLONE_ATOM, &space_atom);