Skip to content

Commit

Permalink
Merge pull request #472 from vsbogd/minimal-interpreter
Browse files Browse the repository at this point in the history
Minimal interpreter: adding function/return and fix most of the tests
  • Loading branch information
vsbogd authored Oct 26, 2023
2 parents 093b069 + 34cb0c1 commit 8fdcbae
Show file tree
Hide file tree
Showing 14 changed files with 569 additions and 256 deletions.
10 changes: 6 additions & 4 deletions c/src/metta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,13 +554,15 @@ pub extern "C" fn atom_error_message(atom: *const atom_ref_t, buf: *mut c_char,
///
#[no_mangle] pub extern "C" fn ATOM_TYPE_GROUNDED_SPACE() -> atom_t { rust_type_atom::<DynSpace>().into() }

/// @brief Creates a Symbol atom for the special MeTTa symbol used to indicate empty results in
/// case expressions.
/// @brief Creates a Symbol atom for the special MeTTa symbol used to indicate empty results
/// returned by function.
/// @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() }
#[no_mangle] pub extern "C" fn EMPTY_SYMBOL() -> atom_t {
hyperon::metta::EMPTY_SYMBOL.into()
}

/// @brief Checks whether Atom `atom` has Type `typ` in context of `space`
/// @ingroup metta_language_group
Expand Down Expand Up @@ -705,7 +707,7 @@ pub extern "C" fn step_has_next(step: *const step_result_t) -> bool {
step.has_next()
}

/// @brief Consumes a `step_result_t` and provides the ultimate outcome of a MeTTa interpreter session
/// @brief Consumes a `step_result_t` and provides the ultimate outcome of a MeTTa interpreter session
/// @ingroup interpreter_group
/// @param[in] step A pointer to a `step_result_t` to render
/// @param[in] callback A function that will be called to provide a vector of all atoms resulting from the interpreter session
Expand Down
8 changes: 4 additions & 4 deletions docs/minimal-metta.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ 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/NotReducible/Void
## Error/Empty/NotReducible/()

There are atoms which can be returned to designate a special situation in a code:
- `(Error <atom> <message>)` means the interpretation is finished with error;
Expand All @@ -46,8 +46,8 @@ There are atoms which can be returned to designate a special situation in a code
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.
- Empty expression `()` 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.
Expand All @@ -72,7 +72,7 @@ returns no results then `NotReducible` atom is a result of the instruction. Grou
function can return a list of atoms, empty result, `Error(<message>)` or
`NoReduce` result. The result of the instruction for a special values are the
following:
- empty result returns `Void` atom;
- empty result returns unit `()` result;
- `Error(<message>)` returns `(Error <original-atom> <message>)` atom;
- `NoReduce` returns `NotReducible` atom.

Expand Down
7 changes: 4 additions & 3 deletions lib/src/atom/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1597,10 +1597,11 @@ mod test {
fn bindings_cleanup() -> Result<(), &'static str> {
let mut bindings = Bindings::new()
.add_var_equality(&VariableAtom::new("a"), &VariableAtom::new("b"))?
.add_var_binding_v2(VariableAtom::new("b"), expr!("B"))?
.add_var_binding_v2(VariableAtom::new("c"), expr!("C"))?;
.add_var_binding_v2(VariableAtom::new("b"), expr!("B" d))?
.add_var_binding_v2(VariableAtom::new("c"), expr!("c"))?
.add_var_binding_v2(VariableAtom::new("d"), expr!("D"))?;
bindings.cleanup(&[&VariableAtom::new("b")].into());
assert_eq!(bindings, bind!{ b: expr!("B") });
assert_eq!(bindings, bind!{ b: expr!("B" d) });
Ok(())
}

Expand Down
191 changes: 156 additions & 35 deletions lib/src/metta/interpreter2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,32 @@ fn is_embedded_op(atom: &Atom) -> bool {
|| *op == CHAIN_SYMBOL
|| *op == UNIFY_SYMBOL
|| *op == CONS_SYMBOL
|| *op == DECONS_SYMBOL,
|| *op == DECONS_SYMBOL
|| *op == FUNCTION_SYMBOL,
_ => false,
}
}

fn is_op(atom: &Atom, op: &Atom) -> bool {
let expr = atom_as_slice(&atom);
match expr {
Some([opp, ..]) => opp == op,
_ => false,
}
}

fn is_function_op(atom: &Atom) -> bool {
is_op(atom, &FUNCTION_SYMBOL)
}

fn is_eval_op(atom: &Atom) -> bool {
is_op(atom, &EVAL_SYMBOL)
}

fn is_chain_op(atom: &Atom) -> bool {
is_op(atom, &CHAIN_SYMBOL)
}

fn interpret_atom<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: InterpretedAtom) -> Vec<InterpretedAtom> {
interpret_atom_root(space, interpreted_atom, true)
}
Expand Down Expand Up @@ -220,7 +241,7 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre
},
Some([op, args @ ..]) if *op == UNIFY_SYMBOL => {
match args {
[atom, pattern, then, else_] => match_(bindings, atom, pattern, then, else_),
[atom, pattern, then, else_] => unify(bindings, atom, pattern, then, else_),
_ => {
let error: String = format!("expected: ({} <atom> <pattern> <then> <else>), found: {}", UNIFY_SYMBOL, atom);
vec![InterpretedAtom(error_atom(atom, error), bindings)]
Expand Down Expand Up @@ -255,23 +276,41 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre
},
}
},
Some([op, args @ ..]) if *op == FUNCTION_SYMBOL => {
match args {
[Atom::Expression(_body)] => {
match atom_into_array(atom) {
Some([_, body]) =>
function(space, bindings, body, None),
_ => panic!("Unexpected state"),
}
},
[Atom::Expression(_body), Atom::Expression(_call)] => {
match atom_into_array(atom) {
Some([_, body, call]) =>
function(space, bindings, body, Some(call)),
_ => panic!("Unexpected state"),
}
},
_ => {
let error: String = format!("expected: ({} (: <body> Expression)), found: {}", FUNCTION_SYMBOL, atom);
vec![InterpretedAtom(error_atom(atom, error), bindings)]
},
}
},
_ => {
vec![InterpretedAtom(return_atom(atom), bindings)]
},
};
if root {
result.iter_mut().for_each(|interpreted| {
let InterpretedAtom(atom, bindings) = interpreted;
bindings.cleanup(&atom.iter().filter_type::<&VariableAtom>().collect());
*bindings = bindings.narrow_vars(&atom.iter().filter_type::<&VariableAtom>().collect());
});
}
result
}

fn return_unit() -> Atom {
VOID_SYMBOL
}

fn return_not_reducible() -> Atom {
NOT_REDUCIBLE_SYMBOL
}
Expand All @@ -293,13 +332,13 @@ fn eval<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec<In
// 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.
// should be removed from the plan then Empty is a proper result.
// If grounded atom returns no value then unit 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)]
vec![]
} else {
results.into_iter()
.map(|atom| InterpretedAtom(atom, bindings.clone()))
Expand All @@ -314,6 +353,8 @@ fn eval<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec<In
vec![InterpretedAtom(return_not_reducible(), bindings)],
}
},
_ if is_embedded_op(&atom) =>
interpret_atom_root(space, InterpretedAtom(atom, bindings), false),
_ => query(space, atom, bindings),
}
}
Expand All @@ -339,27 +380,92 @@ fn query<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec<I
}

fn chain<'a, T: SpaceRef<'a>>(space: T, bindings: Bindings, nested: Atom, var: VariableAtom, templ: Atom) -> Vec<InterpretedAtom> {
if is_embedded_op(&nested) {
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)| {
fn apply(bindings: Bindings, nested: Atom, var: VariableAtom, templ: &Atom) -> InterpretedAtom {
let b = Bindings::new().add_var_binding_v2(var, nested).unwrap();
let result = apply_bindings_to_atom(templ, &b);
InterpretedAtom(result, bindings)
}

let is_eval = is_eval_op(&nested);
if is_function_op(&nested) {
let mut result = interpret_atom_root(space, InterpretedAtom(nested, bindings), false);
if result.len() == 1 {
let InterpretedAtom(r, b) = result.pop().unwrap();
if is_function_op(&r) {
vec![InterpretedAtom(Atom::expr([CHAIN_SYMBOL, r, Atom::Variable(var), templ]), b)]
} else {
vec![apply(b, r, var.clone(), &templ)]
}
} else {
result.into_iter()
.map(|InterpretedAtom(r, b)| {
if is_function_op(&r) {
InterpretedAtom(Atom::expr([CHAIN_SYMBOL, r, Atom::Variable(var.clone()), templ.clone()]), b)
} else {
apply(b, r, var.clone(), &templ)
}
})
.collect()
}
} else if is_embedded_op(&nested) {
let result = interpret_atom_root(space, InterpretedAtom(nested.clone(), bindings), false);
let result = result.into_iter()
.map(|InterpretedAtom(r, b)| {
if is_eval && is_function_op(&r) {
match atom_into_array(r) {
Some([_, body]) =>
InterpretedAtom(Atom::expr([CHAIN_SYMBOL, Atom::expr([FUNCTION_SYMBOL, body, nested.clone()]), Atom::Variable(var.clone()), templ.clone()]), b),
_ => panic!("Unexpected state"),
}
} else if is_chain_op(&r) {
InterpretedAtom(Atom::expr([CHAIN_SYMBOL, r, Atom::Variable(var.clone()), templ.clone()]), b)
})
.collect()
}
} else {
apply(b, r, var.clone(), &templ)
}
})
.collect();
result
} else {
let b = Bindings::new().add_var_binding_v2(var, nested).unwrap();
let result = apply_bindings_to_atom(&templ, &b);
vec![InterpretedAtom(result, bindings)]
vec![apply(bindings, nested, var, &templ)]
}
}

fn function<'a, T: SpaceRef<'a>>(space: T, bindings: Bindings, body: Atom, call: Option<Atom>) -> Vec<InterpretedAtom> {
let call = match call {
Some(call) => call,
None => Atom::expr([FUNCTION_SYMBOL, body.clone()]),
};
match atom_as_slice(&body) {
Some([op, _result]) if *op == RETURN_SYMBOL => {
if let Some([_, result]) = atom_into_array(body) {
// FIXME: check return arguments size
vec![InterpretedAtom(result, bindings)]
} else {
panic!("Unexpected state");
}
},
_ if is_embedded_op(&body) => {
let mut result = interpret_atom_root(space, InterpretedAtom(body, bindings), false);
if result.len() == 1 {
let InterpretedAtom(r, b) = result.pop().unwrap();
vec![InterpretedAtom(Atom::expr([FUNCTION_SYMBOL, r, call]), b)]
} else {
result.into_iter()
.map(|InterpretedAtom(r, b)| {
InterpretedAtom(Atom::expr([FUNCTION_SYMBOL, r, call.clone()]), b)
})
.collect()
}
},
_ => {
let error = format!("function doesn't have return statement");
vec![InterpretedAtom(error_atom(call, error), bindings)]
},
}
}

fn match_(bindings: Bindings, atom: &Atom, pattern: &Atom, then: &Atom, else_: &Atom) -> Vec<InterpretedAtom> {
// TODO: Should match_() be symmetrical or not. While it is symmetrical then
fn unify(bindings: Bindings, atom: &Atom, pattern: &Atom, then: &Atom, else_: &Atom) -> Vec<InterpretedAtom> {
// TODO: Should unify() be symmetrical or not. While it is symmetrical then
// if variable is matched by variable then both variables have the same
// priority. Thus interpreter can use any of them further. This sometimes
// looks unexpected. For example see `metta_car` unit test where variable
Expand Down Expand Up @@ -481,7 +587,7 @@ mod tests {
#[test]
fn interpret_atom_evaluate_grounded_expression_empty() {
let result = interpret_atom(&space(""), InterpretedAtom(expr!("eval" ({ReturnNothing()} {6})), bind!{}));
assert_eq!(result, vec![atom("Void", bind!{})]);
assert_eq!(result, vec![]);
}

#[test]
Expand Down Expand Up @@ -518,20 +624,20 @@ mod tests {
fn interpret_atom_chain_evaluation() {
let space = space("(= (foo $a B) $a)");
let result = interpret_atom(&space, atom("(chain (eval (foo A $b)) $x (bar $x))", bind!{}));
assert_eq!(result, vec![atom("(chain A $x (bar $x))", bind!{})]);
assert_eq!(result, vec![atom("(bar A)", bind!{})]);
}

#[test]
fn interpret_atom_chain_nested_evaluation() {
let space = space("(= (foo $a B) $a)");
let result = interpret_atom(&space, atom("(chain (chain (eval (foo A $b)) $x (bar $x)) $y (baz $y))", bind!{}));
assert_eq!(result, vec![atom("(chain (chain A $x (bar $x)) $y (baz $y))", bind!{})]);
assert_eq!(result, vec![atom("(baz (bar A))", bind!{})]);
}

#[test]
fn interpret_atom_chain_nested_value() {
let result = interpret_atom(&space(""), atom("(chain (chain A $x (bar $x)) $y (baz $y))", bind!{}));
assert_eq!(result, vec![atom("(chain (bar A) $y (baz $y))", bind!{})]);
assert_eq!(result, vec![atom("(baz (bar A))", bind!{})]);
}

#[test]
Expand All @@ -543,9 +649,9 @@ mod tests {
");
let result = interpret_atom(&space, atom("(chain (eval (color)) $x (bar $x))", bind!{}));
assert_eq_no_order!(result, vec![
atom("(chain red $x (bar $x))", bind!{}),
atom("(chain green $x (bar $x))", bind!{}),
atom("(chain blue $x (bar $x))", bind!{})
atom("(bar red)", bind!{}),
atom("(bar green)", bind!{}),
atom("(bar blue)", bind!{})
]);
}

Expand Down Expand Up @@ -637,14 +743,29 @@ mod tests {
#[test]
fn metta_turing_machine() {
let space = space("
(= (if-embedded-op $atom $then $else)
(chain (decons $atom) $list
(unify $list (cons $_) $then
(unify $list (decons $_) $then
(unify $list (chain $_) $then
(unify $list (eval $_) $then
(unify $list (unify $_) $then
$else )))))))
(= (chain-loop $atom $var $templ)
(chain $atom $x
(eval (if-embedded-op $x
(eval (chain-loop $x $var $templ))
(chain $x $var $templ) ))))
(= (tm $rule $state $tape)
(unify $state HALT
$tape
(chain (eval (read $tape)) $char
(chain (eval ($rule $state $char)) $res
(unify $res ($next-state $next-char $dir)
(chain (eval (move $tape $next-char $dir)) $next-tape
(eval (tm $rule $next-state $next-tape)) )
(eval (chain-loop (eval (move $tape $next-char $dir)) $next-tape
(eval (tm $rule $next-state $next-tape)) ))
(Error (tm $rule $state $tape) \"Incorrect state\") )))))
(= (read ($head $hole $tail)) $hole)
Expand Down
Loading

0 comments on commit 8fdcbae

Please sign in to comment.