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: