diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 520e3c534..1c3e43070 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust stable uses: actions-rs/toolchain@v1.0.6 @@ -57,7 +57,7 @@ jobs: args: cbindgen - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 8d4a5347e..69ea7622b 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -9,8 +9,8 @@ jobs: deploy-docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-versoin: 3.x - uses: actions/cache@v2 diff --git a/.github/workflows/minimal.yml b/.github/workflows/minimal.yml index d1841e16f..09334d3d2 100644 --- a/.github/workflows/minimal.yml +++ b/.github/workflows/minimal.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust stable uses: actions-rs/toolchain@v1.0.6 @@ -47,7 +47,7 @@ jobs: args: cbindgen - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.7 diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 32e82e4cb..23dd4fd45 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -20,17 +20,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, macos-11] + os: [ubuntu-20.04, macos-13, macos-14] max-parallel: 3 steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.10" - run: | echo "REF_NAME=${{github.ref_name}}" | tee -a $GITHUB_ENV @@ -40,10 +40,9 @@ jobs: echo "COMMIT_HEAD=${{github.ref_name != '' && github.ref_name || env.GITHUB_SHA}}" | tee -a $GITHUB_ENV - name: Build wheels on ${{ matrix.os }} - uses: pypa/cibuildwheel@v2.13.1 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_ALL: sh -c "./python/install-hyperonc.sh -u https://github.com/${{github.repository}}.git -r ${{env.COMMIT_HEAD}}" - CIBW_SKIP: "*musllinux*" with: package-dir: ./python @@ -59,9 +58,9 @@ jobs: file_glob: true - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: python-wheels + name: python-wheels-${{ matrix.os }} path: ./wheelhouse/*.whl publish-test-pypi: @@ -74,9 +73,10 @@ jobs: needs: [build-wheels] if: github.event.action == 'published' steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: python-wheels + pattern: python-wheels-* + merge-multiple: true path: dist - name: Publish package distributions to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 @@ -93,9 +93,10 @@ jobs: needs: [build-wheels] if: github.event.action == 'published' steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: python-wheels + pattern: python-wheels-* + merge-multiple: true path: dist - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/Cargo.toml b/Cargo.toml index 488539977..68e91e6ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,11 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.8" +version = "0.1.10" edition = "2021" [workspace.dependencies] -hyperon = { path = "./lib", version = "0.1.8" } +hyperon = { path = "./lib", version = "0.1.10" } regex = "1.5.4" log = "0.4.0" env_logger = "0.8.4" diff --git a/README.md b/README.md index da5cf2116..955e32fb6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,15 @@ If you want to contribute the project please see the [contribution guide](./docs If you find troubles with the installation, see the [Troubleshooting](#troubleshooting) section below. For development related instructions see the [development guide](./docs/DEVELOPMENT.md). -# Prepare environment +# Using the latest release version + +It is the most simple way of getting MeTTa interpreter especially if you are a Python developer. +The following command installs the latest release version from PyPi package repository: +``` +python3 -m pip install hyperon +``` + +# Prepare development environment ## Docker @@ -27,6 +35,17 @@ environment. Please keep in mind that resulting image contains temporary build files and takes a lot of a disk space. It is not recommended to distribute it as an image for running MeTTa because of its size. +### Ready to use image + +Run latest docker image from the Dockerhub: +``` +docker run -ti trueagi/hyperon:latest +``` + +### Build image + +Docker 26.0.0 or greater version is required. + Build Docker image from a local copy of the repo running: ``` docker build -t trueagi/hyperon . diff --git a/c/src/atom.rs b/c/src/atom.rs index 0c224969c..85f9afb67 100644 --- a/c/src/atom.rs +++ b/c/src/atom.rs @@ -315,13 +315,13 @@ pub extern "C" fn atoms_are_equivalent(a: *const atom_ref_t, b: *const atom_ref_ crate::atom::matcher::atoms_are_equivalent(a, b) } -/// @brief Returns the type of an atom +/// @brief Returns the metatype of an atom /// @ingroup atom_group /// @param[in] atom A pointer to an `atom_t` or an `atom_ref_t` to inspect /// @return An `atom_type_t` indicating the type of `atom` /// #[no_mangle] -pub unsafe extern "C" fn atom_get_type(atom: *const atom_ref_t) -> atom_type_t { +pub unsafe extern "C" fn atom_get_metatype(atom: *const atom_ref_t) -> atom_type_t { match (*atom).borrow() { Atom::Symbol(_) => atom_type_t::SYMBOL, Atom::Variable(_) => atom_type_t::VARIABLE, diff --git a/c/tests/c_space.c b/c/tests/c_space.c index 27aac750d..26ba15b13 100644 --- a/c/tests/c_space.c +++ b/c/tests/c_space.c @@ -12,7 +12,7 @@ typedef struct _atom_list_item { void collect_variable_atoms(atom_ref_t atom, void* vec_ptr) { atom_vec_t* vec = vec_ptr; - if (atom_get_type(&atom) == VARIABLE) { + if (atom_get_metatype(&atom) == VARIABLE) { atom_vec_push(vec, atom_clone(&atom)); } } diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 003b82b65..0ad489bbf 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,6 +1,6 @@ # Instructions for developers -## How to check Python release procedure locally +## How to release Python distribution packages locally Python packages are released using [cibuildwheel](https://pypi.org/project/cibuildwheel/). First step is to setup @@ -8,8 +8,11 @@ it. Usually it means setup docker and install the package from PyPi (see [setup instructions](https://cibuildwheel.pypa.io/en/stable/setup/#local)). There are additional preparations to be made before running it. First of all -`libhyperonc` library should be built and installed in a build environment. It -is done by `install-hyperonc.sh` script which is called using +`libhyperonc` library should be built and installed in a build environment. By +default library downloads and install version from the `main` branch of the +`trueagi-io/hyperon-experimental` repository. If one need to use the custom +branch then it is done by passing custom parameters to the +`install-hyperonc.sh` script which is called using [CIBW_BEFORE_ALL](https://cibuildwheel.pypa.io/en/stable/options/#before-all) environment variable: ``` @@ -24,13 +27,6 @@ the Python package is copied into container automatically. Code of the have the code in some repo accessible from the container before starting release. The simplest way is to push the changes in your GitHub repo fork. -Some platform are not supported. Use -[CIBW_SKIP](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) to skip -such platforms: -``` -export CIBW_SKIP="*musllinux*" -``` - Also one can start from building the only platform to quickly check whether release works. This can be done using [CIBW_BUILD](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) @@ -42,3 +38,72 @@ export CIBW_BUILD=cp37-manylinux_x86_64 After exporting the variables above one can start release by executing `cibuildwheel` from the `./python` directory of the repo. See [cibuildwheel documentation](https://cibuildwheel.pypa.io/en/stable/) for details. + +## How to update the version + +Usually it is needed after releasing the artifacts or before making a test +release. + +There are three locations to update: +- [/Cargo.toml](/Cargo.toml) file: + - `workspace.package.version` property + - `workspace.dependencies.hyperon.version` property +- [/python/VERSION](/python/VERSION) file + +All three locations should contain the same version. + +## How to release binaries + +Use [Create a new release +link](https://github.com/trueagi-io/hyperon-experimental/releases/new) on the +main page of the GitHub repo. Press `Choose a tag` control and type new tag +which should be in form of `v` (for example if next version is +`0.1.7` then tag is `v0.1.7`). Next version should be identical to versions +which are written in locations mentioned in [How to update the +version](#how-to-update-the-version) instruction. After typing the tag press +`Create new tag on publish`. Now press `Generate release notes` button. It will +automatically fill the `Release title` and `Release description` fields. Tick +`Set as a pre-release` checkbox if needed and press `Publish release` button. +Now you have published new GitHub release and triggered a job to build release +artifacts. + +After release job is finished one need to approve publishing artifacts to the +PyPi repository. Before approving one can download and test Python wheels +built. To check the job status go the `Actions/release-pyhon` page, and select +last workflow run. Links to the archives with the artifacts are located at the +bottom of the page. + +If distribution wheels are good then one can approve the publishing process. At +the top of the workflow run page there are two blocks `Publish to Test PyPi` +and `Publish to PyPi`. First press `Publish to Test PyPi` block approve it and +wait for publishing. It is critical to start from Test PyPi because release +cannot be removed from the PyPi after publishing. + +After release is published check it can be installed executing: +``` +python3 -m pip install --index-url https://test.pypi.org/simple/ hyperon +``` +Check that the latest published version was downloaded and installed. If you +are making a test release then you should not publish it to the PyPi. If it is +a production release then proceed with `Publish to PyPi` block. + +## How to check release job in fork + +First you need to select the test release version. It should contain an +additional version digit after the latest officially released version. Let's +say the latest released version is `0.1.7`. Then the test release version +should be `0.1.7.x` for instance `0.1.7.1`. Start from 1 and increment it after +each release you published successfully. + +Make a separate branch to release the code. It is not necessary but it is +highly recommended to not pollute the main branch of the fork. In order to be +able releasing from the branch one need to temporary make it default branch. It +is done by using GitHub repo `Settings/General/Default branch` control. + +[Update the version](#how-to-update-the-version) in the branch to the test +release version you constructed. Commit and push this change in your test +branch. Now you are ready to make a test release. See [release +binaries instruction](#how-to-release-binaries). + +After testing the release procedure remove the commit with version update from +your branch. And set default branch setting to the previous value. diff --git a/lib/src/atom/matcher.rs b/lib/src/atom/matcher.rs index 22c874610..9246ee352 100644 --- a/lib/src/atom/matcher.rs +++ b/lib/src/atom/matcher.rs @@ -1116,6 +1116,13 @@ pub fn match_result_product(prev: MatchResultIter, next: MatchResultIter) -> Mat Box::new(prev.merge(&next).into_iter()) } +/// Applies bindings to atom and return it (see [apply_bindings_to_atom_mut]). +#[inline] +pub fn apply_bindings_to_atom_move(mut atom: Atom, bindings: &Bindings) -> Atom { + apply_bindings_to_atom_mut(&mut atom, bindings); + atom +} + /// Applies bindings to atom. Function replaces all variables in atom by /// corresponding bindings. /// @@ -1123,25 +1130,30 @@ pub fn match_result_product(prev: MatchResultIter, next: MatchResultIter) -> Mat /// /// ``` /// use hyperon::*; -/// use hyperon::atom::matcher::apply_bindings_to_atom; +/// use hyperon::atom::matcher::apply_bindings_to_atom_mut; /// /// let binds = bind!{ y: expr!("Y") }; -/// let atom = apply_bindings_to_atom(&expr!("+" "X" y), &binds); +/// let mut atom = expr!("+" "X" y); +/// apply_bindings_to_atom_mut(&mut atom, &binds); /// /// assert_eq!(atom, expr!("+" "X" "Y")); /// ``` -pub fn apply_bindings_to_atom(atom: &Atom, bindings: &Bindings) -> Atom { - let mut result = atom.clone(); +pub fn apply_bindings_to_atom_mut(atom: &mut Atom, bindings: &Bindings) { + let trace_atom = match log::log_enabled!(log::Level::Trace) { + true => Some(atom.clone()), + false => None, + }; if !bindings.is_empty() { - result.iter_mut().for_each(|atom| match atom { + atom.iter_mut().for_each(|atom| match atom { Atom::Variable(var) => { bindings.resolve(var).map(|value| *atom = value); }, _ => {}, }); } - log::trace!("apply_bindings_to_atom: {} | {} -> {}", atom, bindings, result); - result + if let Some(atom_copy) = trace_atom { + log::trace!("apply_bindings_to_atom: {} | {} -> {}", atom_copy, bindings, atom); + } } /// Applies bindings `from` to the each value from bindings `to`. diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index 12647ad88..2149c1ec4 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -250,7 +250,7 @@ impl InterpreterCache { fn insert(&mut self, key: Atom, mut value: Results) { value.iter_mut().for_each(|res| { let vars: HashSet<&VariableAtom> = key.iter().filter_type::<&VariableAtom>().collect(); - res.0 = apply_bindings_to_atom(&res.0, &res.1); + apply_bindings_to_atom_mut(&mut res.0, &res.1); res.1.retain(|v| vars.contains(v)); }); self.0.insert(key, value) @@ -362,7 +362,7 @@ fn interpret_as_type_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a fn cast_atom_to_type_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a, T>, input: InterpretedAtom, typ: Atom) -> StepResult<'a, Results, InterpreterError> { // TODO: implement this via interpreting of the (:cast atom typ) expression - let typ = apply_bindings_to_atom(&typ, input.bindings()); + let typ = apply_bindings_to_atom_move(typ, input.bindings()); let mut results = get_type_bindings(&context.space, input.atom(), &typ); log::debug!("cast_atom_to_type_plan: type check results: {:?}", results); if !results.is_empty() { @@ -373,7 +373,7 @@ fn cast_atom_to_type_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a // should we apply bindings to bindings? let bindings = Bindings::merge(&bindings, &typ_bindings); if let Some(bindings) = bindings { - let atom = apply_bindings_to_atom(&atom, &bindings); + let atom = apply_bindings_to_atom_move(atom, &bindings); Some(InterpretedAtom(atom, bindings)) } else { None @@ -446,7 +446,7 @@ fn interpret_expression_as_type_op<'a, T: SpaceRef<'a>>(context: InterpreterCont plan, OperatorPlan::new(move |results: Results| { make_alternives_plan(arg.clone(), results, move |result| -> NoInputPlan { - let arg_typ = apply_bindings_to_atom(&arg_typ, result.bindings()); + let arg_typ = apply_bindings_to_atom_move(arg_typ.clone(), result.bindings()); Box::new(SequencePlan::new( interpret_as_type_plan(context.clone(), InterpretedAtom(arg.clone(), result.bindings().clone()), @@ -500,7 +500,7 @@ fn insert_reducted_arg_op<'a>(expr: InterpretedAtom, atom_idx: usize, mut arg_va let InterpretedAtom(arg, bindings) = arg; let mut expr_with_arg = expr.atom().clone(); get_expr_mut(&mut expr_with_arg).children_mut()[atom_idx] = arg; - InterpretedAtom(apply_bindings_to_atom(&expr_with_arg, &bindings), bindings) + InterpretedAtom(apply_bindings_to_atom_move(expr_with_arg, &bindings), bindings) }).collect(); log::debug!("insert_reducted_arg_op: result: {:?}", result); StepResult::ret(result) @@ -626,7 +626,7 @@ fn match_op<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a, T>, input: I let results: Vec = query_bindings .drain(0..) .map(|query_binding| { - let result = apply_bindings_to_atom(&Atom::Variable(var_x.clone()), &query_binding); + let result = apply_bindings_to_atom_move(Atom::Variable(var_x.clone()), &query_binding); // TODO: sometimes we apply bindings twice: first time here, // second time when inserting matched argument into nesting // expression. It should be enough doing it only once. diff --git a/lib/src/metta/interpreter_minimal.rs b/lib/src/metta/interpreter_minimal.rs index 54726793d..33a99a1f9 100644 --- a/lib/src/metta/interpreter_minimal.rs +++ b/lib/src/metta/interpreter_minimal.rs @@ -7,14 +7,12 @@ use crate::*; use crate::atom::matcher::*; use crate::space::*; -use crate::space::grounding::*; use crate::metta::*; use std::fmt::{Debug, Display, Formatter}; use std::convert::TryFrom; use std::collections::HashSet; use std::rc::Rc; -use std::marker::PhantomData; use std::fmt::Write; use std::cell::RefCell; @@ -35,6 +33,14 @@ macro_rules! match_atom { } }; } + +/// Operation return handler, it is triggered when nested operation is finished +/// and returns its results. First argument gets the reference to the stack +/// which on the top has the frame of the wrapping operation. Last two +/// arguments are the result of the nested operation. Handler returns +/// None when it is not ready to provide new stack (it can happen in case of +/// collapse-bind operation) or new stack with variable bindings to continue +/// execution of the program. type ReturnHandler = fn(Rc>, Atom, Bindings) -> Option<(Stack, Bindings)>; #[derive(Debug, Clone)] @@ -58,10 +64,14 @@ fn no_handler(_stack: Rc>, _atom: Atom, _bindings: Bindings) -> O } impl Stack { + fn from_prev_with_vars(prev: Option>>, atom: Atom, vars: Variables, ret: ReturnHandler) -> Self { + Self{ prev, atom, ret, finished: false, vars } + } + fn from_prev_add_vars(prev: Option>>, atom: Atom, ret: ReturnHandler) -> Self { // TODO: vars are introduced in specific locations of the atom thus // in theory it is possible to optimize vars search for eval, unify and chain - let vars = Self::vars(&prev, &atom); + let vars = Self::add_vars_atom(&prev, &atom); Self{ prev, atom, ret, finished: false, vars } } @@ -71,17 +81,7 @@ impl Stack { } fn finished(prev: Option>>, atom: Atom) -> Self { - let vars = Self::vars_copy(&prev); - Self{ prev, atom, ret: no_handler, finished: true, vars } - } - - fn finished_add_vars(prev: Option>>, atom: Atom) -> Self { - let vars = Self::vars(&prev, &atom); - Self{ prev, atom, ret: no_handler, finished: true, vars } - } - - fn finished_with_vars(prev: Option>>, atom: Atom, vars: Variables) -> Self { - Self{ prev, atom, ret: no_handler, finished: true, vars } + Self{ prev, atom, ret: no_handler, finished: true, vars: Variables::new() } } fn len(&self) -> usize { @@ -104,14 +104,15 @@ impl Stack { } } - fn vars(prev: &Option>>, atom: &Atom) -> Variables { - // TODO: nested atoms are visited twice: first time when outer atom - // is visited, next time when internal atom is visited. - let vars: Variables = atom.iter().filter_type::<&VariableAtom>().cloned().collect(); - match (prev, vars.is_empty()) { - (Some(prev), true) => prev.borrow().vars.clone(), - (Some(prev), false) => prev.borrow().vars.clone().union(vars), - (None, _) => vars, + #[inline] + fn add_vars_atom(prev: &Option>>, atom: &Atom) -> Variables { + Self::add_vars_it(prev, vars_from_atom(atom)) + } + + fn add_vars_it<'a, I: 'a + Iterator>(prev: &Option>>, vars: I) -> Variables { + match prev { + Some(prev) => prev.borrow().vars.clone().insert_all(vars), + None => vars.cloned().collect(), } } } @@ -157,27 +158,28 @@ impl Display for InterpretedAtom { } } -pub trait SpaceRef<'a> : Space + 'a {} -impl<'a, T: Space + 'a> SpaceRef<'a> for T {} - #[derive(Debug)] -struct InterpreterContext<'a, T: SpaceRef<'a>> { +struct InterpreterContext { space: T, - phantom: PhantomData<&'a GroundingSpace>, } -impl<'a, T: SpaceRef<'a>> InterpreterContext<'a, T> { +impl InterpreterContext { fn new(space: T) -> Self { - Self{ space, phantom: PhantomData } + Self{ space } } } +// TODO: This wrapper is for compatibility with interpreter.rs only +pub trait SpaceRef<'a> : Space + 'a {} +impl<'a, T: Space + 'a> SpaceRef<'a> for T {} + #[derive(Debug)] pub struct InterpreterState<'a, T: SpaceRef<'a>> { plan: Vec, finished: Vec, - context: InterpreterContext<'a, T>, + context: InterpreterContext, vars: HashSet, + phantom: std::marker::PhantomData>, } fn atom_as_slice(atom: &Atom) -> Option<&[Atom]> { @@ -202,6 +204,7 @@ impl<'a, T: SpaceRef<'a>> InterpreterState<'a, T> { finished: results, context: InterpreterContext::new(space), vars: HashSet::new(), + phantom: std::marker::PhantomData, } } @@ -226,7 +229,7 @@ impl<'a, T: SpaceRef<'a>> InterpreterState<'a, T> { let InterpretedAtom(stack, bindings) = atom; if stack.atom != EMPTY_SYMBOL { let bindings = bindings.convert_var_equalities_to_bindings(&self.vars); - let atom = apply_bindings_to_atom(&stack.atom, &bindings); + let atom = apply_bindings_to_atom_move(stack.atom, &bindings); self.finished.push(atom); } } else { @@ -255,6 +258,7 @@ pub fn interpret_init<'a, T: Space + 'a>(space: T, expr: &Atom) -> InterpreterSt finished: vec![], context, vars: expr.iter().filter_type::<&VariableAtom>().cloned().collect(), + phantom: std::marker::PhantomData, } } @@ -268,7 +272,8 @@ pub fn interpret_init<'a, T: Space + 'a>(space: T, expr: &Atom) -> InterpreterSt pub fn interpret_step<'a, T: Space + 'a>(mut state: InterpreterState<'a, T>) -> InterpreterState<'a, T> { let interpreted_atom = state.pop().unwrap(); log::debug!("interpret_step:\n{}", interpreted_atom); - for result in interpret_root_atom(&state.context, interpreted_atom) { + let InterpretedAtom(stack, bindings) = interpreted_atom; + for result in interpret_stack(&state.context, stack, bindings) { state.push(result); } state @@ -323,13 +328,17 @@ impl Variables { fn new() -> Self { Self(im::HashSet::new()) } - fn is_empty(&self) -> bool { - self.0.is_empty() + fn insert(&mut self, var: VariableAtom) -> Option { + self.0.insert(var) } - fn union(self, other: Self) -> Self { - Variables(self.0.union(other.0)) + fn insert_all<'a, I: 'a + Iterator>(mut self, it: I) -> Self { + it.for_each(|var| { self.insert(var.clone()); }); + self } } +fn vars_from_atom(atom: &Atom) -> impl Iterator { + atom.iter().filter_type::<&VariableAtom>() +} impl FromIterator for Variables { fn from_iter>(iter: I) -> Self { @@ -359,12 +368,7 @@ impl Display for Variables { } } -fn interpret_root_atom<'a, T: SpaceRef<'a>>(context: &InterpreterContext<'a, T>, interpreted_atom: InterpretedAtom) -> Vec { - let InterpretedAtom(stack, bindings) = interpreted_atom; - interpret_nested_atom(context, stack, bindings) -} - -fn interpret_nested_atom<'a, T: SpaceRef<'a>>(context: &InterpreterContext<'a, T>, mut stack: Stack, bindings: Bindings) -> Vec { +fn interpret_stack<'a, T: Space>(context: &InterpreterContext, mut stack: Stack, bindings: Bindings) -> Vec { if stack.finished { // first executed minimal operation returned error if stack.prev.is_none() { @@ -426,8 +430,8 @@ fn finished_result(atom: Atom, bindings: Bindings, prev: Option>(context: &InterpreterContext<'a, T>, stack: Stack, bindings: Bindings) -> Vec { - let Stack{ prev, atom: eval, ret: _, finished: _, vars} = stack; +fn eval<'a, T: Space>(context: &InterpreterContext, stack: Stack, bindings: Bindings) -> Vec { + let Stack{ prev, atom: eval, ret: _, finished: _, vars: _} = stack; let query_atom = match_atom!{ eval ~ [_op, query] => query, _ => { @@ -476,7 +480,7 @@ fn eval<'a, T: SpaceRef<'a>>(context: &InterpreterContext<'a, T>, stack: Stack, }, _ if is_embedded_op(&query_atom) => vec![InterpretedAtom(atom_to_stack(query_atom, prev), bindings)], - _ => query(&context.space, prev, query_atom, bindings, vars), + _ => query(&context.space, prev, query_atom, bindings), } } @@ -493,7 +497,7 @@ fn is_variable_op(atom: &Atom) -> bool { } } -fn query<'a, T: SpaceRef<'a>>(space: T, prev: Option>>, atom: Atom, bindings: Bindings, _vars: Variables) -> Vec { +fn query<'a, T: Space>(space: T, prev: Option>>, atom: Atom, bindings: Bindings) -> Vec { #[cfg(not(feature = "variable_operation"))] if is_variable_op(&atom) { // TODO: This is a hotfix. Better way of doing this is adding @@ -511,19 +515,20 @@ fn query<'a, T: SpaceRef<'a>>(space: T, prev: Option>>, atom: results.len(), bindings.len(), results, bindings); results.into_iter() .flat_map(|b| { - let res = apply_bindings_to_atom(&atom_x, &b); + let res = apply_bindings_to_atom_move(atom_x.clone(), &b); + let vars = Stack::add_vars_atom(&prev, &res); let stack = if is_function_op(&res) { let call = Stack::from_prev_add_vars(prev.clone(), atom.clone(), call_ret); atom_to_stack(res, Some(Rc::new(RefCell::new(call)))) } else { - Stack::finished_add_vars(prev.clone(), res) + Stack::finished(prev.clone(), res) }; log::debug!("interpreter_minimal::query: b: {}", b); b.merge_v2(&bindings).into_iter().filter_map(move |mut b| { if b.has_loops() { None } else { - b.retain(|v| stack.vars.contains(v)); + b.retain(|v| vars.contains(v)); Some(InterpretedAtom(stack.clone(), b)) } }) @@ -540,34 +545,32 @@ fn query<'a, T: SpaceRef<'a>>(space: T, prev: Option>>, atom: fn atom_to_stack(atom: Atom, prev: Option>>) -> Stack { let expr = atom_as_slice(&atom); let result = match expr { - Some([op, ..]) if *op == CHAIN_SYMBOL => { - chain_to_stack(atom, prev) - }, - Some([op, ..]) if *op == FUNCTION_SYMBOL => { - function_to_stack(atom, prev) - }, - Some([op, ..]) if *op == EVAL_SYMBOL - || *op == UNIFY_SYMBOL => { - Stack::from_prev_add_vars(prev, atom, no_handler) - }, - _ => { - Stack::from_prev_keep_vars(prev, atom, no_handler) - }, + Some([op, ..]) if *op == CHAIN_SYMBOL => + chain_to_stack(atom, prev), + Some([op, ..]) if *op == FUNCTION_SYMBOL => + function_to_stack(atom, prev), + Some([op, ..]) if *op == EVAL_SYMBOL => + Stack::from_prev_keep_vars(prev, atom, no_handler), + Some([op, ..]) if *op == UNIFY_SYMBOL => + unify_to_stack(atom, prev), + _ => + Stack::from_prev_keep_vars(prev, atom, no_handler), }; result } fn chain_to_stack(mut atom: Atom, prev: Option>>) -> Stack { let mut nested = Atom::sym("%Nested%"); - let nested_arg = match atom_as_slice_mut(&mut atom) { - Some([_op, nested, Atom::Variable(_var), _templ]) => nested, + let (nested_arg, templ_arg) = match atom_as_slice_mut(&mut atom) { + Some([_op, nested, Atom::Variable(_var), templ]) => (nested, templ), _ => { let error: String = format!("expected: ({} (: Variable) ), found: {}", CHAIN_SYMBOL, atom); return Stack::finished(prev, error_atom(atom, error)); }, }; std::mem::swap(nested_arg, &mut nested); - let cur = Stack::from_prev_add_vars(prev, atom, chain_ret); + let vars = Stack::add_vars_it(&prev, vars_from_atom(templ_arg)); + let cur = Stack::from_prev_with_vars(prev, atom, vars, chain_ret); atom_to_stack(nested, Some(Rc::new(RefCell::new(cur)))) } @@ -592,8 +595,8 @@ fn chain(stack: Stack, bindings: Bindings) -> Vec { } }; let b = Bindings::new().add_var_binding_v2(var, nested).unwrap(); - let result = apply_bindings_to_atom(&templ, &b); - vec![InterpretedAtom(atom_to_stack(result, prev), bindings)] + let templ = apply_bindings_to_atom_move(templ, &b); + vec![InterpretedAtom(atom_to_stack(templ, prev), bindings)] } fn function_to_stack(mut atom: Atom, prev: Option>>) -> Stack { @@ -660,18 +663,15 @@ fn collapse_bind_ret(stack: Rc>, atom: Atom, bindings: Bindings) finished.children_mut().push(atom_bindings_into_atom(nested, bindings)); } + // all alternatives are evaluated match Rc::into_inner(stack).map(RefCell::into_inner) { Some(stack) => { - let Stack{ prev, atom: collapse, ret: _, finished: _, mut vars } = stack; + let Stack{ prev, atom: collapse, ret: _, finished: _, vars: _ } = stack; let (result, bindings) = match atom_into_array(collapse) { Some([_op, result, bindings]) => (result, atom_into_bindings(bindings)), None => panic!("Unexpected state"), }; - for r in <&ExpressionAtom>::try_from(&result).unwrap().children() { - let (_, bindings) = atom_get_atom_bindings(r); - vars = vars.union(bindings.vars().cloned().collect()); - } - Some((Stack::finished_with_vars(prev, result, vars), bindings)) + Some((Stack::finished(prev, result), bindings)) }, None => None, } @@ -681,30 +681,25 @@ fn atom_bindings_into_atom(atom: Atom, bindings: Bindings) -> Atom { Atom::expr([atom, Atom::value(bindings)]) } -fn atom_get_atom_bindings(pair: &Atom) -> (&Atom, &Bindings) { - match atom_as_slice(pair) { - Some([atom, bindings]) => (atom, atom_get_bindings(bindings)), +fn unify_to_stack(mut atom: Atom, prev: Option>>) -> Stack { + let () = match atom_as_slice_mut(&mut atom) { + Some([_op, _a, _b, _then, _else]) => (), _ => { - panic!("(Atom Bindings) pair is expected, {} was received", pair) - } - } -} - -fn atom_get_bindings(bindings: &Atom) -> &Bindings { - match bindings.as_gnd::() { - Some(bindings) => bindings, - _ => panic!("Unexpected state: second item cannot be converted to Bindings"), - } + let error: String = format!("expected: ({} ), found: {}", UNIFY_SYMBOL, atom); + return Stack::finished(prev, error_atom(atom, error)); + }, + }; + Stack::from_prev_with_vars(prev, atom, Variables::new(), no_handler) } fn unify(stack: Stack, bindings: Bindings) -> Vec { - let Stack{ prev, atom: unify, ret: _, finished: _, vars } = stack; - let (atom, pattern, then, else_) = match atom_as_slice(&unify) { - Some([_op, atom, pattern, then, else_]) => (atom, pattern, then, else_), + let Stack{ prev, atom: unify, ret: _, finished: _, vars: _ } = stack; + let (atom, pattern, then, else_) = match_atom!{ + unify ~ [_op, atom, pattern, then, else_] => (atom, pattern, then, else_), _ => { let error: String = format!("expected: ({} ), found: {}", UNIFY_SYMBOL, unify); return finished_result(error_atom(unify, error), bindings, prev); - }, + } }; // TODO: Should unify() be symmetrical or not. While it is symmetrical then @@ -712,12 +707,15 @@ fn unify(stack: Stack, bindings: Bindings) -> Vec { // priority. Thus interpreter can use any of them further. This sometimes // looks unexpected. For example see `metta_car` unit test where variable // from car's argument is replaced. - let matches: Vec = match_atoms(atom, pattern).collect(); + let matches: Vec = match_atoms(&atom, &pattern).collect(); if matches.is_empty() { + let vars = Stack::add_vars_it(&prev, vars_from_atom(&else_)); let bindings = bindings.narrow_vars(&vars); - let result = apply_bindings_to_atom(else_, &bindings); - finished_result(result, bindings, prev) + let else_ = apply_bindings_to_atom_move(else_, &bindings); + finished_result(else_, bindings, prev) } else { + let then = &then; + let vars = Stack::add_vars_it(&prev, vars_from_atom(then)); matches.into_iter() .flat_map(move |b| { let b = b.narrow_vars(&vars); @@ -726,7 +724,7 @@ fn unify(stack: Stack, bindings: Bindings) -> Vec { if b.has_loops() { None } else { - let then = apply_bindings_to_atom(then, &b); + let then = apply_bindings_to_atom_move(then.clone(), &b); Some(InterpretedAtom(Stack::finished(prev.clone(), then), b)) } }) @@ -798,13 +796,14 @@ fn superpose_bind(stack: Stack, bindings: Bindings) -> Vec { .map(atom_into_atom_bindings) .flat_map(|(atom, b)| { let prev = &prev; + let vars = Stack::add_vars_atom(prev, &atom); b.merge_v2(&bindings).into_iter() .filter_map(move |b| { if b.has_loops() { None } else { - let stack = Stack::finished_add_vars(prev.clone(), atom.clone()); - let b = b.narrow_vars(&stack.vars); + let stack = Stack::finished(prev.clone(), atom.clone()); + let b = b.narrow_vars(&vars); Some(InterpretedAtom(stack, b)) } }) @@ -1068,9 +1067,8 @@ mod tests { let result = superpose_bind(stack, bind!{ b: expr!("B"), d: expr!("D") }); - let expected_vars: Variables = [ "a", "b" ].into_iter().map(VariableAtom::new).collect(); assert_eq!(result, vec![InterpretedAtom( - Stack{ prev: None, atom: expr!("foo" a b), ret: no_handler, finished: true, vars: expected_vars }, + Stack{ prev: None, atom: expr!("foo" a b), ret: no_handler, finished: true, vars: Variables::new() }, bind!{ a: expr!("A"), b: expr!("B") } )]); } @@ -1159,7 +1157,7 @@ mod tests { metta_space(text) } - fn call_interpret<'a, T: SpaceRef<'a>>(space: T, atom: &Atom) -> Vec { + fn call_interpret<'a, T: Space>(space: T, atom: &Atom) -> Vec { let result = interpret(space, atom); assert!(result.is_ok()); result.unwrap() diff --git a/lib/src/metta/runner/stdlib.metta b/lib/src/metta/runner/stdlib.metta index a1b767108..7217e243c 100644 --- a/lib/src/metta/runner/stdlib.metta +++ b/lib/src/metta/runner/stdlib.metta @@ -35,10 +35,10 @@ (@param "Atomspace to add atom into") (@param "Atom to add"))) (@return "Unit atom")) -(: add-reduct (-> Space %Undefined% (->))) -(= (add-reduct $dst $atom) (add-atom $dst $atom)) +(: add-reduct (-> hyperon::space::DynSpace %Undefined% (->))) +(= (add-reduct $dst $atom) (add-atom $dst $atom)) -(@doc add-reduct +(@doc quote (@desc "Prevents atom from being reduced") (@params ( (@param "Atom"))) @@ -50,7 +50,7 @@ (: unify (-> Atom Atom Atom Atom %Undefined%)) (= (unify $a $a $then $else) $then) (= (unify $a $b $then $else) - (case (unify-or-empty $a $b) ((%void% $else))) ) + (case (unify-or-empty $a $b) ((Empty $else))) ) (: unify-or-empty (-> Atom Atom Atom)) (= (unify-or-empty $a $a) unified) (= (unify-or-empty $a $b) (empty)) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index e2aa62668..b10a21fb2 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -18,8 +18,6 @@ use regex::Regex; use super::arithmetics::*; use super::string::*; -pub const VOID_SYMBOL : Atom = sym!("%void%"); - fn unit_result() -> Result, ExecError> { Ok(vec![UNIT_ATOM()]) } @@ -172,7 +170,7 @@ impl Display for IncludeOp { impl Grounded for IncludeOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, UNIT_TYPE()]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { @@ -195,13 +193,16 @@ impl Grounded for IncludeOp { let program_text = String::from_utf8(program_buf) .map_err(|e| e.to_string())?; let parser = crate::metta::text::OwnedSExprParser::new(program_text); - //QUESTION: do we want to do anything with the result from the `include` operation? - let _eval_result = context.run_inline(|context| { + let eval_result = context.run_inline(|context| { context.push_parser(Box::new(parser)); Ok(()) })?; - unit_result() + //NOTE: Current behavior returns the result of the last sub-eval to match the old + // `import!` before before module isolation. However that means the results prior to + // the last are dropped. I don't know how to fix this or if it's even wrong, but it's + // different from the way "eval-type" APIs work when called from host code, e.g. Rust + Ok(eval_result.into_iter().last().unwrap_or_else(|| vec![])) } fn match_(&self, other: &Atom) -> MatchResultIter { @@ -238,7 +239,7 @@ impl Display for ModSpaceOp { impl Grounded for ModSpaceOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, UNIT_TYPE()]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, rust_type_atom::()]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { @@ -1184,7 +1185,7 @@ mod non_minimal_only_stdlib { Ok(result) if result.is_empty() => { cases.into_iter() .find_map(|(pattern, template, _external_vars)| { - if pattern == VOID_SYMBOL { + if pattern == EMPTY_SYMBOL { Some(template) } else { None @@ -1234,7 +1235,7 @@ mod non_minimal_only_stdlib { for (pattern, template, external_vars) in cases { let bindings = matcher::match_atoms(atom, &pattern) .map(|b| b.convert_var_equalities_to_bindings(&external_vars)); - let result: Vec = bindings.map(|b| matcher::apply_bindings_to_atom(&template, &b)).collect(); + let result: Vec = bindings.map(|b| matcher::apply_bindings_to_atom_move(template.clone(), &b)).collect(); if !result.is_empty() { return result } @@ -1454,7 +1455,7 @@ mod non_minimal_only_stdlib { let bindings = matcher::match_atoms(&pattern, &atom) .map(|b| b.convert_var_equalities_to_bindings(&external_vars)); - let result = bindings.map(|b| { matcher::apply_bindings_to_atom(&template, &b) }).collect(); + let result = bindings.map(|b| { matcher::apply_bindings_to_atom_move(template.clone(), &b) }).collect(); log::debug!("LetOp::execute: pattern: {}, atom: {}, template: {}, result: {:?}", pattern, atom, template, result); Ok(result) } @@ -1861,10 +1862,10 @@ mod tests { let case_op = CaseOp::new(space.clone()); assert_eq!(case_op.execute(&mut vec![expr!(("foo")), - expr!(((n "B") n) ("%void%" "D"))]), + expr!(((n "B") n) ("Empty" "D"))]), Ok(vec![Atom::sym("A")])); assert_eq!(case_op.execute(&mut vec![expr!({MatchOp{}} {space} ("B" "C") ("C" "B")), - expr!(((n "C") n) ("%void%" "D"))]), + expr!(((n "C") n) ("Empty" "D"))]), Ok(vec![Atom::sym("D")])); } diff --git a/lib/src/metta/runner/stdlib_minimal.metta b/lib/src/metta/runner/stdlib_minimal.metta index b40442332..0c5061e9f 100644 --- a/lib/src/metta/runner/stdlib_minimal.metta +++ b/lib/src/metta/runner/stdlib_minimal.metta @@ -212,8 +212,8 @@ (eval (match-types $actual-ret-type $ret-type (return ()) (return (Error $atom BadType)) )) - (return (Error $atom BadType)) ))) - (return (Error $atom "Too many arguments")) )) + (return (Error $atom IncorrectNumberOfArguments)) ))) + (return (Error $atom IncorrectNumberOfArguments)) )) (eval (if-decons-expr $args $head $tail (eval (if-decons-expr $arg-types $head-type $tail-types (chain (eval (interpret $head $head-type $space)) $reduced-head diff --git a/lib/src/metta/runner/stdlib_minimal.rs b/lib/src/metta/runner/stdlib_minimal.rs index c64af4637..54bcba1f6 100644 --- a/lib/src/metta/runner/stdlib_minimal.rs +++ b/lib/src/metta/runner/stdlib_minimal.rs @@ -16,8 +16,6 @@ use std::convert::TryInto; use super::arithmetics::*; use super::string::*; -pub const VOID_SYMBOL : Atom = sym!("%void%"); - fn unit_result() -> Result, ExecError> { Ok(vec![UNIT_ATOM()]) } @@ -410,9 +408,7 @@ impl Grounded for CaseOp { log::debug!("CaseOp::execute: atom results: {:?}", results); let results = match results { Ok(results) if results.is_empty() => - // TODO: MINIMAL in minimal MeTTa we should use Empty in both - // places here and in (case ...) calls in code - vec![switch(VOID_SYMBOL)], + 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)])], @@ -633,13 +629,13 @@ mod tests { #[test] fn metta_case_empty() { - let result = run_program("!(case Empty ( (ok ok) (%void% nok) ))"); + 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) (%void% 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 (unify (B C) (C B) ok Empty) ( (ok ok) (%void% nok) ))"); + let result = run_program("!(case (unify (B C) (C B) ok Empty) ( (ok ok) (Empty nok) ))"); assert_eq!(result, Ok(vec![vec![expr!("nok")]])); } @@ -757,11 +753,6 @@ mod tests { !(eval (interpret (Cons S (Cons Z Nil)) %Undefined% &self)) "); assert_eq!(result, Ok(vec![vec![expr!("Error" ("Cons" "Z" "Nil") "BadType")]])); - let result = run_program(" - (: foo (-> Atom Atom Atom)) - !(eval (interpret (foo A) %Undefined% &self)) - "); - assert_eq!(result, Ok(vec![vec![expr!("Error" ("foo" "A") "BadType")]])); } #[test] @@ -1042,6 +1033,36 @@ mod tests { Ok(vec![vec![expr!("Error" "myAtom" "BadType")]])); } + #[test] + fn test_return_incorrect_number_of_args_error() { + let program1 = " + (: a A) + (: b B) + (: c C) + (: foo (-> A B C)) + (= (foo $a $b) c) + + !(eval (interpret (foo a b) %Undefined% &self)) + "; + + let metta = Metta::new(Some(EnvBuilder::test_env())); + metta.tokenizer().borrow_mut().register_token(Regex::new("id_num").unwrap(), + |_| Atom::gnd(ID_NUM)); + + assert_eq!(metta.run(SExprParser::new(program1)), + Ok(vec![vec![expr!("c")]])); + + let program2 = "!(eval (interpret (foo a) %Undefined% &self))"; + + assert_eq!(metta.run(SExprParser::new(program2)), + Ok(vec![vec![expr!("Error" ("foo" "a") "IncorrectNumberOfArguments")]])); + + let program3 = "!(eval (interpret (foo a b c) %Undefined% &self))"; + + assert_eq!(metta.run(SExprParser::new(program3)), + Ok(vec![vec![expr!("Error" ("foo" "a" "b" "c") "IncorrectNumberOfArguments")]])); + } + #[test] fn use_sealed_to_make_scoped_variable() { assert_eq!(run_program("!(let $x (input $x) (output $x))"), Ok(vec![vec![]])); diff --git a/lib/src/metta/types.rs b/lib/src/metta/types.rs index 40245de2a..caed9cc0c 100644 --- a/lib/src/metta/types.rs +++ b/lib/src/metta/types.rs @@ -19,7 +19,7 @@ //! of `%Undefined%` type can be matched with any type required. use super::*; -use crate::atom::matcher::{Bindings, BindingsSet, apply_bindings_to_atom}; +use crate::atom::matcher::{Bindings, BindingsSet, apply_bindings_to_atom_move}; use crate::space::Space; fn typeof_query(atom: &Atom, typ: &Atom) -> Atom { @@ -39,7 +39,7 @@ fn query_super_types(space: &dyn Space, sub_type: &Atom) -> Vec { let var_x = VariableAtom::new("X").make_unique(); let mut super_types = space.query(&isa_query(&sub_type, &Atom::Variable(var_x.clone()))); let atom_x = Atom::Variable(var_x); - super_types.drain(0..).map(|bindings| { apply_bindings_to_atom(&atom_x, &bindings) }).collect() + super_types.drain(0..).map(|bindings| { apply_bindings_to_atom_move(atom_x.clone(), &bindings) }).collect() } fn add_super_types(space: &dyn Space, sub_types: &mut Vec, from: usize) { @@ -108,7 +108,7 @@ fn query_types(space: &dyn Space, atom: &Atom) -> Vec { let mut types = query_has_type(space, atom, &Atom::Variable(var_x.clone())); let atom_x = Atom::Variable(var_x); let mut types = types.drain(0..).filter_map(|bindings| { - let atom = apply_bindings_to_atom(&atom_x, &bindings); + let atom = apply_bindings_to_atom_move(atom_x.clone(), &bindings); if atom_x == atom { None } else { @@ -286,7 +286,7 @@ fn get_application_types(space: &dyn Space, atom: &Atom, expr: &ExpressionAtom) has_function_types = true; let (expected_arg_types, ret_typ) = get_arg_types(&fn_type); for bindings in check_arg_types(actual_arg_types.as_slice(), meta_arg_types.as_slice(), expected_arg_types, Bindings::new()) { - types.push(apply_bindings_to_atom(&ret_typ, &bindings)); + types.push(apply_bindings_to_atom_move(ret_typ.clone(), &bindings)); } } log::trace!("get_application_types: function application {} types {:?}", atom, types); diff --git a/lib/src/space/grounding.rs b/lib/src/space/grounding.rs index 39a759fc2..b4308e840 100644 --- a/lib/src/space/grounding.rs +++ b/lib/src/space/grounding.rs @@ -253,7 +253,7 @@ impl GroundingSpace { acc } else { acc.drain(0..).flat_map(|prev| -> BindingsSet { - let query = matcher::apply_bindings_to_atom(&query, &prev); + let query = matcher::apply_bindings_to_atom_move(query.clone(), &prev); let mut res = self.query(&query); res.drain(0..) .flat_map(|next| next.merge_v2(&prev)) diff --git a/lib/src/space/mod.rs b/lib/src/space/mod.rs index b760154aa..e6d8f5fcb 100644 --- a/lib/src/space/mod.rs +++ b/lib/src/space/mod.rs @@ -9,7 +9,7 @@ use std::cell::{RefCell, Ref, RefMut}; use crate::common::FlexRef; use crate::atom::*; -use crate::atom::matcher::{BindingsSet, apply_bindings_to_atom}; +use crate::atom::matcher::{BindingsSet, apply_bindings_to_atom_move}; /// Contains information about space modification event. #[derive(Clone, Debug, PartialEq)] @@ -193,7 +193,7 @@ pub trait Space: std::fmt::Debug + std::fmt::Display { /// ``` fn subst(&self, pattern: &Atom, template: &Atom) -> Vec { self.query(pattern).drain(0..) - .map(| bindings | apply_bindings_to_atom(template, &bindings)) + .map(| bindings | apply_bindings_to_atom_move(template.clone(), &bindings)) .collect() } diff --git a/lib/tests/case.rs b/lib/tests/case.rs index 8c06219fd..02cfedf99 100644 --- a/lib/tests/case.rs +++ b/lib/tests/case.rs @@ -49,11 +49,11 @@ fn test_case_operation() { (((Rel-P $y) (P $y)) ((Rel-Q $y) (Q $y)))) - ; %void% can be used to capture empty results + ; Empty can be used to capture empty results !(case (match &self ($rel B $x) ($rel $x)) (((Rel-P $y) (P $y)) ((Rel-Q $y) (Q $y)) - (%void% no-match))) + (Empty no-match))) ; a functional example (= (maybe-inc $x) diff --git a/python/VERSION b/python/VERSION index 4ad8ec271..df879447c 100644 --- a/python/VERSION +++ b/python/VERSION @@ -1 +1 @@ -'0.1.8' \ No newline at end of file +'0.1.10' diff --git a/python/hyperon/__init__.py b/python/hyperon/__init__.py index f6f9b7973..c6763cee4 100644 --- a/python/hyperon/__init__.py +++ b/python/hyperon/__init__.py @@ -1,12 +1,12 @@ from .atoms import * from .base import * from .runner import * -from ._version import __version__ as _ver -if _ver is None: +try: + from ._version import __version__ as _ver + __version__ = _ver +except Exception: from pathlib import Path path = Path(__file__).parent / "../VERSION" with path.open() as f: ver = f.read().splitlines()[0].split("'")[1] - __version__ = ver + "+localbuild" -else: - __version__ = _ver \ No newline at end of file + __version__ = ver + "+localbuild" \ No newline at end of file diff --git a/python/hyperon/_version.py b/python/hyperon/_version.py deleted file mode 100644 index c8ffb60db..000000000 --- a/python/hyperon/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = None \ No newline at end of file diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 7d065860a..80fecc99a 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -27,9 +27,9 @@ def __repr__(self): """Renders a human-readable text description of the Atom.""" return hp.atom_to_str(self.catom) - def get_type(self): - """Gets the type of the current Atom instance""" - return hp.atom_get_type(self.catom) + def get_metatype(self): + """Gets the metatype (kind) of the current Atom instance""" + return hp.atom_get_metatype(self.catom) def iterate(self): """Performs a depth-first exhaustive iteration of an Atom and all its children recursively.""" @@ -46,7 +46,7 @@ def match_atom(self, b): @staticmethod def _from_catom(catom): """Constructs an Atom by wrapping a C Atom""" - type = hp.atom_get_type(catom) + type = hp.atom_get_metatype(catom) if type == AtomKind.SYMBOL: return SymbolAtom(catom) elif type == AtomKind.VARIABLE: @@ -190,7 +190,7 @@ def _priv_call_execute_on_grounded_atom(gnd, typ, args): Executes grounded Atoms. """ # ... if hp.atom_to_str(typ) == AtomType.UNDEFINED - res_typ = AtomType.UNDEFINED if hp.atom_get_type(typ) != AtomKind.EXPR \ + res_typ = AtomType.UNDEFINED if hp.atom_get_metatype(typ) != AtomKind.EXPR \ else Atom._from_catom(hp.atom_get_children(typ)[-1]) args = [Atom._from_catom(catom) for catom in args] return gnd.execute(*args, res_typ=res_typ) @@ -214,7 +214,7 @@ def _priv_compare_value_atom(gnd, catom): Private glue for Hyperonpy implementation. Tests for equality between a grounded value atom and another atom """ - if hp.atom_get_type(catom) == AtomKind.GROUNDED: + if hp.atom_get_metatype(catom) == AtomKind.GROUNDED: atom = GroundedAtom(catom) return gnd == atom.get_object() else: @@ -383,7 +383,7 @@ def execute(self, *atoms, res_typ=AtomType.UNDEFINED): except: raise RuntimeError(f"Incorrect kwarg format {kwarg}") try: - kwargs[repr(kwarg[0])] = kwarg[1].get_object().content + kwargs[get_string_value(kwarg[0])] = kwarg[1].get_object().content except: raise NoReduceError() continue @@ -705,3 +705,10 @@ def iterator(self): for r in res: result.append(Bindings(r)) return iter(result) + +def get_string_value(value) -> str: + if not isinstance(value, str): + value = repr(value) + if len(value) > 2 and ("\"" == value[0]) and ("\"" == value[-1]): + return value[1:-1] + return value diff --git a/python/hyperon/stdlib.py b/python/hyperon/stdlib.py index a35142ab3..044ee3870 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -3,7 +3,7 @@ import os from .atoms import ExpressionAtom, E, GroundedAtom, OperationAtom, ValueAtom, NoReduceError, AtomType, MatchableObject, \ - G, S, Atoms + G, S, Atoms, get_string_value, GroundedObject, SymbolAtom from .base import Tokenizer, SExprParser from .ext import register_atoms, register_tokens import hyperonpy as hp @@ -63,13 +63,6 @@ def bool_ops(): r"not": notAtom } -def get_string_value(value) -> str: - if not isinstance(value, str): - value = repr(value) - if len(value) > 2 and ("\"" == value[0]) and ("\"" == value[-1]): - return value[1:-1] - return value - class RegexMatchableObject(MatchableObject): ''' To match atoms with regular expressions''' @@ -212,3 +205,52 @@ def load_ascii_atom(space, name): return { r"load-ascii": loadAtom } + +def try_unwrap_python_object(a, is_symbol_to_str = False): + if isinstance(a, GroundedAtom): + # FIXME? Do we need to unwrap a grounded object if it is not GroundedObject? + return a.get_object().content if isinstance(a.get_object(), GroundedObject) else a.get_object() + if is_symbol_to_str and isinstance(a, SymbolAtom): + return a.get_name() + return a + +# convert nested tuples to nested python tuples or lists +def _py_tuple_list(tuple_list, metta_tuple): + rez = [] + for a in metta_tuple.get_children(): + if isinstance(a, ExpressionAtom): + rez.append(_py_tuple_list(tuple_list, a)) + else: + rez.append(try_unwrap_python_object(a)) + return tuple_list(rez) + +def py_tuple(metta_tuple): + return [ValueAtom(_py_tuple_list(tuple, metta_tuple))] + +def py_list(metta_tuple): + return [ValueAtom(_py_tuple_list(list, metta_tuple))] + +def tuple_to_keyvalue(a): + ac = a.get_children() + if len(ac) != 2: + raise Exception("Syntax error in tuple_to_keyvalue") + return try_unwrap_python_object(ac[0], is_symbol_to_str = True), try_unwrap_python_object(ac[1]) + +# convert pair of tuples to python dictionary +def py_dict(metta_tuple): + return [ValueAtom(dict([tuple_to_keyvalue(a) for a in metta_tuple.get_children()]))] + +# chain python objects with | (syntactic sugar for langchain) +def py_chain(metta_tuple): + objects = [try_unwrap_python_object(a) for a in metta_tuple.get_children()] + result = objects[0] + for obj in objects[1:]: + result = result | obj + return [ValueAtom(result)] + +@register_atoms() +def py_funs(): + return {"py-tuple": OperationAtom("py-tuple", py_tuple, unwrap = False), + "py-list" : OperationAtom("py-list" , py_list , unwrap = False), + "py-dict" : OperationAtom("py-dict" , py_dict , unwrap = False), + "py-chain": OperationAtom("py-chain", py_chain, unwrap = False)} diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index 5e75e7f60..9429c3d50 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -694,7 +694,7 @@ PYBIND11_MODULE(hyperonpy, m) { m.def("atom_to_str", [](CAtom& atom) { return func_to_string((write_to_buf_func_t)&atom_to_str, atom.ptr()); }, "Convert atom to human readable string"); - m.def("atom_get_type", [](CAtom& atom) { return atom_get_type(atom.ptr()); }, "Get type of the atom"); + m.def("atom_get_metatype", [](CAtom& atom) { return atom_get_metatype(atom.ptr()); }, "Get type of the atom"); m.def("atom_get_name", [](CAtom& atom) { return func_to_string((write_to_buf_func_t)&atom_get_name, atom.ptr()); }, "Get name of the Symbol or Variable atom"); diff --git a/python/install-hyperonc.sh b/python/install-hyperonc.sh index 26f0ff03a..65d2ed921 100755 --- a/python/install-hyperonc.sh +++ b/python/install-hyperonc.sh @@ -12,13 +12,15 @@ while getopts 'u:r:' opt; do ;; ?|h) echo "Usage: $(basename $0) [-u hyperonc_repo_url] [-r hyperonc_revision]" + echo "-u hyperonc_repo_url Git repo URL to get hyperonc source code" + echo "-r hyperonc_revision Revision of hyperonc to get from Git" exit 1 ;; esac done -echo "hyperonc repository URL $HYPERONC_URL" -echo "hyperonc revision $HYPERONC_REV" +echo "hyperonc repository URL: $HYPERONC_URL" +echo "hyperonc revision: $HYPERONC_REV" # This is to build subunit from Conan on CentOS based manylinux images. if test "$AUDITWHEEL_POLICY" = "manylinux2014"; then @@ -43,8 +45,13 @@ git reset --hard FETCH_HEAD mkdir -p ${HOME}/hyperonc/c/build cd ${HOME}/hyperonc/c/build -# Rust doesn't support building shared libraries under musllinux environment -cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release .. +# Rust doesn't support building shared libraries under musllinux environment. +CMAKE_ARGS="$CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF" +# Local prefix is used to support MacOSX Apple Silicon GitHub actions environment. +CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${HOME}/.local" +CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release" +echo "hyperonc CMake arguments: $CMAKE_ARGS" +cmake $CMAKE_ARGS .. make make check make install diff --git a/python/pyproject.toml b/python/pyproject.toml index 240eb9b9e..b1d923100 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -36,7 +36,8 @@ package-dir = { "hyperon" = "hyperon" } [tool.cibuildwheel] before-all = "sh -c ./python/install-hyperonc.sh" -# no Rust toolchain is available for musllinux-i686 environment -skip = "*-musllinux_i686" +# There is no Rust toolchain is available for musllinux-i686 environment. +# Other musllinux platforms are opted out to decrease the build time. +skip = "*musllinux*" test-requires = ["pytest==7.3.2"] -test-command = "pytest {project}/python/tests" \ No newline at end of file +test-command = "pytest {project}/python/tests" diff --git a/python/sandbox/repl/metta_repl.py b/python/sandbox/repl/metta_repl.py index 170c6261f..9147c7241 100644 --- a/python/sandbox/repl/metta_repl.py +++ b/python/sandbox/repl/metta_repl.py @@ -84,7 +84,7 @@ def resolve_atom(metta, token): # TODO? the problem is that we need to return an operation to make this # work in parent expressions, thus, it is unclear how to return pure # symbols - if atom.get_type() == hp.AtomKind.GROUNDED: + if atom.get_metatype() == hp.AtomKind.GROUNDED: return atom # TODO: borrow atom type to op return OperationAtom( @@ -141,4 +141,4 @@ def main_loop(self): repl = REPL() readline.add_history("!(match &self $ $)") repl.main_loop() - + diff --git a/python/sandbox/resolve/resolve.py b/python/sandbox/resolve/resolve.py index f73ace948..7bf78c53d 100644 --- a/python/sandbox/resolve/resolve.py +++ b/python/sandbox/resolve/resolve.py @@ -29,7 +29,7 @@ def resolve_atom(metta, token): # TODO? the problem is that we need to return an operation to make this # work in parent expressions, thus, it is unclear how to return pure # symbols - if atom.get_type() == hp.AtomKind.GROUNDED: + if atom.get_metatype() == hp.AtomKind.GROUNDED: return atom # TODO: borrow atom type to op return OperationAtom( diff --git a/python/sandbox/simple_import/example_01.metta b/python/sandbox/simple_import/example_01.metta index f2b14c383..70ca152a5 100644 --- a/python/sandbox/simple_import/example_01.metta +++ b/python/sandbox/simple_import/example_01.metta @@ -1,11 +1,10 @@ -!(import! &self simple_import) - -!(import_from example_01 import simple_fun) -!(import_from example_01 import SimpleObject) +!(bind! simple_fun (py-atom example_01.simple_fun)) +!(bind! SimpleObject (py-atom example_01.SimpleObject)) !(bind! so (SimpleObject)) + ; it is important that obj will have type SimpleObject when passed to simple_fun! -!(simple_fun 1 2 "3" (kwarg1 2) (obj so) ) +!(simple_fun 1 2 "3" (Kwargs (kwarg1 2) (obj so)) ) -!(call_dot so method "arg1" "arg2" (arg3 3)) \ No newline at end of file +!( (py-dot so method) "arg1" "arg2" (Kwargs (arg3 3)) ) diff --git a/python/sandbox/simple_import/example_02_numpy.metta b/python/sandbox/simple_import/example_02_numpy.metta index edf992a8c..6d5aa9b5f 100644 --- a/python/sandbox/simple_import/example_02_numpy.metta +++ b/python/sandbox/simple_import/example_02_numpy.metta @@ -1,18 +1,16 @@ -!(import! &self simple_import) +!(bind! np (py-atom numpy)) -!(import_as numpy as np) +!(bind! a1 ( (py-dot np array) (py-atom (py-tuple (1 2 3)) ))) +!(bind! a2 ( (py-dot a1 __mul__) 3)) +!(bind! a3 ( (py-dot a1 __add__) a2)) -!(bind! a1 (call_dot np array (ptuple 1 2 3) )) -!(bind! a2 (call_dot a1 __mul__ 3)) -!(bind! a3 (call_dot a1 __add__ a2)) +!(a1) +!(a2) +!(a3) -!(__unwrap a1) -!(__unwrap a2) -!(__unwrap a3) +!(bind! m1 ((py-dot np array) (py-atom (py-list ((1 2 3) (py-list (4 4 5)) (py-tuple (6 7 8))) )))) +!(bind! linalg (py-atom numpy.linalg)) +!(bind! m1_inv ( (py-dot linalg inv) m1)) -!(bind! m1 (call_dot np array (ptuple (1 2 3) (4 4 5) (6 7 8)) )) -!(import_as numpy.linalg as linalg) -!(bind! m1_inv (call_dot linalg inv m1)) - -!(__unwrap (call_dot np matmul m1 m1_inv)) +!( (py-dot np matmul) m1 m1_inv) diff --git a/python/sandbox/simple_import/example_03_langchain.metta b/python/sandbox/simple_import/example_03_langchain.metta index 1fe219d2d..93076827d 100644 --- a/python/sandbox/simple_import/example_03_langchain.metta +++ b/python/sandbox/simple_import/example_03_langchain.metta @@ -1,21 +1,18 @@ -!(import! &self simple_import) +!(bind! ChatOpenAI (py-atom langchain_openai.ChatOpenAI)) +!(bind! ChatPromptTemplate (py-atom langchain_core.prompts.ChatPromptTemplate)) +!(bind! StrOutputParser (py-atom langchain_core.output_parsers.StrOutputParser)) -!(import_from langchain_openai import ChatOpenAI) -!(import_from langchain_core.prompts import ChatPromptTemplate) -!(import_from langchain_core.output_parsers import StrOutputParser) +!(bind! model (ChatOpenAI (Kwargs (temperature 0) (model "gpt-3.5-turbo")))) +!(bind! prompt ( (py-dot ChatPromptTemplate from_template) "tell me a joke about cat")) -!(bind! model (ChatOpenAI (temperature 0) (model "gpt-3.5-turbo"))) +!(bind! chain1 (py-chain (prompt model (StrOutputParser)) )) -!(bind! prompt (call_dot ChatPromptTemplate from_template "tell me a joke about cat")) +!( (py-dot chain1 invoke) (py-dict ())) -!(bind! chain1 (chain prompt model (StrOutputParser) )) +!(bind! prompt2 ( (py-dot ChatPromptTemplate from_messages ) (py-tuple (("system" "You are very funny") ("user" "tell me joke about {foo}"))))) -!(__unwrap(call_dot chain1 invoke (pdict))) +!(bind! chain2 (py-chain (prompt2 model (StrOutputParser)) )) -!(bind! prompt2 (call_dot ChatPromptTemplate from_messages (ptuple ("system" "You are very funny") ("user" "tell me joke about {foo}")))) - -!(bind! chain2 (chain prompt2 model (StrOutputParser) )) - -!(__unwrap(call_dot chain2 invoke (pdict (foo "dogs") ))) +!((py-dot chain2 invoke) (py-dict (("foo" "dogs")))) diff --git a/python/sandbox/simple_import/example_04_numpy_simple_import.metta b/python/sandbox/simple_import/example_04_numpy_simple_import.metta index 646ad158a..70f4c8bf3 100644 --- a/python/sandbox/simple_import/example_04_numpy_simple_import.metta +++ b/python/sandbox/simple_import/example_04_numpy_simple_import.metta @@ -1,14 +1,6 @@ -!(import! &self simple_import) +!(bind! linalg (py-atom numpy.linalg)) +!(bind! numpy (py-atom numpy)) -; with simple "import" it is rather common that we import something -; twice because of submodules in python -; So let's import twice to make sure that it does not cause any problems -!(import numpy) -!(import numpy) -!(import numpy.linalg) - - -!(bind! m1 (call_dot2 numpy random rand 3 3 )) -!(bind! m1_inv (call_dot2 numpy linalg inv m1)) - -!(__unwrap (call_dot numpy matmul m1 m1_inv)) +!(bind! m1 ((py-dot numpy random.rand) 3 3 )) +!(bind! m1_inv ( (py-dot linalg inv) m1)) +!( (py-dot numpy matmul) m1 m1_inv) diff --git a/python/sandbox/simple_import/simple_import.py b/python/sandbox/simple_import/simple_import.py deleted file mode 100644 index 7efba4baa..000000000 --- a/python/sandbox/simple_import/simple_import.py +++ /dev/null @@ -1,147 +0,0 @@ -from hyperon.atoms import OperationAtom, OperationObject, GroundedAtom, ValueAtom, ExpressionAtom, SymbolAtom, ValueObject -from hyperon.ext import register_atoms -import os -import sys - -def groundedatom_to_python_object(a): - obj = a.get_object() - if isinstance(obj, ValueObject): - obj = obj.value - if isinstance(obj, OperationObject): - obj = obj.content - #we need to make __unwrap if needed - if isinstance(obj, PythonCaller): - obj = obj.obj - return obj - -def tuple_to_keyvalue(a): - ac = a.get_children() - if len(ac) != 2: - raise Exception("Syntax error in tuple_to_keyvalue") - return str(ac[0]), groundedatom_to_python_object(ac[1]) - -def atoms_to_args(*atoms): - args = [] - kwargs = {} - for a in atoms: - if isinstance(a, GroundedAtom): - args.append(groundedatom_to_python_object(a)) - elif isinstance(a, ExpressionAtom): - k,v = tuple_to_keyvalue(a) - kwargs[k] = v - else: - raise Exception(f"Unexpected error: {a}, {type(a)}") - return args, kwargs - -class PythonCaller: - def __init__(self, obj): - self.obj = obj - - def __call__(self, *atoms): - args, kwargs = atoms_to_args(*atoms) - return [ValueAtom(PythonCaller(self.obj(*args, **kwargs)))] - -def _import_and_create_operationatom(metta, import_str, obj): - - # we only need these 3 lines to import from the current directory - # TODO fix it somehow differently - current_directory = os.getcwd() - if current_directory not in sys.path: - sys.path.append(current_directory) - - local_scope = {} - exec(import_str, {}, local_scope) - oatom = OperationAtom(obj, PythonCaller(local_scope[obj]), unwrap = False) - metta.register_atom(obj, oatom) - - -def import_from(metta, lib, i, obj): - if str(i) != "import": - raise Exception("bad import syntax") - lib = str(lib) - obj = str(obj) - _import_and_create_operationatom(metta, f"from {lib} import {obj}", obj) - return [] - -def import_as(metta, lib, a, obj): - if str(a) != "as": - raise Exception("bad import syntax") - lib = str(lib) - obj = str(obj) - _import_and_create_operationatom(metta, f"import {lib} as {obj}", obj) - return [] - -def import_simple(metta, lib): - lib = str(lib) - obj = lib.split(".")[0] - _import_and_create_operationatom(metta, f"import {lib}", obj) - return [] - -def call_dot(*atoms): - if len(atoms) < 2: - raise Exception("Syntax error") - obj = groundedatom_to_python_object(atoms[0]) - method = str(atoms[1]) - atoms = atoms[2:] - args, kwargs = atoms_to_args(*atoms) - rez = getattr(obj, method)(*args, **kwargs) - return [ValueAtom(PythonCaller(rez))] - -def call_dot2(*atoms): - if len(atoms) < 3: - raise Exception("Syntax error") - obj = groundedatom_to_python_object(atoms[0]) - method1 = str(atoms[1]) - method2 = str(atoms[2]) - atoms = atoms[3:] - args, kwargs = atoms_to_args(*atoms) - rez = getattr(getattr(obj, method1), method2)(*args, **kwargs) - return [ValueAtom(PythonCaller(rez))] - -def __unwrap(obj): - return obj.obj - -@register_atoms(pass_metta=True) -def my_atoms(metta): - return {'import_from': OperationAtom('import_from', lambda *args: import_from (metta, *args), unwrap = False), - 'import_as': OperationAtom('import_as', lambda *args: import_as (metta, *args), unwrap = False), - 'import': OperationAtom('import', lambda *args: import_simple(metta, *args), unwrap = False)} - -@register_atoms() -def my_atoms2(): - return {'__unwrap': OperationAtom('__unwrap', __unwrap), - "call_dot": OperationAtom("call_dot", call_dot, unwrap = False), - "call_dot2": OperationAtom("call_dot2", call_dot2, unwrap = False)} - -# The functions which are not required for import, but nice for examples - -# convert nested tuples to nested python tuples -def _ptuple(*atoms): - rez = [] - for a in atoms: - if isinstance(a, GroundedAtom): - rez.append(groundedatom_to_python_object(a)) - elif isinstance(a, ExpressionAtom): - rez.append(_ptuple(*a.get_children())) - return tuple(rez) - -def ptuple(*atoms): - return [ValueAtom(_ptuple(*atoms))] - -# convert pair of tuples to python dictionary -def pdict(*atoms): - return [ValueAtom(dict([tuple_to_keyvalue(a) for a in atoms]))] - -# chain python objects with | (syntactic sugar for langchain) -def chain(*atoms): - objects = [groundedatom_to_python_object(a) for a in atoms] - result = objects[0] - for obj in objects[1:]: - result = result | obj - return [ValueAtom(PythonCaller(result))] - -@register_atoms() -def my_atoms3(): - return {"ptuple": OperationAtom("ptuple", ptuple, unwrap = False), - "pdict": OperationAtom("pdict", pdict, unwrap = False), - "chain": OperationAtom("chain", chain, unwrap = False)} diff --git a/python/setup.py b/python/setup.py index cb320db4c..41dc47e71 100644 --- a/python/setup.py +++ b/python/setup.py @@ -38,11 +38,13 @@ def _build_with_cmake(self, ext: CMakeExtension) -> None: extdir = ext_fullpath.parent.resolve() debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug cfg = "Debug" if debug else "Release" + local_prefix = os.path.join(os.environ["HOME"], ".local") cmake_args = [ f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", f"-DPython3_EXECUTABLE={sys.executable}", - f"-DCMAKE_BUILD_TYPE={cfg}" + f"-DCMAKE_BUILD_TYPE={cfg}", + f"-DCMAKE_PREFIX_PATH={local_prefix}" ] build_args = [] # Adding CMake arguments set as environment variable @@ -88,4 +90,4 @@ def version_scheme(*args): use_scm_version={'root': '..', 'version_scheme': version_scheme, 'write_to': 'python/hyperon/_version.py'}, - ) \ No newline at end of file + ) diff --git a/python/tests/test_atom.py b/python/tests/test_atom.py index 71f86ed20..411156c3f 100644 --- a/python/tests/test_atom.py +++ b/python/tests/test_atom.py @@ -12,7 +12,7 @@ def test_symbol_str(self): self.assertEqual(str(S("a")), "a") def test_symbol_type(self): - self.assertEqual(S("a").get_type(), AtomKind.SYMBOL) + self.assertEqual(S("a").get_metatype(), AtomKind.SYMBOL) def test_symbol_get_symbol(self): self.assertEqual(S("a").get_name(), "a") @@ -28,7 +28,7 @@ def test_variable_str(self): self.assertEqual(str(V("x")), "$x") def test_variable_type(self): - self.assertEqual(V("x").get_type(), AtomKind.VARIABLE) + self.assertEqual(V("x").get_metatype(), AtomKind.VARIABLE) def test_variable_get_name(self): self.assertEqual(V("x").get_name(), "x") @@ -42,7 +42,7 @@ def test_grounded_str(self): self.assertEqual(str(ValueAtom("1.0")), '"1.0"') def test_grounded_type(self): - self.assertEqual(ValueAtom(1.0).get_type(), AtomKind.GROUNDED) + self.assertEqual(ValueAtom(1.0).get_metatype(), AtomKind.GROUNDED) def test_grounded_grounded_type(self): atom = G(GroundedObject(None), S("Float")) @@ -74,7 +74,7 @@ def test_expr_str(self): self.assertEqual(str(E(x2Atom, ValueAtom(1.0))), "(*2 1.0)") def test_expr_type(self): - self.assertEqual(E(x2Atom, ValueAtom(1.0)).get_type(), AtomKind.EXPR) + self.assertEqual(E(x2Atom, ValueAtom(1.0)).get_metatype(), AtomKind.EXPR) def test_expr_get_children(self): self.assertEqual(E(x2Atom, ValueAtom(1.0)).get_children(), @@ -154,7 +154,7 @@ class GroundedNoCopy: class MatchableObjectTest(MatchableObject): def match_(self, atom): - return [{'atom_type': S(atom.get_children()[0].get_type().name)}] + return [{'atom_type': S(atom.get_children()[0].get_metatype().name)}] def MatchableAtomTest(value, type_name=None, atom_id=None): return G(MatchableObjectTest(value, atom_id), AtomType.UNDEFINED) diff --git a/python/tests/test_custom_space.py b/python/tests/test_custom_space.py index e7ff08aa4..b188a7eb2 100644 --- a/python/tests/test_custom_space.py +++ b/python/tests/test_custom_space.py @@ -15,7 +15,7 @@ def __init__(self, unwrap=True): def query(self, query_atom): # Extract only the variables from the query atom - query_vars = list(filter(lambda atom: atom.get_type() == AtomKind.VARIABLE, query_atom.iterate())) + query_vars = list(filter(lambda atom: atom.get_metatype() == AtomKind.VARIABLE, query_atom.iterate())) # Match the query atom against every atom in the space # BindingsSet() creates a binding set with the only matching result diff --git a/python/tests/test_sexparser.py b/python/tests/test_sexparser.py index 996deedf5..a5c6486a7 100644 --- a/python/tests/test_sexparser.py +++ b/python/tests/test_sexparser.py @@ -11,7 +11,7 @@ def testParseToSyntaxNodes(self): parser = SExprParser("(+ one \"one\")") syntax_node = parser.parse_to_syntax_tree() leaf_node_list = syntax_node.unroll() - leaf_node_types = []; + leaf_node_types = [] for node in leaf_node_list: leaf_node_types.append(node.get_type()) @@ -21,7 +21,7 @@ def testParseToSyntaxNodes(self): SyntaxNodeType.WORD_TOKEN, SyntaxNodeType.WHITESPACE, SyntaxNodeType.STRING_TOKEN, - SyntaxNodeType.CLOSE_PAREN]; + SyntaxNodeType.CLOSE_PAREN] self.assertEqual(leaf_node_types, expected_node_types) diff --git a/python/tests/test_stdlib.py b/python/tests/test_stdlib.py index 7a0d47021..6c7f1f37b 100644 --- a/python/tests/test_stdlib.py +++ b/python/tests/test_stdlib.py @@ -84,6 +84,37 @@ def test_regex(self): self.assertEqual(metta.run('!(intent "Hi")', True), []) + def test_py_list_tuple(self): + metta = MeTTa(env_builder=Environment.test_env()) + self.assertEqual(metta.run('!(py-list ())'), [[ValueAtom( [] )]]) + self.assertEqual(metta.run('!(py-tuple ())'), [[ValueAtom( () )]]) + self.assertEqual(metta.run('!(py-dict ())'), [[ValueAtom( {} )]]) + self.assertEqual(metta.run('!(py-tuple (1 (2 (3 "3")) (py-atom list)))'), [[ValueAtom((1,(2,(3, "3")), list))]]) + self.assertEqual(metta.run('!(py-list (1 2 (4.5 3)))'), [[ValueAtom( [1,2,[4.5,3]] )]]) + self.assertEqual(metta.run('!(py-list (1 2 (py-tuple (3 4))))'), [[ValueAtom( [1,2, (3,4)] )]]) + + self.assertEqual(metta.run('!(py-dict ((a "b") ("b" "c")))'), [[ValueAtom( {"a":"b", "b":"c"} )]]) + + self.assertEqual(str(metta.run('!(py-list (a b c))')[0][0].get_object().content[2]), "c") + + # We need py-chain for langchain, but we test with bitwise operation | (1 | 2 | 3 | 4 = 7) + self.assertEqual(metta.run('!(py-chain (1 2 3 4))'), [[ValueAtom( 7 )]]) + + # test when we except errors (just in case we reset metta after each exception) + self.assertRaises(Exception, metta.run('!(py-dict (("a" "b" "c") ("b" "c")))')) + metta = MeTTa(env_builder=Environment.test_env()) + + self.assertRaises(Exception, metta.run('!(py-dict (("a") ("b" "c")))')) + metta = MeTTa(env_builder=Environment.test_env()) + + self.assertRaises(Exception, metta.run('!(py-dict ("a" "b") ("b" "c"))')) + metta = MeTTa(env_builder=Environment.test_env()) + + self.assertRaises(Exception, metta.run('!(py-list 1 2)')) + metta = MeTTa(env_builder=Environment.test_env()) + + self.assertRaises(Exception, metta.run('!(py-list 1)')) + metta = MeTTa(env_builder=Environment.test_env()) if __name__ == "__main__": unittest.main() diff --git a/repl/src/py_shim.py b/repl/src/py_shim.py index 7acd0c52e..addda986e 100644 --- a/repl/src/py_shim.py +++ b/repl/src/py_shim.py @@ -29,7 +29,7 @@ def parse_line(metta, line): return e.args[0] def parse_line_to_syntax_tree(line): - leaf_node_types = []; + leaf_node_types = [] parser = SExprParser(line) while True: syntax_node = parser.parse_to_syntax_tree()