diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 000000000..151d55cd4
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,21 @@
+name: build docs
+on:
+ push:
+ branches:
+ - main
+permissions:
+ contents: write
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-versoin: 3.x
+ - uses: actions/cache@v2
+ with:
+ key: ${{ github.ref }}
+ path: .cache
+ - run: pip install mkdocs-material mkdocs-minify-plugin mkdocstrings[python]
+ - run: mkdocs gh-deploy --force
diff --git a/README.md b/README.md
index 63e0a0a01..de5f8da00 100644
--- a/README.md
+++ b/README.md
@@ -241,9 +241,9 @@ All components which depend on `libhyperonc` are built using
[CMake](https://cmake.org/) build tool in order to manage dependencies
automatically.
-Diagram below demonstrates main components and dependencies between them:
-![Diagram of the structure](./doc/structure.svg)
-[Source code of the diagram](./doc/structure.plantuml)
+The diagram below demonstrates main components and dependencies between them:
+![Diagram of the structure](./docs/assets/structure.svg)
+[Source code of the diagram](./docs/assets/structure.plantuml)
## Language support for IDEs
diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html
new file mode 100644
index 000000000..d5539f750
--- /dev/null
+++ b/docs/.overrides/main.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% block announce %}
+
+ For updates follow
+
+
+ {% include ".icons/fontawesome/brands/twitter.svg" %}
+
+ OpenCog Hyperon
+
+{% endblock %}
diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md
similarity index 100%
rename from CONTRIBUTING.md
rename to docs/CONTRIBUTING.md
diff --git a/docs/assets/P8T2_ASO_400x400.jpg b/docs/assets/P8T2_ASO_400x400.jpg
new file mode 100644
index 000000000..ad286b93e
Binary files /dev/null and b/docs/assets/P8T2_ASO_400x400.jpg differ
diff --git a/doc/structure.plantuml b/docs/assets/structure.plantuml
similarity index 100%
rename from doc/structure.plantuml
rename to docs/assets/structure.plantuml
diff --git a/doc/structure.svg b/docs/assets/structure.svg
similarity index 100%
rename from doc/structure.svg
rename to docs/assets/structure.svg
diff --git a/doc/minimal-metta.md b/docs/minimal-metta.md
similarity index 100%
rename from doc/minimal-metta.md
rename to docs/minimal-metta.md
diff --git a/docs/reference/atoms.md b/docs/reference/atoms.md
new file mode 100644
index 000000000..9655c9f7e
--- /dev/null
+++ b/docs/reference/atoms.md
@@ -0,0 +1 @@
+::: hyperon.atoms
diff --git a/docs/reference/base.md b/docs/reference/base.md
new file mode 100644
index 000000000..c0ec722fc
--- /dev/null
+++ b/docs/reference/base.md
@@ -0,0 +1 @@
+::: hyperon.base
diff --git a/docs/reference/ext.md b/docs/reference/ext.md
new file mode 100644
index 000000000..64e464aec
--- /dev/null
+++ b/docs/reference/ext.md
@@ -0,0 +1 @@
+::: hyperon.ext
diff --git a/docs/reference/runner.md b/docs/reference/runner.md
new file mode 100644
index 000000000..03a637f28
--- /dev/null
+++ b/docs/reference/runner.md
@@ -0,0 +1 @@
+::: hyperon.runner
diff --git a/docs/reference/stdlib.md b/docs/reference/stdlib.md
new file mode 100644
index 000000000..ac52b18da
--- /dev/null
+++ b/docs/reference/stdlib.md
@@ -0,0 +1 @@
+::: hyperon.stdlib
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..760318bfc
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+mkdocs-minify-plugin
+mkdocstrings[python]
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..01726572f
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,144 @@
+site_name: OpenCog Hyperon
+repo_name: trueagi-io/hyperon-experimental
+repo_url: https://github.com/trueagi-io/hyperon-experimental
+
+copyright: |
+ © 2023 OpenCog Hyperon
+
+theme:
+ name: material
+ custom_dir: docs/.overrides
+ features:
+ - announce.dismiss
+ - content.action.edit
+ - content.action.view
+ - content.code.annotate
+ - content.code.copy
+ # - content.tabs.link
+ - content.tooltips
+ # - header.autohide
+ - navigation.expand
+ - navigation.footer
+ - navigation.indexes
+ - navigation.instant
+ # - navigation.prune
+ - navigation.sections
+ # - navigation.tabs
+ # - navigation.tabs.sticky
+ - navigation.top
+ - navigation.tracking
+ - search.highlight
+ - search.share
+ - search.suggest
+ - toc.follow
+ # - toc.integrate
+ language: en
+ palette:
+ - scheme: default
+ toggle:
+ icon: material/toggle-switch-off-outline
+ name: Switch to dark mode
+ primary: teal
+ accent: purple
+ - scheme: slate
+ toggle:
+ icon: material/toggle-switch
+ name: Switch to light mode
+ primary: teal
+ accent: lime
+ font:
+ text: Roboto
+ code: Roboto Mono
+ favicon: assets/P8T2_ASO_400x400.jpg
+ icon:
+ logo: logo
+
+plugins:
+ - search:
+ separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
+ - minify:
+ minify_html: true
+ - mkdocstrings:
+ default_hander: python
+ handlers:
+ python:
+ paths: [python]
+ import:
+ - https://docs.python.org/3/objects.inv
+ options:
+ docstring_options:
+ ignore_init_summary: true
+ docstring_section_style: list
+ heading_level: 1
+ inherited_members: true
+ merge_init_into_class: true
+ separate_signature: true
+ show_root_heading: true
+ show_root_full_path: false
+ show_signature_annotations: true
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
+ signature_crossrefs: true
+ - autorefs
+
+extra:
+ social:
+ - icon: fontawesome/brands/github-alt
+ link: https://github.com/trueagi-io/hyperon-experimental
+ - icon: fontawesome/brands/twitter
+ link: https://twitter.com/OpenCog
+
+markdown_extensions:
+ - pymdownx.snippets
+ - abbr
+ - admonition
+ - attr_list
+ - def_list
+ - footnotes
+ - md_in_html
+ - toc:
+ permalink: true
+ - pymdownx.arithmatex:
+ generic: true
+ - pymdownx.betterem:
+ smart_enable: all
+ - pymdownx.caret
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_generator: !!python/name:materialx.emoji.to_svg
+ emoji_index: !!python/name:materialx.emoji.twemoji
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.keys
+ - pymdownx.magiclink:
+ repo_url_shorthand: true
+ user: squidfunk
+ repo: mkdocs-material
+ - pymdownx.mark
+ - pymdownx.smartsymbols
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+
+# Page tree
+nav:
+ - Minimal Metta: minimal-metta.md
+ - Contribution: CONTRIBUTING.md
+ - Python Reference:
+ - Atoms: reference/atoms.md
+ - Base: reference/base.md
+ - Ext: reference/ext.md
+ - Runner: reference/runner.md
+ - Stdlib: reference/stdlib.md
+ - C Reference: mainpage.md
+ - Doxygen: html
diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py
index 7a8786fe1..6dcd4c0e1 100644
--- a/python/hyperon/atoms.py
+++ b/python/hyperon/atoms.py
@@ -1,28 +1,38 @@
+"""
+The Python wrapper for Hyperon Atom Rust types
+"""
+
import hyperonpy as hp
from hyperonpy import AtomKind
from typing import Union
class Atom:
+ """Represents an Atom of any type"""
def __init__(self, catom):
+ """Initialize an Atom"""
self.catom = catom
def __del__(self):
+ """Frees an Atom and all associated resources."""
#import sys; sys.stderr.write("Atom._del_(" + str(self) + ")\n"); sys.stderr.flush()
hp.atom_free(self.catom)
-
def __eq__(self, other):
+ """Checks if two atom objects represent the same conceptual Atom."""
return (isinstance(other, Atom) and
hp.atom_eq(self.catom, other.catom))
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 iterate(self):
+ """Performs a depth-first exhaustive iteration of an Atom and all its children recursively."""
res = hp.atom_iterate(self.catom)
result = []
for r in res:
@@ -30,10 +40,12 @@ def iterate(self):
return result
def match_atom(self, b):
+ """Matches one Atom with another, establishing bindings between them."""
return BindingsSet(hp.atom_match_atom(self.catom, b.catom))
@staticmethod
def _from_catom(catom):
+ """Constructs an Atom by wrapping a C Atom"""
type = hp.atom_get_type(catom)
if type == AtomKind.SYMBOL:
return SymbolAtom(catom)
@@ -44,43 +56,58 @@ def _from_catom(catom):
elif type == AtomKind.GROUNDED:
return GroundedAtom(catom)
else:
- raise Exception("Unexpected type of the atom: " + str(type))
+ raise Exception("Unexpected type of the Atom: " + str(type))
class SymbolAtom(Atom):
+ """A SymbolAtom represents a single concept, identified by name. If two symbols
+ have the same name, they reference the same concept."""
def __init__(self, catom):
+ """Initialize a SymbolAtom"""
super().__init__(catom)
def get_name(self):
+ """Returns the name of the Atom."""
return hp.atom_get_name(self.catom)
def S(name):
+ """A convenient method to construct a SymbolAtom"""
return SymbolAtom(hp.atom_sym(name))
class VariableAtom(Atom):
+ """A VariableAtom represents a variable in an expression. It serves as a
+ placeholder that can be matched with, or bound to other Atoms."""
def __init__(self, catom):
+ """Initialize a VariableAtom"""
super().__init__(catom)
def get_name(self):
+ """Returns the name of the Atom."""
return hp.atom_get_name(self.catom)
def V(name):
+ """A convenient method to construct a VariableAtom"""
return VariableAtom(hp.atom_var(name))
class ExpressionAtom(Atom):
+ """An ExpressionAtom combines different kinds of Atoms, including expressions."""
def __init__(self, catom):
+ """Initialize an expression atom"""
super().__init__(catom)
def get_children(self):
- return [Atom._from_catom(catom) for catom in
- hp.atom_get_children(self.catom)]
+ """Returns all children Atoms of an expression"""
+ return [Atom._from_catom(catom) for catom in hp.atom_get_children(self.catom)]
+
def E(*args):
+ """A convenient method to construct an ExpressionAtom"""
return ExpressionAtom(hp.atom_expr([atom.catom for atom in args]))
class AtomType:
+ """Defines all Atom types"""
UNDEFINED = Atom._from_catom(hp.CAtomType.UNDEFINED)
TYPE = Atom._from_catom(hp.CAtomType.TYPE)
@@ -96,11 +123,23 @@ class Atoms:
VOID = Atom._from_catom(hp.CAtoms.VOID)
class GroundedAtom(Atom):
+ """
+ A GroundedAtom represents sub-symbolic knowledge. At the API level, it allows
+ keeping data and behaviour inside an Atom. There are three aspects of a GroundedAtom
+ which can be customized:
+
+ - the type of GroundedAtom is provided by the Atom itself;
+ - the matching algorithm used by the Atom;
+ - an Atom can be made executable, and used to apply sub-symbolic
+ operations to other Atoms as arguments.
+ """
def __init__(self, catom):
+ """Initialize a GroundedAtom"""
super().__init__(catom)
def get_object(self):
+ """Returns the GroundedAtom object, or the Space wrapped inside a GroundedAtom"""
from .base import SpaceRef
if self.get_grounded_type() == AtomType.GROUNDED_SPACE:
return SpaceRef._from_cspace(hp.atom_get_space(self.catom))
@@ -108,38 +147,47 @@ def get_object(self):
return hp.atom_get_object(self.catom)
def get_grounded_type(self):
+ """Retrieve the grounded type of the GroundedAtom."""
return Atom._from_catom(hp.atom_get_grounded_type(self.catom))
def G(object, type=AtomType.UNDEFINED):
+ """A convenient method to construct a GroundedAtom"""
assert hasattr(object, "copy"), "Method copy should be implemented by grounded object"
return GroundedAtom(hp.atom_gnd(object, type.catom))
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_execute_on_grounded_atom(gnd, typ, args):
+ """
+ Private glue for Hyperonpy implementation.
+ Executes grounded Atoms.
+ """
# ... if hp.atom_to_str(typ) == AtomType.UNDEFINED
res_typ = AtomType.UNDEFINED if hp.atom_get_type(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)
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_match_on_grounded_atom(gnd, catom):
+ """
+ Private glue for Hyperonpy implementation.
+ Matches grounded atoms
+ """
return gnd.match_(Atom._from_catom(catom))
def atoms_are_equivalent(first, second):
+ """Check if two atoms are equivalent"""
return hp.atoms_are_equivalent(first.catom, second.catom)
class GroundedObject:
+ """A GroundedObject holds some content and, optionally, an identifier."""
def __init__(self, content, id=None):
+ """Initializes a new GroundedObject with the given content and identifier."""
self.content = content
self.id = id
def __repr__(self):
+ """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.
if isinstance(self.content, str):
@@ -149,43 +197,120 @@ def __repr__(self):
return repr(self.content) if self.id is None else self.id
def copy(self):
+ """
+ Returns a copy of this GroundedObject instance.
+
+ Note: Currently, this method returns the original instance.
+ """
return self
class ValueObject(GroundedObject):
+ """
+ A ValueObject is a specialized form of GroundedObject, which treats its content
+ as a value. It allows for equality comparison between the content of two ValueObjects.
+
+ Example:
+ obj1 = ValueObject(5)
+ obj2 = ValueObject(5)
+ obj3 = ValueObject(6)
+
+ print(obj1 == obj2) # True
+ print(obj1 == obj3) # False
+ """
@property
def value(self):
+ """Gets the value of the object, which is its content."""
return self.content
def __eq__(self, other):
- # TODO: ?typecheck
+ """Compares the equality of this ValueObject with another based on their content."""
+ # TODO: ?typecheck for the contents
return isinstance(other, ValueObject) and self.content == other.content
class NoReduceError(Exception):
+ """Custom exception; raised when a reduction operation cannot be performed."""
pass
class OperationObject(GroundedObject):
+ """
+ An OperationObject represents an operation as a grounded object, allowing for more
+ advanced logic like lazy evaluation, type-checking, and more.
+
+ Inherits:
+ GroundedObject: The parent class that provides the basic wrapper around content.
+
+ Attributes:
+ unwrap (bool): Determines whether to unwrap the content of GroundedAtoms
+ when passed as arguments to the operation.
+
+ Properties:
+ op: Returns the operation function.
+ name: Returns the identifier name for this operation object.
+
+ Methods:
+ __init__(name, op, unwrap): Initializes an OperationObject instance.
+ execute(*args, res_typ): Executes the operation with the provided arguments.
+ __eq__(other): Compares the equality of this OperationObject instance with another.
+
+ Example:
+ def add(a, b):
+ return a + b
+
+ op_obj = OperationObject("addition", add)
+ result = op_obj.execute(3, 4)
+ """
def __init__(self, name, op, unwrap=True):
+ """
+ Initializes a new OperationObject with a name identifier, operation function,
+ and an optional unwrap flag.
+ Parameters:
+ name (str): The identifier for this operation.
+ op (function): The function representing the operation.
+ unwrap (bool, optional): Whether to unwrap GroundedAtom content when applying
+ the operation. Defaults to True.
+
+ """
super().__init__(op, name)
self.unwrap = unwrap
@property
def op(self):
+ """Returns the operation function."""
return self.content
@property
def name(self):
+ """Returns the identifier name for this operation object."""
return self.id
def execute(self, *args, res_typ=AtomType.UNDEFINED):
+ """
+ Executes the operation with the provided arguments.
+
+ Parameters:
+ *args: Arguments to pass to the operation function.
+ res_typ (AtomType, optional): The expected result type. Defaults to AtomType.UNDEFINED.
+
+ Returns:
+ The result of the operation.
+
+ Raises:
+ NoReduceError: Raised when `unwrap=True` and a non-GroundedAtom argument is provided.
+ RuntimeError: Raised when the result of the operation is not a list.
+
+ Note:
+ Depending on the `unwrap` attribute, this method will either unwrap GroundedAtoms
+ before passing them to the operation or pass them as is.
+ """
# type-check?
if self.unwrap:
for arg in args:
if not isinstance(arg, GroundedAtom):
# REM:
# Currently, applying grounded operations to pure atoms is not reduced.
- # If we want, we can raise an exception, or to form a error expression instead,
+ # If we want, we can raise an exception, or form an error expression instead,
# so a MeTTa program can catch and analyze it.
# raise RuntimeError("Grounded operation " + self.name + " with unwrap=True expects only grounded arguments")
raise NoReduceError()
@@ -198,13 +323,84 @@ def execute(self, *args, res_typ=AtomType.UNDEFINED):
return result
def __eq__(self, other):
+ """
+ Compares the equality of this OperationObject with another based on their names.
+
+ Parameters:
+ other (OperationObject): Another OperationObject instance to compare.
+
+ Returns:
+ True if both OperationObjects have the same name; False otherwise.
+ """
return isinstance(other, OperationObject) and self.name == other.name
class MatchableObject(ValueObject):
+ """
+ Represents an object that can be involved in a matching operation with an Atom.
+
+ This class is meant to be subclassed by objects that define specific matching behavior
+ with an Atom. It provides a stub method for the matching operation that raises
+ a RuntimeError when called, which must be overridden by subclasses.
+
+ Inherits:
+ ValueObject: The parent class that provides basic value-based equality and representation.
+
+ Methods:
+ match_(atom): A stub method for matching the object with an Atom.
+
+ Example:
+ class MyMatchableObject(MatchableObject):
+ def match_(self, atom):
+ # Implement the matching logic here
+ pass
+
+ my_obj = MyMatchableObject("some_value")
+ my_obj.match_(some_atom) # Should not raise RuntimeError
+
+ Raises:
+ RuntimeError: Raised when the match_ method is called without being overridden by a subclass.
+ """
+
def match_(self, atom):
+ """
+ A stub method for matching the object with an Atom.
+
+ This method is intended to be overridden by subclasses to provide specific
+ matching behavior with an Atom.
+
+ Parameters:
+ atom (Atom): An Atom object to match against.
+
+ Raises:
+ RuntimeError: Raised when this method is called without being overridden in a subclass.
+ """
raise RuntimeError("MatchableObject::match_() is not implemented")
def _type_sugar(type_names):
+ """
+ Transforms a variety of type representations into a unified Atom-based format.
+
+ This utility function is intended for internal use to handle different ways in which
+ type information can be provided. It converts `type_names` into a form that can be
+ readily used for type checking or other internal operations.
+
+ Parameters:
+ type_names (Union[None, list, str, AtomType]): The type information to be converted.
+ - If None, will return AtomType.UNDEFINED.
+ - If list, will recursively transform each element.
+ - If str, will return a Variable Atom (`V`) if the string starts with '$'; otherwise, returns a Symbol Atom (`S`).
+ - If already an AtomType, returns it as is.
+
+ Returns:
+ AtomType: The transformed type information in AtomType format.
+
+ Examples:
+ _type_sugar(None) => AtomType.UNDEFINED
+ _type_sugar(["int", "str"]) => E(S("->"), S("int"), S("str"))
+ _type_sugar("$var") => V("var")
+ _type_sugar("int") => S("int")
+ _type_sugar(AtomType.SOME_TYPE) => AtomType.SOME_TYPE
+ """
if type_names is None:
return AtomType.UNDEFINED
if isinstance(type_names, list):
@@ -214,61 +410,83 @@ def _type_sugar(type_names):
return type_names
def OperationAtom(name, op, type_names=None, unwrap=True):
+ """
+ An OperationAtom wraps an operation with optional type information into a GroundedAtom
+ and associates a name with it. Useful for registering custom operations
+ that can be executed in an Atom-based computational environment.
+ """
return G(OperationObject(name, op, unwrap), _type_sugar(type_names))
def ValueAtom(value, type_name=None, atom_id=None):
+ """Creates a GroundedAtom that wraps a given value, optionally specifying its type and identifier."""
return G(ValueObject(value, atom_id), _type_sugar(type_name))
def MatchableAtom(value, type_name=None, atom_id=None):
+ """
+ Creates a Grounded Atom that wraps a matchable value, optionally specifying its type and identifier.
+ """
return G(MatchableObject(value, atom_id), _type_sugar(type_name))
class Bindings:
+ """Interface for working with atom matching and variable-to-atom binding."""
def __init__(self, bindings: Union[hp.CBindings, None] = None):
+ """Initializes with or without pre-existing bindings."""
if bindings is None:
self.cbindings = hp.bindings_new()
else:
self.cbindings = bindings
def __del__(self):
+ """Frees the binding resources."""
if self.cbindings is not None:
hp.bindings_free(self.cbindings)
def __eq__(self, other):
+ """Checks if two bindings objects contain identical associations."""
return (isinstance(other, Bindings) and
hp.bindings_eq(self.cbindings, other.cbindings))
def __repr__(self):
+ """Renders a text description of the bindings"""
return hp.bindings_to_str(self.cbindings)
def __deepcopy__(self, memodict={}):
+ """Makes a "deep copy" of the bindings."""
return self.clone()
def __enter__(self):
+ """For context management."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
+ """Frees resources on exit."""
if self.cbindings is not None:
hp.bindings_free(self.cbindings)
self.cbindings = None
def clone(self):
+ """Makes a "deep copy" of the bindings"""
return Bindings(hp.bindings_clone(self.cbindings))
def merge(self, other: 'Bindings') -> 'BindingsSet':
+ """Merges with another Bindings instance, into a Bindings Set."""
return BindingsSet(hp.bindings_merge(self.cbindings, other.cbindings))
def add_var_binding(self, var: Union[str, Atom], atom: Atom) -> bool:
+ """Adds a binding between a variable and an Atom."""
if isinstance(var, Atom):
return hp.bindings_add_var_binding(self.cbindings, var.get_name(), atom.catom)
else:
return hp.bindings_add_var_binding(self.cbindings, var, atom.catom)
def is_empty(self) -> bool:
+ """Checks if a bindings contains no associations."""
return hp.bindings_is_empty(self.cbindings)
def narrow_vars(self, vars ):
+ """Keeps only specific variable associations."""
cvars = hp.CVecAtom = hp.atom_vec_new()
for var in vars:
hp.atom_vec_push(cvars, var.catom)
@@ -276,14 +494,17 @@ def narrow_vars(self, vars ):
hp.atom_vec_free(cvars)
def resolve(self, var_name: str) -> Union[Atom, None]:
+ """Finds the atom for a given variable name"""
raw_atom = hp.bindings_resolve(self.cbindings, var_name)
return None if raw_atom is None else Atom._from_catom(raw_atom)
def resolve_and_remove(self, var_name: str) -> Union[Atom, None]:
+ """Finds and removes the atom for a given variable name"""
raw_atom = hp.bindings_resolve_and_remove(self.cbindings, var_name)
return None if raw_atom is None else Atom._from_catom(raw_atom)
def iterator(self):
+ """Returns an iterator over the variable-atom pairs in the bindings"""
res = hp.bindings_list(self.cbindings)
result = []
for r in res:
@@ -292,8 +513,11 @@ def iterator(self):
return iter(result)
class BindingsSet:
+ """Represents a set of Bindings frames, potentially expressing all possible
+ matches produced by a match operation."""
def __init__(self, input: Union[hp.CBindingsSet, Bindings, None] = None):
+ """Initializes with optional input."""
self.shadow_list = None # A lazily initialized list that shadows the BindingsSet values for indexed access
if input is None:
self.c_set = hp.bindings_set_single()
@@ -303,51 +527,78 @@ def __init__(self, input: Union[hp.CBindingsSet, Bindings, None] = None):
self.c_set = input
def __del__(self):
+ """Frees the BindingsSet"""
if self.c_set is not None:
hp.bindings_set_free(self.c_set)
self.c_set = None
def __eq__(self, other):
+ """Checks if other BindingsSet contains identical associations."""
return (isinstance(other, BindingsSet) and
hp.bindings_set_eq(self.c_set, other.c_set))
def __repr__(self):
+ """Renders a text description of a BindingsSet"""
return hp.bindings_set_to_str(self.c_set)
def __deepcopy__(self, memodict={}):
+ """Makes a "deep copy" of a BindingsSet"""
return self.clone()
def __enter__(self):
+ """For context management."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
+ """Frees resources on exit."""
if self.c_set is not None:
hp.bindings_set_free(self.c_set)
self.c_set = None
def __getitem__(self, key):
+ """Gets a Bindings frame by index"""
if self.shadow_list is None:
result = hp.bindings_set_unpack(self.c_set)
self.shadow_list = [{k: Atom._from_catom(v) for k, v in bindings.items()} for bindings in result]
return self.shadow_list[key]
def empty():
+ """Creates a new BindingsSet without any Bindings frames.
+ Conceptually, this means no valid matches exist.
+ """
return BindingsSet(hp.bindings_set_empty())
def clone(self):
+ """Makes a "deep copy" of a BindingsSet"""
return BindingsSet(hp.bindings_set_clone(self.c_set))
def is_empty(self) -> bool:
+ """Checks if a BindingsSet contains no Bindings frames, and thus indicates
+ no match."""
return hp.bindings_set_is_empty(self.c_set)
def is_single(self) -> bool:
+ """Checks if a Bindings set contains a frame with no associations, and
+ thus allows variables to take any value.
+ """
return hp.bindings_set_is_single(self.c_set)
def push(self, bindings: Bindings):
+ """Adds a Bindings frame to an existing BindingsSet
+
+ Parameters
+ ----------
+ bindings:
+ The Bindings set to incorporate into set. Ownership of this argument is
+ taken by this function.
+ """
self.shadow_list = None
hp.bindings_set_push(self.c_set, bindings.cbindings)
def add_var_binding(self, var: Union[str, Atom], value: Atom) -> bool:
+ """Adds a new variable to atom association to every Bindings frame in a
+ BindingsSet.
+ """
self.shadow_list = None
if isinstance(var, Atom):
return hp.bindings_set_add_var_binding(self.c_set, var.catom, value.catom)
@@ -355,10 +606,12 @@ def add_var_binding(self, var: Union[str, Atom], value: Atom) -> bool:
return hp.bindings_set_add_var_binding(self.c_set, V(var), value.catom)
def add_var_equality(self, a: Atom, b: Atom) -> bool:
+ """Asserts equality between two Variable atoms in a BindingsSet."""
self.shadow_list = None
return hp.bindings_set_add_var_equality(self.c_set, a.catom, b.catom)
def merge_into(self, input: Union['BindingsSet', Bindings]):
+ """Merges the contents of another BindingsSet or Bindings frame."""
self.shadow_list = None
if isinstance(input, BindingsSet):
hp.bindings_set_merge_into(self.c_set, input.c_set);
@@ -367,6 +620,7 @@ def merge_into(self, input: Union['BindingsSet', Bindings]):
hp.bindings_set_merge_into(self.c_set, new_set.c_set);
def iterator(self):
+ """Returns an iterator over all Bindings frames"""
res = hp.bindings_set_list(self.c_set)
result = []
for r in res:
diff --git a/python/hyperon/base.py b/python/hyperon/base.py
index 5529cb3b5..7b49edde9 100644
--- a/python/hyperon/base.py
+++ b/python/hyperon/base.py
@@ -2,15 +2,19 @@
from .atoms import Atom, BindingsSet
-"""
-A virtual base class upon which Spaces can be implemented in Python
-"""
class AbstractSpace:
-
+ """
+ A virtual base class upon which Spaces can be implemented in Python
+ """
def __init__(self):
+ """Initialiize the AbstractSpace. Does nothing in the base class"""
return
def query(self, query_atom):
+ """
+ Performs the specified query on the Space.
+ Should be overridden to return a BindingsSet as the result of the query.
+ """
raise RuntimeError("Space::query() is not implemented")
# TODO (INTERNAL): Currently unimplemented. We may do this differently depending on lazy / comprehensions
@@ -19,84 +23,128 @@ def query(self, query_atom):
# None
def add(self, atom):
+ """
+ Adds an Atom to the atom space. Must be implemented in derived classes.
+ """
raise RuntimeError("Space::add() is not implemented")
def remove(self, atom):
+ """
+ Removes an Atom from the atom space. Must be implemented in derived classes.
+ """
raise RuntimeError("Space::remove() is not implemented")
def replace(self, atom, replacement):
+ """
+ Replaces an Atom from the atom space. Must be implemented in derived classes.
+ """
raise RuntimeError("Space::replace() is not implemented")
def atom_count(self):
+ """
+ Counts the number of atoms in the atom space. Optional for derived classes.
+ """
None
def atoms_iter(self):
+ """
+ Returns an iterator over atoms in the Space. Optional for derived classes.
+ """
None
-"""
-A wrapper over the native GroundingSpace implementation, that can be subclassed and extended within Python
-"""
class GroundingSpace(AbstractSpace):
-
+ """
+ A wrapper over the native GroundingSpace implementation, which can be subclassed
+ and extended within Python
+ """
def __init__(self, unwrap=True):
+ """Initialize GroundingSpace and its underlying native implementation."""
super().__init__()
# self.cspace = hp.space_new_grounding()
self.gspace = GroundingSpaceRef()
def query(self, query_atom):
+ """
+ Delegates the query to the underlying native GroundingSpace
+ and returns the result BindingsSet
+ """
return self.gspace.query(query_atom)
# TODO (INTERNAL): Currently unimplemented.
# def subst(self, pattern, templ):
def add(self, atom):
+ """
+ Adds an Atom to the atom space.
+ """
self.gspace.add_atom(atom)
def remove(self, atom):
+ """
+ Removes an Atom from the atom space.
+ """
return self.gspace.remove_atom(atom)
def replace(self, from_atom, to_atom):
+ """
+ Replaces an Atom in the atom space.
+ """
return self.gspace.replace_atom(from_atom, to_atom)
def atom_count(self):
+ """
+ Counts the number of Atoms in the atom space.
+ """
return self.gspace.atom_count()
def atoms_iter(self):
+ """
+ Returns an iterator over atoms in the atom space.
+ """
return iter(self.gspace.get_atoms())
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_query_on_python_space(space, query_catom):
+ """
+ Private glue for Hyperonpy implementation.
+ Translates a native 'catom' into an Atom object, and then delegates the query
+ to the provided 'space' object.
+ """
query_atom = Atom._from_catom(query_catom)
return space.query(query_atom)
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_add_on_python_space(space, catom):
+ """
+ Private glue for Hyperonpy implementation.
+ Translates a native 'catom' into an Atom object, and then adds it
+ to the provided 'space' object.
+ """
atom = Atom._from_catom(catom)
space.add(atom)
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_remove_on_python_space(space, catom):
+ """
+ Private glue for Hyperonpy implementation.
+ Translates a native 'catom' into an Atom object, and then removes it
+ from the provided 'space' object.
+ """
atom = Atom._from_catom(catom)
return space.remove(atom)
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_replace_on_python_space(space, cfrom, cto):
+ """
+ Private glue for Hyperonpy implementation.
+ Translates native 'catom' objects into Atom objects, and then replaces
+ the first with the second in the provided 'space' object.
+ """
from_atom = Atom._from_catom(cfrom)
to_atom = Atom._from_catom(cto)
return space.replace(from_atom, to_atom)
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_atom_count_on_python_space(space):
+ """
+ Private glue for Hyperonpy implementation.
+ Returns the number of Atoms in the provided 'space' object.
+ """
if hasattr(space, "atom_count"):
count = space.atom_count()
if count is not None:
@@ -106,71 +154,81 @@ def _priv_call_atom_count_on_python_space(space):
else:
return -1
-"""
-Private glue for Hyperonpy implementation
-"""
def _priv_call_new_iter_state_on_python_space(space):
+ """
+ Private glue for Hyperonpy implementation.
+ Returns an iterator over Atoms in the provided 'space' object.
+ """
if hasattr(space, "atoms_iter"):
return space.atoms_iter()
else:
return None
-"""
-A reference to a Space, which may be accessed directly, wrapped in a grounded atom,
-or passed to a MeTTa interpreter
-"""
class SpaceRef:
+ """
+ A reference to a Space, which may be accessed directly, wrapped in a grounded atom,
+ or passed to a MeTTa interpreter.
+ """
def __init__(self, space_obj):
+ """
+ Initialize a new SpaceRef based on the given space object, either a CSpace
+ or a custom Python object.
+ """
if type(space_obj) is hp.CSpace:
self.cspace = space_obj
else:
self.cspace = hp.space_new_custom(space_obj)
def __del__(self):
+ """Free the underlying CSpace object """
hp.space_free(self.cspace)
def __eq__(self, other):
+ """Compare two SpaceRef objects for equality, based on their underlying spaces."""
return hp.space_eq(self.cspace, other.cspace)
@staticmethod
def _from_cspace(cspace):
+ """
+ Create a new SpaceRef based on the given CSpace object.
+ """
return SpaceRef(cspace)
- """
- Returns a new copy of the SpaceRef, referencing the same underlying Space
- """
def copy(self):
+ """
+ Returns a new copy of the SpaceRef, referencing the same underlying Space.
+ """
return self
- """
- Add an Atom to the Space
- """
def add_atom(self, atom):
+ """
+ Add an Atom to the Space.
+ """
hp.space_add(self.cspace, atom.catom)
- """
- Delete the specified atom from the Space
- """
def remove_atom(self, atom):
+ """
+ Delete the specified Atom from the Space.
+ """
return hp.space_remove(self.cspace, atom.catom)
- """
- Replace the specified Atom, if it exists in the Space, with the supplied replacement Atom
- """
def replace_atom(self, atom, replacement):
+ """
+ Replaces the specified Atom, if it exists in the Space, with the supplied replacement.
+ """
return hp.space_replace(self.cspace, atom.catom, replacement.catom)
- """
- Returns the number of Atoms in the Space, or -1 if it cannot readily computed
- """
def atom_count(self):
+ """
+ Returns the number of Atoms in the Space, or -1 if it cannot be readily computed.
+ """
return hp.space_atom_count(self.cspace)
- """
- Returns a list of all Atoms in the Space, or None if that is impossible
- """
def get_atoms(self):
+ """
+ Returns a list of all Atoms in the Space, or None if that is impossible.
+ """
res = hp.space_list(self.cspace)
if res == None:
return None
@@ -179,34 +237,38 @@ def get_atoms(self):
result.append(Atom._from_catom(r))
return result
- """
- Returns the Space object referenced by the SpaceRef, or None if the object does not have a
- direct Python interface
- """
def get_payload(self):
+ """
+ Returns the Space object referenced by the SpaceRef, or None if the object does not have a
+ direct Python interface.
+ """
return hp.space_get_payload(self.cspace)
- """
- Performs the specified query on the Space, and returns the result BindingsSet
- """
def query(self, pattern):
+ """
+ Performs the specified query on the Space, and returns the result as a BindingsSet.
+ """
result = hp.space_query(self.cspace, pattern.catom)
return BindingsSet(result)
- """
- Performs a substitution within the Space
- """
def subst(self, pattern, templ):
+ """
+ Performs a substitution within the Space, based on a pattern and a template.
+ """
return [Atom._from_catom(catom) for catom in
hp.space_subst(self.cspace, pattern.catom,
templ.catom)]
-"""
-A reference to a native GroundingSpace, implemented by the MeTTa core library
-"""
class GroundingSpaceRef(SpaceRef):
+ """
+ A reference to a native GroundingSpace, implemented by the MeTTa core library.
+ """
def __init__(self, cspace = None):
+ """
+ Initialize a new GroundingSpaceRef.
+ If a CSpace object is provided, use it; otherwise create a new GroundingSpace.
+ """
if cspace is None:
self.cspace = hp.space_new_grounding()
else:
@@ -214,11 +276,21 @@ def __init__(self, cspace = None):
@staticmethod
def _from_cspace(cspace):
+ """
+ Creates a GroundingSpaceRef from a CSpace object.
+ """
return GroundingSpaceRef(cspace)
class Tokenizer:
+ """
+ A class responsible for text tokenization in the context of Hyperon.
+ This class wraps around a Tokenizer object from the core library.
+ """
def __init__(self, ctokenizer = None):
+ """
+ Initialize a new Tokenizer.
+ """
if ctokenizer is None:
self.ctokenizer = hp.tokenizer_new()
else:
@@ -226,57 +298,135 @@ def __init__(self, ctokenizer = None):
@staticmethod
def _from_ctokenizer(ctokenizer):
+ """
+ Creates a Tokenizer from a CTokenizer object.
+ """
return Tokenizer(ctokenizer)
def __del__(self):
+ """
+ Destructor that frees the underlying resources when the Tokenizer instance is destroyed.
+ """
hp.tokenizer_free(self.ctokenizer)
def register_token(self, regex, constr):
+ """
+ Registers a new custom Token in the Tokenizer based on a regular expression.
+
+ Parameters:
+ ----------
+ regex:
+ A string representing the regular expression to match incoming text.
+ Hyperon uses the Rust RegEx engine and syntax.
+ constr:
+ A constructor function for generating a new atom when the regex is triggered.
+ """
hp.tokenizer_register_token(self.ctokenizer, regex, constr)
class SExprParser:
+ """
+ A class responsible for parsing S-expressions (Symbolic Expressions).
+ This class wraps around a SExprParser object from the core library.
+ """
def __init__(self, text):
+ """Initialize a new SExprParser object."""
self.cparser = hp.CSExprParser(text)
def parse(self, tokenizer):
+ """
+ Parses the S-expression using the provided Tokenizer.
+ """
catom = self.cparser.parse(tokenizer.ctokenizer)
return Atom._from_catom(catom) if catom is not None else None
class Interpreter:
+ """
+ A wrapper class for the MeTTa interpreter that handles the interpretation of expressions in a given grounding space.
+ """
def __init__(self, gnd_space, expr):
+ """
+ Initializes the interpreter with the given grounding space and expression.
+ """
self.step_result = hp.interpret_init(gnd_space.cspace, expr.catom)
def has_next(self):
+ """
+ Checks if there are more steps to execute in the interpretation plan.
+ """
return hp.step_has_next(self.step_result)
def next(self):
+ """
+ Executes the next step in the interpretation plan.
+ """
if not self.has_next():
raise StopIteration()
self.step_result = hp.interpret_step(self.step_result)
def get_result(self):
+ """
+ Retrieves the final outcome of the interpretation plan.
+ """
if self.has_next():
raise RuntimeError("Plan execution is not finished")
return hp.step_get_result(self.step_result)
def get_step_result(self):
+ """
+ Gets the current result of the interpretation plan.
+ """
return self.step_result
def interpret(gnd_space, expr):
+ """
+ Parses the given expression in the specified grounding space.
+ """
interpreter = Interpreter(gnd_space, expr)
while interpreter.has_next():
interpreter.next()
return [Atom._from_catom(catom) for catom in interpreter.get_result()]
def check_type(gnd_space, atom, type):
+ """
+ Checks whether the given Atom has the specified type in the given space context.
+
+ Parameters
+ ----------
+ gnd_space:
+ A pointer to the space_t representing the space context in which to perform
+ the check
+ atom:
+ A pointer to the atom_t or atom_ref_t representing the atom whose Type the
+ function will check
+ type:
+ A pointer to the atom_t or atom_ref_t representing the type to check against
+ """
+
return hp.check_type(gnd_space.cspace, atom.catom, type.catom)
def validate_atom(gnd_space, atom):
+ """
+ Checks whether the given Atom is correctly typed.
+
+ Parameters
+ ----------
+ gnd_space:
+ A pointer to the space_t representing the space context in which to perform
+ the check
+ atom:
+ A pointer to the atom_t or atom_ref_t representing the atom whose Type the
+ function will check
+
+ Returns
+ -------
+ True if the Atom is correctly typed, otherwise false
+ """
return hp.validate_atom(gnd_space.cspace, atom.catom)
def get_atom_types(gnd_space, atom):
+ """Provides all types for the given Atom in the context of the given Space."""
result = hp.get_atom_types(gnd_space.cspace, atom.catom)
return [Atom._from_catom(catom) for catom in result]
diff --git a/python/hyperon/ext.py b/python/hyperon/ext.py
index 90cdca070..ac2629b58 100644
--- a/python/hyperon/ext.py
+++ b/python/hyperon/ext.py
@@ -1,29 +1,41 @@
from .runner import MeTTa
def register_results(method, args, kwargs):
+ """Returns a decorator for registering the results of a method.
+ The behavior of the decorator depends on whether it is used with or without arguments."""
+
+ # Case 1: Decorator used without arguments (i.e., @decorator instead of @decorator(args))
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
- # no arguments
- func = args[0]
+ func = args[0] # func is the decorated function
+
+ # Define the decorator
def metta_register(metta):
+ # Register the results of calling the decorated function using the provided method
method(metta, func())
return metta_register
+
+ # Case 2: Decorator used with arguments (i.e., @decorator(args))
else:
- # with arguments
+ # Check if the decorator is used with arguments
pass_metta = kwargs.get('pass_metta', False)
+
+ # Define the decorator
def inner(func):
def metta_register(metta):
+ # Get the results of calling the decorated function
regs = func(metta) if pass_metta else func()
+ # Register the results using the provided method
method(metta, regs)
return metta_register
return inner
-def register_atoms(*args, **kwargs):
+def register_atoms(*args, pass_metta=False, **kwargs):
"""Function decorator which registers returned pairs of regular expressions
and atoms in MeTTa tokenizer using MeTTa.register_atom() method.
Parameters
----------
- pass_metta : bool, optional
+ pass_metta:
Pass instance of MeTTa class to the decorated function as an argument.
Default is False.
"""
diff --git a/python/hyperon/metta.py b/python/hyperon/metta.py
index 5f7eaeb02..ca77757d4 100644
--- a/python/hyperon/metta.py
+++ b/python/hyperon/metta.py
@@ -1,8 +1,23 @@
+"""
+This is the MeTTa entrypoint
+"""
import sys
import argparse
import hyperon
def main():
+ """
+ usage: metta.py [-h] metta file
+
+ Metta script interpreter
+
+ positional arguments:
+ metta file metta script
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ """
parser = argparse.ArgumentParser(description='Metta script interpreter')
group = parser.add_mutually_exclusive_group()
group.add_argument(
diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py
index 78f4aa33e..da428df98 100644
--- a/python/hyperon/runner.py
+++ b/python/hyperon/runner.py
@@ -5,6 +5,7 @@
from .base import GroundingSpaceRef, Tokenizer, SExprParser
class MeTTa:
+ """This class contains the MeTTa program execution utilities"""
def __init__(self, space = None, cwd = ".", cmetta = None):
if cmetta is not None:
@@ -25,15 +26,19 @@ def __del__(self):
hp.metta_free(self.cmetta)
def space(self):
+ """Gets the metta space"""
return GroundingSpaceRef._from_cspace(hp.metta_space(self.cmetta))
def tokenizer(self):
+ """Gets the tokenizer"""
return Tokenizer._from_ctokenizer(hp.metta_tokenizer(self.cmetta))
def register_token(self, regexp, constr):
+ """Registers a token"""
self.tokenizer().register_token(regexp, constr)
def register_atom(self, name, symbol):
+ """Registers an Atom"""
self.register_token(name, lambda _: symbol)
def _parse_all(self, program):
@@ -45,12 +50,15 @@ def _parse_all(self, program):
yield atom
def parse_all(self, program):
+ """Parse the entire program"""
return list(self._parse_all(program))
def parse_single(self, program):
+ """Parse the next single line in the program"""
return next(self._parse_all(program))
def load_py_module(self, name):
+ """Loads the given python module"""
if not isinstance(name, str):
name = repr(name)
mod = import_module(name)
@@ -60,6 +68,7 @@ def load_py_module(self, name):
obj(self)
def import_file(self, fname):
+ """Loads the program file and runs it"""
path = fname.split(os.sep)
if len(path) == 1:
path = ['.'] + path
@@ -75,6 +84,7 @@ def import_file(self, fname):
return result
def run(self, program, flat=False):
+ """Runs the program"""
parser = SExprParser(program)
results = hp.metta_run(self.cmetta, parser.cparser)
if flat: