diff --git a/c/src/metta.rs b/c/src/metta.rs index 30934a163..f4e88ffbb 100644 --- a/c/src/metta.rs +++ b/c/src/metta.rs @@ -233,6 +233,7 @@ pub extern "C" fn sexpr_parser_free(parser: sexpr_parser_t) { /// atom if parsing is finished, or an error expression atom if a parse error occurred. /// @note The caller must take ownership responsibility for the returned `atom_t`, and ultimately free /// it with `atom_free()` or pass it to another function that takes ownership responsibility +/// @note If this function encounters an error, the error may be accessed with `sexpr_parser_err_str()` /// #[no_mangle] pub extern "C" fn sexpr_parser_parse( @@ -259,12 +260,12 @@ pub extern "C" fn sexpr_parser_parse( /// @return A pointer to the C-string containing the parse error that occurred, or NULL if no /// parse error occurred /// @warning The returned pointer should NOT be freed. It must never be accessed after the -/// sexpr_parser_t has been freed, or any subsequent `sexpr_parser_parse` or +/// sexpr_parser_t has been freed, or any subsequent call to `sexpr_parser_parse` or /// `sexpr_parser_parse_to_syntax_tree` has been made. /// #[no_mangle] pub extern "C" fn sexpr_parser_err_str( - parser: *mut sexpr_parser_t) -> *const c_char { + parser: *const sexpr_parser_t) -> *const c_char { let parser = unsafe{ &*parser }; parser.err_string } @@ -554,13 +555,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::().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 @@ -705,7 +708,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 @@ -731,21 +734,36 @@ pub extern "C" fn step_get_result(step: step_result_t, pub struct metta_t { /// Internal. Should not be accessed directly metta: *mut RustMettaInterpreter, + err_string: *mut c_char, +} + +impl metta_t { + fn free_err_string(&mut self) { + if !self.err_string.is_null() { + let string = unsafe{ std::ffi::CString::from_raw(self.err_string) }; + drop(string); + self.err_string = core::ptr::null_mut(); + } + } } struct RustMettaInterpreter(Metta); impl From for metta_t { fn from(metta: Metta) -> Self { - Self{ metta: Box::into_raw(Box::new(RustMettaInterpreter(metta))) } + Self{ + metta: Box::into_raw(Box::new(RustMettaInterpreter(metta))), + err_string: core::ptr::null_mut(), + } } } impl metta_t { fn borrow(&self) -> &Metta { - unsafe{ &(&*self.metta).0 } + &unsafe{ &*self.metta }.0 } - fn into_inner(self) -> Metta { + fn into_inner(mut self) -> Metta { + self.free_err_string(); unsafe{ Box::from_raw(self.metta).0 } } } @@ -792,7 +810,10 @@ pub extern "C" fn metta_new_with_space_environment_and_stdlib(space: *mut space_ }; let metta = Metta::new_with_stdlib_loader(|metta| { - let mut metta = metta_t{metta: (metta as *const Metta).cast_mut().cast()}; + let mut metta = metta_t{ + metta: (metta as *const Metta).cast_mut().cast(), + err_string: core::ptr::null_mut(), + }; callback(&mut metta, context); }, Some(dyn_space.clone()), env_builder); metta.into() @@ -844,6 +865,22 @@ pub extern "C" fn metta_free(metta: metta_t) { drop(metta); } +/// @brief Returns the error string associated with the last `metta_run`, `metta_evaluate_atom`, +/// or `metta_load_module` call +/// @ingroup interpreter_group +/// @param[in] metta A pointer to the MeTTa handle +/// @return A pointer to the C-string containing the error that occurred, or NULL if no +/// error occurred +/// @warning The returned pointer should NOT be freed. It must never be accessed after the +/// metta_t has been freed, or any subsequent call to `metta_run`, `metta_evaluate_atom`, or +/// `metta_load_module` has been made. +/// +#[no_mangle] +pub extern "C" fn metta_err_str(metta: *const metta_t) -> *const c_char { + let metta = unsafe{ &*metta }; + metta.err_string +} + /// @brief Compares two `metta_t` handles to test whether the referenced MeTTa runner is the same /// @ingroup interpreter_group /// @param[in] a A pointer to the first Interpreter handle @@ -923,18 +960,75 @@ pub extern "C" fn metta_tokenizer(metta: *mut metta_t) -> tokenizer_t { /// @param[in] parser An S-Expression Parser containing the MeTTa text /// @param[in] callback A function that will be called to provide a vector of atoms produced by the evaluation /// @param[in] context A pointer to a caller-defined structure to facilitate communication with the `callback` function +/// @note If this function encounters an error, the callback will not be called and the error may be accessed with `metta_err_str()` /// @warning Ownership of the provided parser will be taken by this function, so it must not be subsequently accessed /// nor freed. /// #[no_mangle] pub extern "C" fn metta_run(metta: *mut metta_t, parser: sexpr_parser_t, callback: c_atom_vec_callback_t, context: *mut c_void) { - let metta = unsafe{ &*metta }.borrow(); + let metta = unsafe{ &mut *metta }; + metta.free_err_string(); let parser = parser.into_inner(); - let results = metta.run(parser); - // TODO: return erorrs properly after step_get_result() is changed to return errors. - for result in results.expect("Returning errors from C API is not implemented yet") { - return_atoms(&result, callback, context); + let rust_metta = metta.borrow(); + let results = rust_metta.run(parser); + match results { + Ok(results) => { + for result in results { + return_atoms(&result, callback, context); + } + }, + Err(err) => { + let err_cstring = std::ffi::CString::new(err).unwrap(); + metta.err_string = err_cstring.into_raw(); + } + } +} + +/// @brief Runs the MeTTa Interpreter to evaluate an input Atom +/// @ingroup interpreter_group +/// @param[in] metta A pointer to the Interpreter handle +/// @param[in] atom The `atom_t` representing the atom to evaluate +/// @param[in] callback A function that will be called to provide a vector of atoms produced by the evaluation +/// @param[in] context A pointer to a caller-defined structure to facilitate communication with the `callback` function +/// @note If this function encounters an error, the callback will not be called and the error may be accessed with `metta_err_str()` +/// @warning This function takes ownership of the provided `atom_t`, so it must not be subsequently accessed or freed +/// +#[no_mangle] +pub extern "C" fn metta_evaluate_atom(metta: *mut metta_t, atom: atom_t, + callback: c_atom_vec_callback_t, context: *mut c_void) { + let metta = unsafe{ &mut *metta }; + metta.free_err_string(); + let atom = atom.into_inner(); + let rust_metta = metta.borrow(); + let result = rust_metta.evaluate_atom(atom); + match result { + Ok(result) => return_atoms(&result, callback, context), + Err(err) => { + let err_cstring = std::ffi::CString::new(err).unwrap(); + metta.err_string = err_cstring.into_raw(); + } + } +} + +/// @brief Loads a module into a MeTTa interpreter +/// @ingroup interpreter_group +/// @param[in] metta A pointer to the handle specifying the interpreter into which to load the module +/// @param[in] name A C-style string containing the module name +/// @note If this function encounters an error, the error may be accessed with `metta_err_str()` +/// +#[no_mangle] +pub extern "C" fn metta_load_module(metta: *mut metta_t, name: *const c_char) { + let metta = unsafe{ &mut *metta }; + metta.free_err_string(); + let rust_metta = metta.borrow(); + let result = rust_metta.load_module(PathBuf::from(cstr_as_str(name))); + match result { + Ok(()) => {}, + Err(err) => { + let err_cstring = std::ffi::CString::new(err).unwrap(); + metta.err_string = err_cstring.into_raw(); + } } } @@ -949,24 +1043,39 @@ pub extern "C" fn metta_run(metta: *mut metta_t, parser: sexpr_parser_t, pub struct runner_state_t { /// Internal. Should not be accessed directly state: *mut RustRunnerState, + err_string: *mut c_char, } -struct RustRunnerState(RunnerState<'static>); +struct RustRunnerState(RunnerState<'static, 'static>); -impl From> for runner_state_t { - fn from(state: RunnerState<'static>) -> Self { - Self{ state: Box::into_raw(Box::new(RustRunnerState(state))) } +impl runner_state_t { + fn free_err_string(&mut self) { + if !self.err_string.is_null() { + let string = unsafe{ std::ffi::CString::from_raw(self.err_string) }; + drop(string); + self.err_string = core::ptr::null_mut(); + } + } +} + +impl From> for runner_state_t { + fn from(state: RunnerState<'static, 'static>) -> Self { + Self{ + state: Box::into_raw(Box::new(RustRunnerState(state))), + err_string: core::ptr::null_mut(), + } } } impl runner_state_t { - fn into_inner(self) -> RunnerState<'static> { + fn into_inner(mut self) -> RunnerState<'static, 'static> { + self.free_err_string(); unsafe{ Box::from_raw(self.state).0 } } - fn borrow(&self) -> &RunnerState<'static> { + fn borrow(&self) -> &RunnerState<'static, 'static> { &unsafe{ &*(&*self).state }.0 } - fn borrow_mut(&mut self) -> &mut RunnerState<'static> { + fn borrow_mut(&mut self) -> &mut RunnerState<'static, 'static> { &mut unsafe{ &mut *(&*self).state }.0 } } @@ -1015,14 +1124,37 @@ pub extern "C" fn runner_state_free(state: runner_state_t) { drop(state); } +/// @brief Returns the error string associated with the last `runner_state_step` +/// @ingroup interpreter_group +/// @param[in] state A pointer to the runner state +/// @return A pointer to the C-string containing the error that occurred, or NULL if no +/// error occurred +/// @warning The returned pointer should NOT be freed. It must never be accessed after the +/// runner_state_t has been freed, or any subsequent call to `runner_state_step` has been made. +/// +#[no_mangle] +pub extern "C" fn runner_state_err_str(state: *const runner_state_t) -> *const c_char { + let state = unsafe{ &*state }; + state.err_string +} + /// @brief Runs one step of the interpreter /// @ingroup interpreter_group /// @param[in] state A pointer to the in-flight runner state +/// @note If this function encounters an error, the error may be accessed with `runner_state_err_str()` /// #[no_mangle] pub extern "C" fn runner_state_step(state: *mut runner_state_t) { - let state = unsafe{ &mut *state }.borrow_mut(); - state.run_step().unwrap_or_else(|err| panic!("Unhandled MeTTa error: {}", err)); + let state = unsafe{ &mut *state }; + state.free_err_string(); + let rust_state = state.borrow_mut(); + match rust_state.run_step() { + Ok(_) => {}, + Err(err) => { + let err_cstring = std::ffi::CString::new(err).unwrap(); + state.err_string = err_cstring.into_raw(); + } + } } /// @brief Returns whether or not the runner_state_t has completed all outstanding work @@ -1069,37 +1201,6 @@ pub extern "C" fn runner_state_current_results(state: *const runner_state_t, } } -/// @brief Runs the MeTTa Interpreter to evaluate an input Atom -/// @ingroup interpreter_group -/// @param[in] metta A pointer to the Interpreter handle -/// @param[in] atom The `atom_t` representing the atom to evaluate -/// @param[in] callback A function that will be called to provide a vector of atoms produced by the evaluation -/// @param[in] context A pointer to a caller-defined structure to facilitate communication with the `callback` function -/// @warning This function takes ownership of the provided `atom_t`, so it must not be subsequently accessed or freed -/// -#[no_mangle] -pub extern "C" fn metta_evaluate_atom(metta: *mut metta_t, atom: atom_t, - callback: c_atom_vec_callback_t, context: *mut c_void) { - let metta = unsafe{ &*metta }.borrow(); - let atom = atom.into_inner(); - let result = metta.evaluate_atom(atom) - .expect("Returning errors from C API is not implemented yet"); - return_atoms(&result, callback, context); -} - -/// @brief Loads a module into a MeTTa interpreter -/// @ingroup interpreter_group -/// @param[in] metta A pointer to the handle specifying the interpreter into which to load the module -/// @param[in] name A C-style string containing the module name -/// -#[no_mangle] -pub extern "C" fn metta_load_module(metta: *mut metta_t, name: *const c_char) { - let metta = unsafe{ &*metta }.borrow(); - // TODO: return erorrs properly - metta.load_module(PathBuf::from(cstr_as_str(name))) - .expect("Returning errors from C API is not implemented yet"); -} - // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // Environment Interface // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/c/tests/check_runner.c b/c/tests/check_runner.c index 2a25d7c7e..53214744d 100644 --- a/c/tests/check_runner.c +++ b/c/tests/check_runner.c @@ -54,10 +54,27 @@ START_TEST (test_incremental_runner) } END_TEST +START_TEST (test_runner_errors) +{ + metta_t runner = new_test_metta(); + + sexpr_parser_t parser = sexpr_parser_new("!(+ 1 (+ 2 (+ 3 4))"); + atom_vec_t* results = NULL; + metta_run(&runner, parser, ©_atom_vec, &results); + + //We have a parse error, so the callback should never be called + ck_assert(results == NULL); + ck_assert_str_eq(metta_err_str(&runner), "Unexpected end of expression"); + + metta_free(runner); +} +END_TEST + void init_test(TCase* test_case) { tcase_set_timeout(test_case, 300); //300s = 5min. To test for memory leaks tcase_add_checked_fixture(test_case, setup, teardown); tcase_add_test(test_case, test_incremental_runner); + tcase_add_test(test_case, test_runner_errors); } TEST_MAIN(init_test); diff --git a/docs/minimal-metta.md b/docs/minimal-metta.md index 1461d3767..e510e5a69 100644 --- a/docs/minimal-metta.md +++ b/docs/minimal-metta.md @@ -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 )` means the interpretation is finished with error; @@ -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. @@ -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()` 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()` returns `(Error )` atom; - `NoReduce` returns `NotReducible` atom. diff --git a/lib/src/atom/matcher.rs b/lib/src/atom/matcher.rs index 525353695..5a1fd2561 100644 --- a/lib/src/atom/matcher.rs +++ b/lib/src/atom/matcher.rs @@ -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(()) } diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index 5daa012bc..faa9f93e5 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -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 { interpret_atom_root(space, interpreted_atom, true) } @@ -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: ({} ), found: {}", UNIFY_SYMBOL, atom); vec![InterpretedAtom(error_atom(atom, error), bindings)] @@ -255,6 +276,28 @@ 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: ({} (: Expression)), found: {}", FUNCTION_SYMBOL, atom); + vec![InterpretedAtom(error_atom(atom, error), bindings)] + }, + } + }, _ => { vec![InterpretedAtom(return_atom(atom), bindings)] }, @@ -262,16 +305,12 @@ fn interpret_atom_root<'a, T: SpaceRef<'a>>(space: T, interpreted_atom: Interpre 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 } @@ -293,13 +332,13 @@ fn eval<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec>(space: T, atom: Atom, bindings: Bindings) -> Vec + interpret_atom_root(space, InterpretedAtom(atom, bindings), false), _ => query(space, atom, bindings), } } @@ -339,27 +380,92 @@ fn query<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec>(space: T, bindings: Bindings, nested: Atom, var: VariableAtom, templ: Atom) -> Vec { - 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) -> Vec { + 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 { - // 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 { + // 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 @@ -482,7 +588,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] @@ -519,20 +625,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] @@ -544,9 +650,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!{}) ]); } @@ -638,14 +744,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) diff --git a/lib/src/metta/mod.rs b/lib/src/metta/mod.rs index a825b4e1d..b9df36ee3 100644 --- a/lib/src/metta/mod.rs +++ b/lib/src/metta/mod.rs @@ -29,13 +29,27 @@ 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"); pub const DECONS_SYMBOL : Atom = sym!("decons"); pub const CONS_SYMBOL : Atom = sym!("cons"); +pub const FUNCTION_SYMBOL : Atom = sym!("function"); +pub const RETURN_SYMBOL : Atom = sym!("return"); + +pub const INTERPRET_SYMBOL : Atom = sym!("interpret"); + +//TODO: convert these from functions to static strcutures, when Atoms are Send+Sync +#[allow(non_snake_case)] +pub fn UNIT_ATOM() -> Atom { + Atom::expr([]) +} + +#[allow(non_snake_case)] +pub fn UNIT_TYPE() -> Atom { + Atom::expr([ARROW_SYMBOL]) +} /// Initializes an error expression atom pub fn error_atom(err_atom: Option, err_code: Option, message: String) -> Atom { diff --git a/lib/src/metta/runner/environment.rs b/lib/src/metta/runner/environment.rs index 01773fe6f..fe765d5aa 100644 --- a/lib/src/metta/runner/environment.rs +++ b/lib/src/metta/runner/environment.rs @@ -146,8 +146,8 @@ impl EnvBuilder { /// /// NOTE: The most recently added paths will have the highest search priority, save for the `working_dir`, /// and paths returned first by the iterator will have higher priority within the same call to add_include_paths. - pub fn add_include_paths, I: IntoIterator>(mut self, paths: I) -> Self { - let mut additional_paths: Vec = paths.into_iter().map(|path| path.borrow().into()).collect(); + pub fn add_include_paths, I: IntoIterator>(mut self, paths: I) -> Self { + let mut additional_paths: Vec = paths.into_iter().map(|path| path.as_ref().into()).collect(); additional_paths.extend(self.env.extra_include_paths); self.env.extra_include_paths = additional_paths; self diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index 7c0009651..fdcc1d870 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -14,7 +14,6 @@ use std::sync::Arc; mod environment; pub use environment::{Environment, EnvBuilder}; -#[cfg(not(feature = "minimal"))] pub mod stdlib; #[cfg(not(feature = "minimal"))] use super::interpreter::{interpret, interpret_init, interpret_step, InterpreterState}; @@ -58,16 +57,16 @@ enum MettaRunnerMode { TERMINATE, } -pub struct RunnerState<'a> { +pub struct RunnerState<'m, 'i> { mode: MettaRunnerMode, - metta: &'a Metta, - parser: Option>, - atoms: Option<&'a [Atom]>, - interpreter_state: Option>, + metta: &'m Metta, + parser: Option>, + atoms: Option<&'i [Atom]>, + interpreter_state: Option>, results: Vec>, } -impl std::fmt::Debug for RunnerState<'_> { +impl std::fmt::Debug for RunnerState<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RunnerState") .field("mode", &self.mode) @@ -112,8 +111,13 @@ impl Metta { metta.load_module(PathBuf::from("stdlib")).expect("Could not load stdlib"); //Run the `init.metta` file - if let Some(init_meta_file) = metta.0.environment.initialization_metta_file_path() { - metta.load_module(init_meta_file.into()).unwrap(); + if let Some(init_meta_file_path) = metta.0.environment.initialization_metta_file_path() { + let program = match std::fs::read_to_string(init_meta_file_path) + { + Ok(program) => program, + Err(err) => panic!("Could not read file, path: {}, error: {}", init_meta_file_path.display(), err) + }; + metta.run(SExprParser::new(program.as_str())).unwrap(); } metta } @@ -241,13 +245,7 @@ impl Metta { self.0.settings.borrow().get(key.into()).map(|a| a.to_string()) } - /// Runs a MeTTa program expressed as a string of characters - pub fn run_program_str(&self, program: &str) -> Result>, String> { - let parser = SExprParser::new(program); - self.run(parser) - } - - pub fn run<'p, 'a: 'p>(&'a self, parser: SExprParser<'p>) -> Result>, String> { + pub fn run(&self, parser: SExprParser) -> Result>, String> { let state = RunnerState::new_with_parser(self, parser); state.run_to_completion() } @@ -314,8 +312,8 @@ fn wrap_atom_by_metta_interpreter(runner: &Metta, atom: Atom) -> Atom { eval } -impl<'a> RunnerState<'a> { - fn new(metta: &'a Metta) -> Self { +impl<'m, 'i> RunnerState<'m, 'i> { + fn new(metta: &'m Metta) -> Self { Self { metta, mode: MettaRunnerMode::ADD, @@ -326,14 +324,14 @@ impl<'a> RunnerState<'a> { } } /// Returns a new RunnerState, for running code from the [SExprParser] with the specified [Metta] runner - pub fn new_with_parser(metta: &'a Metta, parser: SExprParser<'a>) -> Self { + pub fn new_with_parser(metta: &'m Metta, parser: SExprParser<'i>) -> Self { let mut state = Self::new(metta); state.parser = Some(parser); state } /// Returns a new RunnerState, for running code encoded as a slice of [Atom]s with the specified [Metta] runner - pub fn new_with_atoms(metta: &'a Metta, atoms: &'a[Atom]) -> Self { + pub fn new_with_atoms(metta: &'m Metta, atoms: &'i[Atom]) -> Self { let mut state = Self::new(metta); state.atoms = Some(atoms); state @@ -360,16 +358,12 @@ impl<'a> RunnerState<'a> { } else { //This interpreter is finished, process the results - match interpreter_state.into_result() { - Err(msg) => return Err(msg), - Ok(result) => { - let error = result.iter().any(|atom| atom_is_error(atom)); - self.results.push(result); - if error { - self.mode = MettaRunnerMode::TERMINATE; - return Ok(()); - } - } + let result = interpreter_state.into_result().unwrap(); + let error = result.iter().any(|atom| atom_is_error(atom)); + self.results.push(result); + if error { + self.mode = MettaRunnerMode::TERMINATE; + return Ok(()); } } @@ -377,7 +371,13 @@ impl<'a> RunnerState<'a> { // Get the next atom, and start a new intperpreter let next_atom = if let Some(parser) = self.parser.as_mut() { - parser.parse(&self.metta.0.tokenizer.borrow())? + match parser.parse(&self.metta.0.tokenizer.borrow()) { + Ok(atom) => atom, + Err(err) => { + self.mode = MettaRunnerMode::TERMINATE; + return Err(err); + } + } } else { if let Some(atoms) = self.atoms.as_mut() { if let Some((atom, rest)) = atoms.split_first() { diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index 4e770b98b..ead35aaaf 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -1,175 +1,241 @@ -;`$then`, `$else` should be of `Atom` type to avoid evaluation -; and infinite cycle in inference -(: if (-> Bool Atom Atom $t)) -(= (if True $then $else) $then) -(= (if False $then $else) $else) - +(: ErrorType Type) (: Error (-> Atom Atom ErrorType)) +(: ReturnType Type) +(: return (-> Atom ReturnType)) + +(: function (-> Atom Atom)) +(: eval (-> Atom Atom)) +(: chain (-> Atom Variable Atom Atom)) +(: unify (-> Atom Atom Atom Atom Atom)) +(: cons (-> Atom Atom Atom)) +(: decons (-> Atom Atom)) + +(: id (-> Atom Atom)) +(= (id $x) $x) + +(: apply (-> Atom Variable Atom Atom)) +(= (apply $atom $var $templ) + (function (chain (eval (id $atom)) $var (return $templ))) ) (: if-non-empty-expression (-> Atom Atom Atom Atom)) (= (if-non-empty-expression $atom $then $else) - (chain (eval (get-metatype $atom)) $type + (function (chain (eval (get-metatype $atom)) $type (eval (if-equal $type Expression - (eval (if-equal $atom () $else $then)) - $else )))) + (eval (if-equal $atom () (return $else) (return $then))) + (return $else) ))))) (: if-decons (-> Atom Variable Variable Atom Atom Atom)) (= (if-decons $atom $head $tail $then $else) - (eval (if-non-empty-expression $atom + (function (eval (if-non-empty-expression $atom (chain (decons $atom) $list - (unify $list ($head $tail) $then $else) ) - $else ))) + (unify $list ($head $tail) (return $then) (return $else)) ) + (return $else) )))) (: if-empty (-> Atom Atom Atom Atom)) (= (if-empty $atom $then $else) - (eval (if-equal $atom Empty $then $else))) + (function (eval (if-equal $atom Empty (return $then) (return $else)))) ) (: if-not-reducible (-> Atom Atom Atom Atom)) (= (if-not-reducible $atom $then $else) - (eval (if-equal $atom NotReducible $then $else))) + (function (eval (if-equal $atom NotReducible (return $then) (return $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 ))) + (function (eval (if-decons $atom $head $_ + (eval (if-equal $head Error (return $then) (return $else))) + (return $else) )))) (: return-on-error (-> Atom Atom Atom)) (= (return-on-error $atom $then) - (eval (if-empty $atom Empty - (eval (if-error $atom $atom - $then ))))) + (function (eval (if-empty $atom (return (return Empty)) + (eval (if-error $atom (return (return $atom)) + (return $then) )))))) (: switch (-> %Undefined% Expression Atom)) (= (switch $atom $cases) - (chain (decons $cases) $list + (function (chain (decons $cases) $list (chain (eval (switch-internal $atom $list)) $res - (eval (if-not-reducible $res Empty $res)) ))) + (chain (eval (if-not-reducible $res Empty $res)) $x (return $x)) )))) + (= (switch-internal $atom (($pattern $template) $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 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 - (eval (if-error $res $res - (eval (if-not-reducible $res - (eval (subst $atom $var $templ)) - (eval (reduce $res $var $templ)) )))))))) + (function (unify $atom $pattern + (return $template) + (chain (eval (switch $atom $tail)) $ret (return $ret)) ))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; MeTTa interpreter implementation ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (= (match-types $type1 $type2 $then $else) - (eval (if-equal $type1 %Undefined% $then - (eval (if-equal $type2 %Undefined% $then - (eval (if-equal $type1 Atom $then - (eval (if-equal $type2 Atom $then - (unify $type1 $type2 $then $else) ))))))))) + (function (eval (if-equal $type1 %Undefined% + (return $then) + (eval (if-equal $type2 %Undefined% + (return $then) + (eval (if-equal $type1 Atom + (return $then) + (eval (if-equal $type2 Atom + (return $then) + (unify $type1 $type2 (return $then) (return $else)) )))))))))) (= (type-cast $atom $type $space) - (chain (eval (get-type $atom $space)) $actual-type - (chain (eval (get-metatype $atom)) $meta - (eval (if-equal $type $meta $atom - (eval (match-types $actual-type $type $atom (Error $atom BadType))) ))))) + (function (chain (eval (get-metatype $atom)) $meta + (eval (if-equal $type $meta + (return $atom) + ; TODO: the proper way to get types is something like + ; `(collapse (get-type ))` but it leads to the infinite + ; recursion because interpreter called by `collapse` evaluates + ; `type-cast` again. + (chain (eval (collapse-get-type $atom $space)) $actual-types + (chain (eval (foldl-atom $actual-types False + $a $b (chain (eval (match-types $b $type True False)) $is-b-comp + (chain (eval (or $a $is-b-comp)) $or $or) ))) $is-some-comp + (eval (if $is-some-comp + (return $atom) + (return (Error $atom BadType)) ))))))))) + (= (is-function $type) - (chain (eval (get-metatype $type)) $meta - (eval (switch ($type $meta) - ( - (($_ Expression) - (chain (eval (car $type)) $head - (unify $head -> True False) )) - ($_ False) ))))) + (function (chain (eval (get-metatype $type)) $meta + (eval (switch ($type $meta) ( + (($_ Expression) + (eval (if-decons $type $head $_tail + (unify $head -> (return True) (return False)) + (return (Error (is-function $type) "is-function non-empty expression as an argument")) ))) + ($_ (return False)) + )))))) + +(: filter-atom (-> Expression Variable Atom Expression)) +(= (filter-atom $list $var $filter) + (function (eval (if-decons $list $head $tail + (chain (eval (filter-atom $tail $var $filter)) $tail-filtered + (chain (eval (apply $head $var $filter)) $filter-expr + (chain $filter-expr $is-filtered + (eval (if $is-filtered + (chain (cons $head $tail-filtered) $res (return $res)) + (return $tail-filtered) ))))) + (return ()) )))) + +(: map-atom (-> Expression Variable Atom Expression)) +(= (map-atom $list $var $map) + (function (eval (if-decons $list $head $tail + (chain (eval (map-atom $tail $var $map)) $tail-mapped + (chain (eval (apply $head $var $map)) $map-expr + (chain $map-expr $head-mapped + (chain (cons $head-mapped $tail-mapped) $res (return $res)) ))) + (return ()) )))) + +(: foldl-atom (-> Expression Atom Variable Variable Atom Atom)) +(= (foldl-atom $list $init $a $b $op) + (function (eval (if-decons $list $head $tail + (chain (eval (apply $init $a $op)) $op-init + (chain (eval (apply $head $b $op-init)) $op-head + (chain $op-head $head-folded + (chain (eval (foldl-atom $tail $head-folded $a $b $op)) $res (return $res)) ))) + (return $init) )))) (= (interpret $atom $type $space) - (chain (eval (get-metatype $atom)) $meta - (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))) ))))))))) + (function (chain (eval (get-metatype $atom)) $meta + (eval (if-equal $type Atom + (return $atom) + (eval (if-equal $type $meta + (return $atom) + (eval (switch ($type $meta) ( + (($_type Variable) (return $atom)) + (($_type Symbol) + (chain (eval (type-cast $atom $type $space)) $ret (return $ret))) + (($_type Grounded) + (chain (eval (type-cast $atom $type $space)) $ret (return $ret))) + (($_type Expression) + (chain (eval (interpret-expression $atom $type $space)) $ret (return $ret))) + )))))))))) (= (interpret-expression $atom $type $space) - (eval (if-decons $atom $op $args + (function (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 $type $space)) $reduced-atom - (eval (call $reduced-atom $type $space)) ) + (chain (eval (metta-call $reduced-atom $type $space)) $ret (return $ret)) ) (chain (eval (interpret-tuple $atom $space)) $reduced-atom - (eval (call $reduced-atom $type $space)) )))) - (eval (type-cast $atom $type $space)) ))) + (chain (eval (metta-call $reduced-atom $type $space)) $ret (return $ret)) )))) + (chain (eval (type-cast $atom $type $space)) $ret (return $ret)) )))) (= (interpret-func $expr $type $ret-type $space) - (eval (if-decons $expr $op $args + (function (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 $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") ))) + (chain (cons $reduced-op $reduced-args) $r (return $r))))) + (return (Error $type "Function type expected")) ))))) + (return (Error $expr "Non-empty expression atom is expected")) )))) (= (interpret-args $atom $args $arg-types $ret-type $space) - (unify $args () - (chain (eval (car $arg-types)) $actual-ret-type - (eval (match-types $actual-ret-type $ret-type () (Error $atom BadType)))) + (function (unify $args () + (eval (if-decons $arg-types $actual-ret-type $_tail + (eval (match-types $actual-ret-type $ret-type + (return ()) + (return (Error $atom BadType)) )) + (return (Error (interpret-args $atom $args $arg-types $ret-type $space) "interpret-args expects a non-empty value for $arg-types argument")) )) (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 $ret-type $space)) + (chain (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $ret-type $space)) $ret (return $ret)) (eval (return-on-error $reduced-head - (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") )))) + (chain (eval (interpret-args-tail $atom $reduced-head $tail $tail-types $ret-type $space)) $ret (return $ret)) ))))) + (return (Error $atom BadType)) )) + (return (Error (interpret-atom $atom $args $arg-types $space) "Non-empty expression atom is expected")) ))))) (= (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 + (function (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) )))) + (chain (cons $head $reduced-tail) $ret (return $ret)) ))))) (= (interpret-tuple $atom $space) - (unify $atom () - $atom + (function (unify $atom () + (return $atom) (eval (if-decons $atom $head $tail (chain (eval (interpret $head %Undefined% $space)) $rhead - (eval (if-empty $rhead Empty + (eval (if-empty $rhead (return 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") )))) + (eval (if-empty $rtail (return Empty) + (chain (cons $rhead $rtail) $ret (return $ret)) )))))) + (return (Error (interpret-tuple $atom $space) "Non-empty expression atom is expected as an argument")) ))))) -(= (call $atom $type $space) - (eval (if-error $atom $atom +(= (metta-call $atom $type $space) + (function (eval (if-error $atom (return $atom) (chain (eval $atom) $result - (eval (if-not-reducible $result $atom - (eval (if-empty $result Empty - (eval (if-error $result $result - (eval (interpret $result $type $space)) )))))))))) + (eval (if-not-reducible $result (return $atom) + (eval (if-empty $result (return Empty) + (eval (if-error $result (return $result) + (chain (eval (interpret $result $type $space)) $ret (return $ret)) ))))))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Standard library written in MeTTa ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;`$then`, `$else` should be of `Atom` type to avoid evaluation +; and infinite cycle in inference +(: if (-> Bool Atom Atom $t)) +(= (if True $then $else) $then) +(= (if False $then $else) $else) + +(: or (-> Bool Bool Bool)) +(= (or False False) False) +(= (or False True) True) +(= (or True False) True) +(= (or True True) True) + +(: and (-> Bool Bool Bool)) +(= (and False False) False) +(= (and False True) False) +(= (and True False) False) +(= (and True True) True) + (: match (-> Atom Atom Atom %Undefined%)) (= (match $space $pattern $template) (unify $pattern $space $template Empty)) @@ -184,17 +250,25 @@ (let $pattern $atom (let* $tail $template)) $template ))) -(: case (-> %Undefined% Expression Atom)) -(= (case $atom $cases) (switch $atom $cases)) - -(: car (-> Expression Atom)) -(= (car $atom) +(: car-atom (-> Expression Atom)) +(= (car-atom $atom) (eval (if-decons $atom $head $_ $head - (Error (car $atom) "car expects a non-empty expression as an argument") ))) + (Error (car-atom $atom) "car-atom expects a non-empty expression as an argument") ))) -(: cdr (-> Expression Expression)) -(= (cdr $atom) +(: cdr-atom (-> Expression Expression)) +(= (cdr-atom $atom) (eval (if-decons $atom $_ $tail $tail - (Error (cdr $atom) "cdr expects a non-empty expression as an argument") ))) + (Error (cdr-atom $atom) "cdr-atom expects a non-empty expression as an argument") ))) + +(: quote (-> Atom Atom)) +(= (quote $atom) NotReducible) + +(: unquote (-> %Undefined% %Undefined%)) +(= (unquote (quote $atom)) $atom) + +; TODO: there is no way to define operation which consumes any number of +; arguments and returns unit +(= (nop) ()) +(= (nop $x) ()) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 09baec581..d4e6ad9f2 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -23,15 +23,6 @@ use super::arithmetics::*; pub const VOID_SYMBOL : Atom = sym!("%void%"); -//TODO: convert these from functions to static strcutures, when Atoms are Send+Sync -#[allow(non_snake_case)] -pub fn UNIT_ATOM() -> Atom { - Atom::expr([]) -} -#[allow(non_snake_case)] -pub fn UNIT_TYPE() -> Atom { - Atom::expr([ARROW_SYMBOL]) -} fn unit_result() -> Result, ExecError> { Ok(vec![UNIT_ATOM()]) } @@ -1204,7 +1195,7 @@ pub static METTA_CODE: &'static str = " (: Error (-> Atom Atom ErrorType)) "; -#[cfg(test)] +#[cfg(all(test, not(feature = "minimal")))] mod tests { use super::*; use crate::metta::text::*; diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 77545b3a0..951be5976 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -6,30 +6,32 @@ 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 crate::common::shared::Shared; +use crate::metta::runner::stdlib; use std::fmt::Display; use regex::Regex; use std::convert::TryInto; -use std::collections::HashMap; use super::arithmetics::*; pub const VOID_SYMBOL : Atom = sym!("%void%"); -//TODO: convert these from functions to static strcutures, when Atoms are Send+Sync -#[allow(non_snake_case)] -pub fn UNIT_ATOM() -> Atom { - Atom::expr([]) -} -#[allow(non_snake_case)] -pub fn UNIT_TYPE() -> Atom { - Atom::expr([ARROW_SYMBOL]) +fn unit_result() -> Result, ExecError> { + Ok(vec![UNIT_ATOM()]) } #[derive(Clone, PartialEq, Debug)] -pub struct GetTypeOp {} +pub struct GetTypeOp { + // TODO: MINIMAL this is temporary compatibility fix to be removed after + // migration to the minimal MeTTa + space: DynSpace, +} + +impl GetTypeOp { + pub fn new(space: DynSpace) -> Self { + Self{ space } + } +} impl Display for GetTypeOp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -45,8 +47,11 @@ impl Grounded for GetTypeOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("get-type expects single atom as an argument"); 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")?; + let space = match args.get(1) { + Some(space) => Atom::as_gnd::(space) + .ok_or("match expects a space as the first argument"), + None => Ok(&self.space), + }?; let types = get_atom_types(space, atom); if types.is_empty() { Ok(vec![EMPTY_SYMBOL]) @@ -60,6 +65,38 @@ impl Grounded for GetTypeOp { } } +#[derive(Clone, PartialEq, Debug)] +pub struct CollapseGetTypeOp {} + +impl Display for CollapseGetTypeOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "collapse-get-type") + } +} + +impl Grounded for CollapseGetTypeOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_EXPRESSION]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("get-type expects single atom as an argument"); + 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")?; + let types = get_atom_types(space, atom); + if types.is_empty() { + Ok(vec![Atom::expr([])]) + } else { + Ok(vec![Atom::expr(types)]) + } + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} + #[derive(Clone, PartialEq, Debug)] pub struct GetMetaTypeOp { } @@ -122,7 +159,6 @@ 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 { @@ -131,11 +167,16 @@ fn interpret_no_error(space: DynSpace, expr: &Atom) -> Result, String> } } +fn interpret(space: DynSpace, expr: &Atom) -> Result, String> { + let expr = Atom::expr([EVAL_SYMBOL, Atom::expr([INTERPRET_SYMBOL, expr.clone(), ATOM_TYPE_UNDEFINED, Atom::gnd(space.clone())])]); + crate::metta::interpreter2::interpret(space, &expr) +} + 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]), + Ok(()) => unit_result(), Err(diff) => Err(ExecError::Runtime(format!("{}\n{}", report, diff))) } } @@ -304,34 +345,58 @@ impl Grounded for CollapseOp { } #[derive(Clone, PartialEq, Debug)] -pub struct PragmaOp { - settings: Shared>, +pub struct CaseOp { + space: DynSpace, } -impl PragmaOp { - pub fn new(settings: Shared>) -> Self { - Self{ settings } +impl CaseOp { + pub fn new(space: DynSpace) -> Self { + Self{ space } } } -impl Display for PragmaOp { +impl Display for CaseOp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "pragma!") + write!(f, "case") } } -impl Grounded for PragmaOp { +impl Grounded for CaseOp { fn type_(&self) -> Atom { - ATOM_TYPE_UNDEFINED + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) } 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 = args.get(1).ok_or_else(arg_error)?; - self.settings.borrow_mut().insert(key.into(), value.clone()); - Ok(vec![]) + let arg_error = || ExecError::from("case expects two arguments: atom and expression of cases"); + let cases = args.get(1).ok_or_else(arg_error)?; + let atom = args.get(0).ok_or_else(arg_error)?; + log::debug!("CaseOp::execute: atom: {}, cases: {:?}", atom, cases); + + let switch = |interpreted: Atom| -> Atom { + Atom::expr([sym!("switch"), interpreted, cases.clone()]) + }; + + // Interpreting argument inside CaseOp is required because otherwise `Empty` result + // calculated inside interpreter cuts the whole branch of the interpretation. Also we + // cannot use `unify` in a unit test because after interpreting `(chain... (chain (eval + // (interpret (unify ...) Atom ))) ...)` `chain` executes `unify` and also gets + // `Empty` even if we have `Atom` as a resulting type. It can be solved by different ways. + // One way is to invent new type `EmptyType` (type of the `Empty` atom) and use this type + // in a function to allow `Empty` atoms as an input. `EmptyType` type should not be + // casted to the `%Undefined%` thus one cannot pass `Empty` to the function which accepts + // `%Undefined%`. Another way is to introduce "call" level. Thus if function called + // returned the result to the `chain` it should stop reducing it and insert it into the + // last argument. + let results = interpret(self.space.clone(), atom); + log::debug!("CaseOp::execute: atom results: {:?}", results); + let results = match results { + Ok(results) if results.is_empty() => + vec![switch(EMPTY_SYMBOL)], + Ok(results) => + results.into_iter().map(|atom| switch(atom)).collect(), + Err(err) => vec![Atom::expr([ERROR_SYMBOL, atom.clone(), Atom::sym(err)])], + }; + Ok(results) } fn match_(&self, other: &Atom) -> MatchResultIter { @@ -346,13 +411,30 @@ fn regex(regex: &str) -> Regex { pub fn register_common_tokens(metta: &Metta) { let tokenizer = metta.tokenizer(); let mut tref = tokenizer.borrow_mut(); + let space = metta.space(); - let get_type_op = Atom::gnd(GetTypeOp{}); + let get_type_op = Atom::gnd(GetTypeOp::new(space.clone())); tref.register_token(regex(r"get-type"), move |_| { get_type_op.clone() }); + let collapse_get_type_op = Atom::gnd(CollapseGetTypeOp{}); + tref.register_token(regex(r"collapse-get-type"), move |_| { collapse_get_type_op.clone() }); let get_meta_type_op = Atom::gnd(GetMetaTypeOp{}); tref.register_token(regex(r"get-metatype"), move |_| { get_meta_type_op.clone() }); let is_equivalent = Atom::gnd(IfEqualOp{}); tref.register_token(regex(r"if-equal"), move |_| { is_equivalent.clone() }); + let new_space_op = Atom::gnd(stdlib::NewSpaceOp{}); + tref.register_token(regex(r"new-space"), move |_| { new_space_op.clone() }); + let add_atom_op = Atom::gnd(stdlib::AddAtomOp{}); + tref.register_token(regex(r"add-atom"), move |_| { add_atom_op.clone() }); + let remove_atom_op = Atom::gnd(stdlib::RemoveAtomOp{}); + tref.register_token(regex(r"remove-atom"), move |_| { remove_atom_op.clone() }); + let get_atoms_op = Atom::gnd(stdlib::GetAtomsOp{}); + tref.register_token(regex(r"get-atoms"), move |_| { get_atoms_op.clone() }); + let new_state_op = Atom::gnd(stdlib::NewStateOp{}); + tref.register_token(regex(r"new-state"), move |_| { new_state_op.clone() }); + let change_state_op = Atom::gnd(stdlib::ChangeStateOp{}); + tref.register_token(regex(r"change-state!"), move |_| { change_state_op.clone() }); + let get_state_op = Atom::gnd(stdlib::GetStateOp{}); + tref.register_token(regex(r"get-state"), move |_| { get_state_op.clone() }); } pub fn register_runner_tokens(metta: &Metta) { @@ -369,8 +451,14 @@ 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())); + let case_op = Atom::gnd(CaseOp::new(space.clone())); + tref.register_token(regex(r"case"), move |_| { case_op.clone() }); + let pragma_op = Atom::gnd(stdlib::PragmaOp::new(metta.settings().clone())); tref.register_token(regex(r"pragma!"), move |_| { pragma_op.clone() }); + let import_op = Atom::gnd(stdlib::ImportOp::new(metta.clone())); + tref.register_token(regex(r"import!"), move |_| { import_op.clone() }); + let bind_op = Atom::gnd(stdlib::BindOp::new(tokenizer.clone())); + tref.register_token(regex(r"bind!"), move |_| { bind_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 @@ -433,7 +521,7 @@ mod tests { (: A C) ")); - let get_type_op = GetTypeOp{}; + let get_type_op = GetTypeOp::new(space.clone()); assert_eq_no_order!(get_type_op.execute(&mut vec![sym!("A"), expr!({space.clone()})]).unwrap(), vec![sym!("B"), sym!("C")]); } @@ -446,7 +534,7 @@ mod tests { (: \"test\" String) ")); - let get_type_op = GetTypeOp{}; + let get_type_op = GetTypeOp::new(space.clone()); 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(), @@ -455,17 +543,17 @@ mod tests { #[test] - fn metta_car() { - let result = run_program("!(eval (car (A $b)))"); + fn metta_car_atom() { + let result = run_program("!(eval (car-atom (A $b)))"); assert_eq!(result, Ok(vec![vec![expr!("A")]])); - let result = run_program("!(eval (car ($a B)))"); + let result = run_program("!(eval (car-atom ($a B)))"); //assert_eq!(result, Ok(vec![vec![expr!(a)]])); assert!(result.is_ok_and(|res| res.len() == 1 && res[0].len() == 1 && atoms_are_equivalent(&res[0][0], &expr!(a)))); - let result = run_program("!(eval (car ()))"); - assert_eq!(result, Ok(vec![vec![expr!("Error" ("car" ()) "\"car expects a non-empty expression as an argument\"")]])); - let result = run_program("!(eval (car A))"); - assert_eq!(result, Ok(vec![vec![expr!("Error" ("car" "A") "\"car expects a non-empty expression as an argument\"")]])); + let result = run_program("!(eval (car-atom ()))"); + assert_eq!(result, Ok(vec![vec![expr!("Error" ("car-atom" ()) "\"car-atom expects a non-empty expression as an argument\"")]])); + let result = run_program("!(eval (car-atom A))"); + assert_eq!(result, Ok(vec![vec![expr!("Error" ("car-atom" "A") "\"car-atom expects a non-empty expression as an argument\"")]])); } #[test] @@ -478,6 +566,18 @@ mod tests { assert_eq!(result, Ok(vec![vec![]])); } + #[test] + fn metta_case_empty() { + let result = run_program("!(case Empty ( (ok ok) (Empty nok) ))"); + assert_eq!(result, Ok(vec![vec![expr!("nok")]])); + let result = run_program("!(case (unify (C B) (C B) ok Empty) ( (ok ok) (Empty nok) ))"); + assert_eq!(result, Ok(vec![vec![expr!("ok")]])); + let result = run_program("!(case (unify (B C) (C B) ok nok) ( (ok ok) (nok nok) ))"); + assert_eq!(result, Ok(vec![vec![expr!("nok")]])); + let result = run_program("!(case (match (B C) (C B) ok) ( (ok ok) (Empty nok) ))"); + assert_eq!(result, Ok(vec![vec![expr!("nok")]])); + } + #[test] fn metta_is_function() { let result = run_program("!(eval (is-function (-> $t)))"); @@ -488,32 +588,6 @@ mod tests { assert_eq!(result, Ok(vec![vec![expr!({Bool(false)})]])); } - #[test] - fn metta_reduce_chain() { - assert_eq!(run_program(" - (= (foo $x) (bar $x)) - (= (bar $x) (baz $x)) - (= (baz $x) $x) - !(eval (reduce (foo A) $x (reduced $x))) - "), Ok(vec![vec![expr!("reduced" "A")]])); - assert_eq!(run_program(" - !(eval (reduce (foo A) $x (reduced $x))) - "), Ok(vec![vec![expr!("reduced" ("foo" "A"))]])); - assert_eq!(run_program(" - (= (foo A) (Error (foo A) \"Test error\")) - !(eval (reduce (foo A) $x (reduced $x))) - "), Ok(vec![vec![expr!("Error" ("foo" "A") "\"Test error\"")]])); - } - - #[test] - fn metta_reduce_reduce() { - assert_eq!(run_program(" - (= (foo $x) (reduce (bar $x) $t $t)) - (= (bar $x) $x) - !(eval (reduce (foo A) $x $x)) - "), Ok(vec![vec![expr!("A")]])); - } - #[test] fn metta_type_cast() { assert_eq!(run_program("(: a A) !(eval (type-cast a A &self))"), Ok(vec![vec![expr!("a")]])); @@ -529,6 +603,26 @@ mod tests { 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")]])); + assert_eq!(run_program("(: a A) (: a B) !(eval (type-cast a A &self))"), Ok(vec![vec![expr!("a")]])); + } + + #[test] + fn metta_filter_atom() { + assert_eq!(run_program("!(eval (filter-atom () $x (eval (if-error $x False True))))"), Ok(vec![vec![expr!()]])); + assert_eq!(run_program("!(eval (filter-atom (a (b) $c) $x (eval (if-error $x False True))))"), Ok(vec![vec![expr!("a" ("b") c)]])); + assert_eq!(run_program("!(eval (filter-atom (a (Error (b) \"Test error\") $c) $x (eval (if-error $x False True))))"), Ok(vec![vec![expr!("a" c)]])); + } + + #[test] + fn metta_map_atom() { + assert_eq!(run_program("!(eval (map-atom () $x ($x mapped)))"), Ok(vec![vec![expr!()]])); + assert_eq!(run_program("!(eval (map-atom (a (b) $c) $x (mapped $x)))"), Ok(vec![vec![expr!(("mapped" "a") ("mapped" ("b")) ("mapped" c))]])); + } + + #[test] + fn metta_foldl_atom() { + assert_eq!(run_program("!(eval (foldl-atom () 1 $a $b (eval (+ $a $b))))"), Ok(vec![vec![expr!({Number::Integer(1)})]])); + assert_eq!(run_program("!(eval (foldl-atom (1 2 3) 0 $a $b (eval (+ $a $b))))"), Ok(vec![vec![expr!({Number::Integer(6)})]])); } #[test] @@ -620,6 +714,12 @@ mod tests { assert_eq!(run_program("!(eval (interpret () SomeType &self))"), Ok(vec![vec![expr!(())]])); } + #[test] + fn metta_interpret_single_atom_with_two_types() { + let result = run_program("(: a A) (: a B) !(eval (interpret a %Undefined% &self))"); + assert_eq!(result, Ok(vec![vec![expr!("a")]])); + } + #[test] fn metta_assert_equal_op() { let metta = Metta::new(Some(EnvBuilder::test_env())); @@ -630,7 +730,7 @@ mod tests { "; assert_eq!(metta.run(SExprParser::new(program)), Ok(vec![])); assert_eq!(metta.run(SExprParser::new("!(assertEqual (foo A) (bar A))")), Ok(vec![ - vec![VOID_SYMBOL], + vec![UNIT_ATOM()], ])); assert_eq!(metta.run(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")], @@ -653,7 +753,7 @@ mod tests { "; assert_eq!(metta.run(SExprParser::new(program)), Ok(vec![])); assert_eq!(metta.run(SExprParser::new("!(assertEqualToResult (foo) (A B))")), Ok(vec![ - vec![VOID_SYMBOL], + vec![UNIT_ATOM()], ])); assert_eq!(metta.run(SExprParser::new("!(assertEqualToResult (bar) (A))")), Ok(vec![ vec![expr!("Error" ({assert.clone()} ("bar") ("A")) "\nExpected: [A]\nGot: [C]\nMissed result: A")], @@ -695,7 +795,7 @@ mod tests { } #[test] - fn metta_let() { + fn metta_let_novar() { 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(" @@ -724,6 +824,18 @@ mod tests { assert_eq!(result, Ok(vec![vec![]])); } + #[test] + fn metta_quote_unquote() { + let header = " + (= (foo) A) + (= (bar $x) $x) + "; + assert_eq!(run_program(&format!("{header} !(bar (foo))")), Ok(vec![vec![sym!("A")]]), "sanity check"); + assert_eq!(run_program(&format!("{header} !(bar (quote (foo)))")), Ok(vec![vec![expr!("quote" ("foo"))]]), "quote"); + assert_eq!(run_program(&format!("{header} !(bar (unquote (quote (foo))))")), Ok(vec![vec![expr!("A")]]), "unquote before call"); + assert_eq!(run_program(&format!("{header} !(unquote (bar (quote (foo))))")), Ok(vec![vec![expr!("A")]]), "unquote after call"); + } + #[test] fn test_frog_reasoning() { diff --git a/lib/tests/metta.rs b/lib/tests/metta.rs index a4ffe60a1..0d9081038 100644 --- a/lib/tests/metta.rs +++ b/lib/tests/metta.rs @@ -1,7 +1,4 @@ -#[cfg(not(feature = "minimal"))] -use hyperon::metta::runner::stdlib::UNIT_ATOM; -#[cfg(feature = "minimal")] -use hyperon::metta::runner::stdlib2::UNIT_ATOM; +use hyperon::metta::UNIT_ATOM; use hyperon::metta::text::*; use hyperon::metta::runner::{Metta, EnvBuilder}; diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 6dcd4c0e1..0e2beddd7 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -120,7 +120,7 @@ class AtomType: class Atoms: - VOID = Atom._from_catom(hp.CAtoms.VOID) + EMPTY = Atom._from_catom(hp.CAtoms.EMPTY) class GroundedAtom(Atom): """ @@ -186,7 +186,7 @@ def __init__(self, content, id=None): self.id = id def __repr__(self): - """Returns the object's ID if present, or a string representation of + """Returns the object's ID if present, or a string representation of its content if not.""" # Overwrite Python default representation of a string to use # double quotes instead of single quotes. @@ -213,7 +213,7 @@ class ValueObject(GroundedObject): obj1 = ValueObject(5) obj2 = ValueObject(5) obj3 = ValueObject(6) - + print(obj1 == obj2) # True print(obj1 == obj3) # False """ diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index 664063a26..20e35f8d8 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -29,6 +29,9 @@ def run_step(self): Executes the next step in the interpretation plan, or begins interpretation of the next atom in the stream of MeTTa code. """ hp.runner_state_step(self.cstate) + err_str = hp.runner_state_err_str(self.cstate) + if (err_str is not None): + raise RuntimeError(err_str) def is_complete(self): """ @@ -169,6 +172,7 @@ def import_file(self, fname): program = f.read() f.close() # changing cwd + # TODO: Changing the working dir will not be necessary when the stdlib ops can access the correct runner context. See https://github.com/trueagi-io/hyperon-experimental/issues/410 prev_cwd = os.getcwd() os.chdir(os.sep.join(path[:-1])) result = self.run(program) @@ -180,6 +184,9 @@ def run(self, program, flat=False): """Runs the program""" parser = SExprParser(program) results = hp.metta_run(self.cmetta, parser.cparser) + err_str = hp.metta_err_str(self.cmetta) + if (err_str is not None): + raise RuntimeError(err_str) if flat: return [Atom._from_catom(catom) for result in results for catom in result] else: diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index 5466496b0..07d181236 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -791,13 +791,17 @@ PYBIND11_MODULE(hyperonpy, m) { #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"); + ADD_SYMBOL(EMPTY, "Empty"); py::class_(m, "CMetta"); m.def("metta_new", [](CSpace space, EnvBuilder env_builder) { return CMetta(metta_new_with_space_environment_and_stdlib(space.ptr(), env_builder.obj, &run_python_loader_callback, NULL)); }, "New MeTTa interpreter instance"); m.def("metta_free", [](CMetta metta) { metta_free(metta.obj); }, "Free MeTTa interpreter"); + m.def("metta_err_str", [](CMetta& metta) { + const char* err_str = metta_err_str(metta.ptr()); + return err_str != NULL ? py::cast(std::string(err_str)) : py::none(); + }, "Returns the error string from the last MeTTa operation or None"); m.def("metta_eq", [](CMetta& a, CMetta& b) { return metta_eq(a.ptr(), b.ptr()); }, "Compares two MeTTa handles"); m.def("metta_search_path_cnt", [](CMetta& metta) { return metta_search_path_cnt(metta.ptr()); }, "Returns the number of module search paths in the runner's environment"); m.def("metta_nth_search_path", [](CMetta& metta, size_t idx) { @@ -816,6 +820,8 @@ PYBIND11_MODULE(hyperonpy, m) { metta_evaluate_atom(metta.ptr(), atom_clone(atom.ptr()), copy_atoms, &atoms); return atoms; }, "Run MeTTa interpreter on an atom"); + //QUESTION: Should we eliminate the `metta_load_module` function from the native APIs, in favor of + // allowing the caller to load modules using the `import` operation in MeTTa code? m.def("metta_load_module", [](CMetta& metta, std::string text) { metta_load_module(metta.ptr(), text.c_str()); }, "Load MeTTa module"); @@ -833,6 +839,10 @@ PYBIND11_MODULE(hyperonpy, m) { }, "Initializes the MeTTa runner state for incremental execution"); m.def("runner_state_step", [](CRunnerState& state) { runner_state_step(state.ptr()); }, "Runs one incremental step of the MeTTa interpreter"); m.def("runner_state_free", [](CRunnerState state) { runner_state_free(state.obj); }, "Frees a Runner State"); + m.def("runner_state_err_str", [](CRunnerState& state) { + const char* err_str = runner_state_err_str(state.ptr()); + return err_str != NULL ? py::cast(std::string(err_str)) : py::none(); + }, "Returns the error string from the last RunnerState operation or None"); m.def("runner_state_is_complete", [](CRunnerState& state) { return runner_state_is_complete(state.ptr()); }, "Returns whether a RunnerState is finished"); m.def("runner_state_current_results", [](CRunnerState& state) { py::list lists_of_atom; diff --git a/python/sandbox/das_gate/dasgate.py b/python/sandbox/das_gate/dasgate.py index 57661dc55..091245984 100644 --- a/python/sandbox/das_gate/dasgate.py +++ b/python/sandbox/das_gate/dasgate.py @@ -1,12 +1,9 @@ from hyperon import * from hyperon.ext import register_atoms -import os -import re -import json -from hyperon_das import DasAPI -from hyperon_das.das import QueryOutputFormat as Format +from hyperon_das import DistributedAtomSpace + from hyperon_das.pattern_matcher import ( Link, Node, @@ -21,7 +18,7 @@ class DASpace(AbstractSpace): def __init__(self, unwrap=True): super().__init__() - self.das = DasAPI('hash_table') + self.das = DistributedAtomSpace('ram_only') self.unwrap = unwrap def _atom2dict(self, atom): diff --git a/python/tests/scripts/b5_types_prelim.metta b/python/tests/scripts/b5_types_prelim.metta index 17b4fa0ae..b98d7e5ea 100644 --- a/python/tests/scripts/b5_types_prelim.metta +++ b/python/tests/scripts/b5_types_prelim.metta @@ -81,10 +81,12 @@ (Cons (S Z) (Cons Z Nil)) ((Cons (S Z) (Cons Z Nil)))) +; TODO: MINIMAL This test has different behavior in old and new versions of the +; interpreter versions. Uncomment it after migration to the minimal MeTTa. ; This list is badly typed, because S and Z are not the same type -!(assertEqualToResult - (Cons S (Cons Z Nil)) - ((Error Z BadType))) +;!(assertEqualToResult +; (Cons S (Cons Z Nil)) +; ((Error (Cons Z Nil) BadType))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index 58d907551..10acf5b18 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -96,31 +96,31 @@ def test_frog_reasoning(self): metta = MeTTa(env_builder=Environment.test_env()) metta.run(''' - (= (Fritz croaks) True) - (= (Tweety chirps) True) - (= (Tweety yellow) True) - (= (Tweety eats_flies) True) - (= (Fritz eats_flies) True) + (= (croaks Fritz) True) + (= (chirps Tweety) True) + (= (yellow Tweety) True) + (= (eats_flies Tweety) True) + (= (eats_flies Fritz) True) ''') - fritz_frog = metta.run('!(if (and ($x croaks) ($x eats_flies)) (= ($x frog) True) nop)')[0] - self.assertEqual(metta.parse_all('(= (Fritz frog) True)'), fritz_frog) + fritz_frog = metta.run('!(if (and (croaks $x) (eats_flies $x)) (= (frog $x) True) nop)')[0] + self.assertEqual(metta.parse_all('(= (frog Fritz) True)'), fritz_frog) metta.space().add_atom(fritz_frog[0]) - self.assertEqualMettaRunnerResults([metta.parse_all('(= (Fritz green) True)')], - metta.run('!(if ($x frog) (= ($x green) True) nop)')) + self.assertEqualMettaRunnerResults([metta.parse_all('(= (green Fritz) True)')], + metta.run('!(if (frog $x) (= (green $x) True) nop)')) def test_infer_function_application_type(self): metta = MeTTa(env_builder=Environment.test_env()) metta.run(''' - (= (: (apply $f $x) $r) (and (: $f (=> $a $r)) (: $x $a))) + (= (: (apply\' $f $x) $r) (and (: $f (=> $a $r)) (: $x $a))) (= (: reverse (=> String String)) True) (= (: "Hello" String) True) ''') - output = metta.run('!(if (: (apply reverse "Hello") $t) $t Wrong)') + output = metta.run('!(if (: (apply\' reverse "Hello") $t) $t Wrong)') self.assertEqualMettaRunnerResults(output, [[S('String')]]) def test_plus_reduces_Z(self): diff --git a/python/tests/test_metta.py b/python/tests/test_metta.py index a96d2858e..2294d0ac7 100644 --- a/python/tests/test_metta.py +++ b/python/tests/test_metta.py @@ -57,3 +57,14 @@ def test_gnd_type_error(self): result = runner.run(program) self.assertEqual([[E(S('Error'), ValueAtom('String'), S('BadType'))]], result) + + def test_runner_error(self): + program = ''' + !(+ 2 3 + ''' + runner = MeTTa(env_builder=Environment.test_env()) + try: + runner.run(program) + self.assertTrue(False, "Parse error expected") + except RuntimeError as e: + self.assertEqual(e.args[0], 'Unexpected end of expression')