diff --git a/.vitepress/config.mts b/.vitepress/config.mts index ec5a6e9c..274d83e0 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -36,7 +36,7 @@ export default defineConfig({ items: [ { text: "Terms", link: "/en/concepts/terms" }, { text: "Rules", link: "/en/concepts/rules" }, - { text: "Search Engine", link: "/en/concepts/search" }, + { text: "Engine", link: "/en/concepts/engine" }, ], }, { @@ -94,7 +94,7 @@ export default defineConfig({ items: [ { text: "Term", link: "/zh/concepts/terms" }, { text: "Rule", link: "/zh/concepts/rules" }, - { text: "搜索引擎", link: "/zh/concepts/search" }, + { text: "引擎", link: "/zh/concepts/engine" }, ], }, { diff --git a/README.md b/README.md index a957a079..74aa58cb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A deductive system for logical inference, implemented in C++. The library provid - **Rule-Based Inference**: Flexible framework for defining rules and facts to perform complex logical deduction. - **Unification Engine**: Powerful built-in mechanisms for term unification and rule matching. - **Automated Search**: Built-in search engine for iterative inference. +- **Chain Inference**: Chain engine that matches all premises of a rule in a single cycle. ## Installation @@ -266,6 +267,7 @@ console.log(mp.match(pq).toString()); // "(! (! `x))\n----------\n`x\n" - `Term`: General term class (variable, item, or list) - `Rule`: Logical rule class - `Search`: Search engine for inference +- `Chain`: Chain engine for inference (matches all premises in a single cycle) ### Python @@ -278,6 +280,7 @@ console.log(mp.match(pq).toString()); // "(! (! `x))\n----------\n`x\n" - `Term`: General term class - `Rule`: Logical rule class - `Search`: Search engine for inference +- `Chain`: Chain engine for inference (matches all premises in a single cycle) ### C++ (Core) @@ -290,6 +293,7 @@ All classes are in the `ds` namespace: - `term_t`: General terms - `rule_t`: Logical rules - `search_t`: Search engine (in ``) +- `chain_t`: Chain engine (in ``) See header files in `include/ds/` for detailed API documentation. diff --git a/apyds/__init__.py b/apyds/__init__.py index f414a93a..34f42721 100644 --- a/apyds/__init__.py +++ b/apyds/__init__.py @@ -13,6 +13,7 @@ "Term", "Rule", "Search", + "Chain", ] from .buffer_size import buffer_size, scoped_buffer_size @@ -23,3 +24,4 @@ from .term_t import Term from .rule_t import Rule from .search_t import Search +from .chain_t import Chain diff --git a/apyds/_ds.pyi b/apyds/_ds.pyi index bd741f18..0358a1fd 100644 --- a/apyds/_ds.pyi +++ b/apyds/_ds.pyi @@ -674,3 +674,57 @@ class Search: The number of rules processed. """ ... + +class Chain: + """C++ binding for ds::chain_t.""" + + def __init__(self, limit_size: int, buffer_size: int) -> None: + """Create a new chain engine instance. + + Args: + limit_size: Size of the buffer for storing final objects. + buffer_size: Size of the buffer for internal operations. + """ + ... + + def set_limit_size(self, limit_size: int) -> None: + """Set the size of the buffer for storing final objects. + + Args: + limit_size: The new limit size. + """ + ... + + def set_buffer_size(self, buffer_size: int) -> None: + """Set the buffer size for internal operations. + + Args: + buffer_size: The new buffer size. + """ + ... + + def reset(self) -> None: + """Reset the chain engine, clearing all rules and facts.""" + ... + + def add(self, text: str) -> bool: + """Add a rule or fact to the knowledge base. + + Args: + text: The rule or fact as a string. + + Returns: + True if successfully added, False otherwise. + """ + ... + + def execute(self, callback: Callable[[Rule], bool]) -> int: + """Execute the chain engine with a callback for each inferred rule. + + Args: + callback: Function called for each candidate rule. + Return False to continue, True to stop. + + Returns: + The number of rules processed. + """ diff --git a/apyds/chain_t.py b/apyds/chain_t.py new file mode 100644 index 00000000..33d99447 --- /dev/null +++ b/apyds/chain_t.py @@ -0,0 +1,80 @@ +"""Chain engine for the deductive system.""" + +__all__ = [ + "Chain", +] + +import typing +from . import ds +from .rule_t import Rule + + +class Chain: + """Chain engine for the deductive system. + + Similar to Search, but matches all premises of a rule in a single cycle. + + Example: + >>> chain = Chain() + >>> chain.add("p q r") + >>> chain.add("p") + >>> chain.add("q") + >>> def callback(rule): + ... print(rule) + ... return False # Return False to continue, True to stop + >>> chain.execute(callback) # Will find r in a single cycle + """ + + def __init__(self, limit_size: int = 1000, buffer_size: int = 10000): + """Creates a new chain engine instance. + + Args: + limit_size: Size of the buffer for storing the final objects (rules/facts) + in the knowledge base (default: 1000). + buffer_size: Size of the buffer for internal operations like conversions + and transformations (default: 10000). + """ + self._chain: ds.Chain = ds.Chain(limit_size, buffer_size) + + def set_limit_size(self, limit_size: int) -> None: + """Set the size of the buffer for storing final objects. + + Args: + limit_size: The new limit size for storing rules/facts. + """ + self._chain.set_limit_size(limit_size) + + def set_buffer_size(self, buffer_size: int) -> None: + """Set the buffer size for internal operations. + + Args: + buffer_size: The new buffer size. + """ + self._chain.set_buffer_size(buffer_size) + + def reset(self) -> None: + """Reset the chain engine, clearing all rules and facts.""" + self._chain.reset() + + def add(self, text: str) -> bool: + """Add a rule or fact to the knowledge base. + + Args: + text: The rule or fact as a string. + + Returns: + True if successfully added, False otherwise. + """ + return self._chain.add(text) + + def execute(self, callback: typing.Callable[[Rule], bool]) -> int: + """Execute the chain engine with a callback for each inferred rule. + + Args: + callback: Function called for each candidate rule. Return False to continue, + True to stop. + + Returns: + The number of rules processed. + """ + return self._chain.execute(lambda candidate: callback(Rule(candidate.clone()))) diff --git a/apyds/ds.cc b/apyds/ds.cc index 63398118..42b86f99 100644 --- a/apyds/ds.cc +++ b/apyds/ds.cc @@ -1,3 +1,4 @@ +#include #include #include #include @@ -168,4 +169,12 @@ PYBIND11_MODULE(_ds, m, py::mod_gil_not_used()) { search_t.def("reset", &ds::search_t::reset); search_t.def("add", &ds::search_t::add); search_t.def("execute", &ds::search_t::execute); + + auto chain_t = py::class_(m, "Chain"); + chain_t.def(py::init()); + chain_t.def("set_limit_size", &ds::chain_t::set_limit_size); + chain_t.def("set_buffer_size", &ds::chain_t::set_buffer_size); + chain_t.def("reset", &ds::chain_t::reset); + chain_t.def("add", &ds::chain_t::add); + chain_t.def("execute", &ds::chain_t::execute); } diff --git a/apyds/ds.py b/apyds/ds.py index e0bf52d5..8073ceef 100644 --- a/apyds/ds.py +++ b/apyds/ds.py @@ -8,6 +8,7 @@ "Term", "Rule", "Search", + "Chain", ] -from ._ds import String, Variable, Item, List, Term, Rule, Search +from ._ds import String, Variable, Item, List, Term, Rule, Search, Chain diff --git a/atsds/ds.cc b/atsds/ds.cc index 1e9b5b14..86a91b8a 100644 --- a/atsds/ds.cc +++ b/atsds/ds.cc @@ -1,3 +1,4 @@ +#include #include #include #include @@ -133,6 +134,14 @@ auto search_execute(ds::search_t* search, const em::val& callback) -> ds::length return search->execute([&callback](ds::rule_t* candidate) -> bool { return callback(candidate, em::allow_raw_pointers()).as(); }); } +auto chain_add(ds::chain_t* chain, const std::string& text) -> bool { + return chain->add(text); +} + +auto chain_execute(ds::chain_t* chain, const em::val& callback) -> ds::length_t { + return chain->execute([&callback](ds::rule_t* candidate) -> bool { return callback(candidate, em::allow_raw_pointers()).as(); }); +} + EMSCRIPTEN_BINDINGS(ds) { em::register_vector("Buffer"); @@ -186,4 +195,13 @@ EMSCRIPTEN_BINDINGS(ds) { // 因为embind的限制,这里无法使用string_view和function。 search_t.function("add", &search_add, em::allow_raw_pointers()); search_t.function("execute", &search_execute, em::allow_raw_pointers()); + + auto chain_t = em::class_("Chain"); + chain_t.constructor(); + chain_t.function("set_limit_size", &ds::chain_t::set_limit_size); + chain_t.function("set_buffer_size", &ds::chain_t::set_buffer_size); + chain_t.function("reset", &ds::chain_t::reset); + // 因为 embind 的限制,这里无法使用 string_view 和 function。 + chain_t.function("add", &chain_add, em::allow_raw_pointers()); + chain_t.function("execute", &chain_execute, em::allow_raw_pointers()); } diff --git a/atsds/index.mts b/atsds/index.mts index 979241b4..a40a36c3 100644 --- a/atsds/index.mts +++ b/atsds/index.mts @@ -621,3 +621,80 @@ export class Search { }); } } + +/** + * Chain engine for the deductive system. + * Similar to Search, but matches all premises of a rule in a single cycle. + * + * @example + * ```typescript + * const chain = new Chain(); + * chain.add("p q r"); + * chain.add("p"); + * chain.add("q"); + * chain.execute((rule) => { + * console.log(rule.toString()); // Will find r in a single cycle + * return false; // Return false to continue, true to stop + * }); + * ``` + */ +export class Chain { + _chain: dst.Chain; + + /** + * Creates a new chain engine instance. + * + * @param limit_size - Size of the buffer for storing the final objects (rules/facts) in the knowledge base (default: 1000). + * @param buffer_size - Size of the buffer for internal operations like conversions and transformations (default: 10000). + */ + constructor(limit_size: number = 1000, buffer_size: number = 10000) { + this._chain = new ds.Chain(limit_size, buffer_size); + } + + /** + * Set the size of the buffer for storing final objects. + * + * @param limit_size - The new limit size for storing rules/facts. + */ + set_limit_size(limit_size: number): void { + this._chain.set_limit_size(limit_size); + } + + /** + * Set the buffer size for internal operations. + * + * @param buffer_size - The new buffer size. + */ + set_buffer_size(buffer_size: number): void { + this._chain.set_buffer_size(buffer_size); + } + + /** + * Reset the chain engine, clearing all rules and facts. + */ + reset(): void { + this._chain.reset(); + } + + /** + * Add a rule or fact to the knowledge base. + * + * @param text - The rule or fact as a string. + * @returns True if successfully added, false otherwise. + */ + add(text: string): boolean { + return this._chain.add(text); + } + + /** + * Execute the chain engine with a callback for each inferred rule. + * + * @param callback - Function called for each candidate rule. Return false to continue, true to stop. + * @returns The number of rules processed. + */ + execute(callback: (candidate: Rule) => boolean): number { + return this._chain.execute((candidate: dst.Rule): boolean => { + return callback(new Rule(candidate).copy()); + }); + } +} diff --git a/docs/en/api/cpp.md b/docs/en/api/cpp.md index 8d59f996..bc1eea4b 100644 --- a/docs/en/api/cpp.md +++ b/docs/en/api/cpp.md @@ -9,6 +9,7 @@ All classes and functions are in the `ds` namespace. ```cpp #include // All basic types #include // Search engine +#include // Chain engine #include // Helper functions ``` @@ -487,6 +488,75 @@ length_t execute(const std::function& callback); --- +## chain_t + +Chain engine class. Defined in ``. + +Similar to `search_t`, but matches all premises of a rule in a single cycle. + +### Constructor + +```cpp +chain_t(length_t limit_size, length_t buffer_size); +``` + +**Parameters:** + +- `limit_size`: Maximum size for each stored rule/fact +- `buffer_size`: Size of the internal buffer for operations + +### Methods + +#### set_limit_size() + +Set the maximum rule/fact size. + +```cpp +void set_limit_size(length_t limit_size); +``` + +#### set_buffer_size() + +Set the internal buffer size. + +```cpp +void set_buffer_size(length_t buffer_size); +``` + +#### reset() + +Clear all rules and facts. + +```cpp +void reset(); +``` + +#### add() + +Add a rule or fact from text. + +```cpp +bool add(std::string_view text); +``` + +#### execute() + +Execute one round of chain inference, matching all premises of each rule. + +```cpp +length_t execute(const std::function& callback); +``` + +**Parameters:** + +- `callback`: Function called for each new inference. Return false to continue, true to stop. + +**Returns:** The number of new inferences generated. + +**Note:** Unlike `search_t::execute()`, `chain_t::execute()` matches all premises of a rule completely in a single cycle. + +--- + ## Utility Functions Helper functions in ``. diff --git a/docs/en/api/python.md b/docs/en/api/python.md index c63ed7d0..e00ae682 100644 --- a/docs/en/api/python.md +++ b/docs/en/api/python.md @@ -13,6 +13,7 @@ from apyds import ( Term, Rule, Search, + Chain, ) ``` @@ -479,6 +480,92 @@ search.execute(callback) --- +## Chain + +Chain engine for the deductive system. + +Similar to `Search`, but matches all premises of a rule in a single cycle. + +### Constructor + +```python +def __init__(self, limit_size: int = 1000, buffer_size: int = 10000) +``` + +**Parameters:** + +- `limit_size` (optional): Size of the buffer for storing rules/facts (default: 1000) +- `buffer_size` (optional): Size of the buffer for internal operations (default: 10000) + +### Methods + +#### set_limit_size() + +Set the size of the buffer for storing final objects. + +```python +def set_limit_size(self, limit_size: int) -> None +``` + +#### set_buffer_size() + +Set the buffer size for internal operations. + +```python +def set_buffer_size(self, buffer_size: int) -> None +``` + +#### reset() + +Reset the chain engine, clearing all rules and facts. + +```python +def reset(self) -> None +``` + +#### add() + +Add a rule or fact to the knowledge base. + +```python +def add(self, text: str) -> bool +``` + +**Returns:** True if successfully added, False otherwise. + +#### execute() + +Execute the chain engine with a callback for each inferred rule. + +```python +def execute(self, callback: Callable[[Rule], bool]) -> int +``` + +**Parameters:** + +- `callback`: Function called for each candidate rule. Return False to continue, True to stop. + +**Returns:** The number of rules processed. + +**Note:** Unlike `Search.execute()`, `Chain.execute()` matches all premises of a rule completely in a single cycle. + +**Example:** + +```python +chain = Chain(100, 1000) +chain.add("p q r") # p and q imply r +chain.add("p") # fact: p +chain.add("q") # fact: q + +def callback(candidate): + print(candidate) + return False # Continue searching + +chain.execute(callback) # Will find r in a single cycle +``` + +--- + ## Complete Example Here's a complete example demonstrating most of the API: diff --git a/docs/en/api/typescript.md b/docs/en/api/typescript.md index ce2a1c55..95c27c2f 100644 --- a/docs/en/api/typescript.md +++ b/docs/en/api/typescript.md @@ -3,15 +3,16 @@ This page documents the TypeScript API for the `atsds` package. The documentation is generated from the TypeScript source code. ```typescript -import { +import { buffer_size, - String_, - Variable, - Item, - List, - Term, - Rule, - Search + String_, + Variable, + Item, + List, + Term, + Rule, + Search, + Chain } from "atsds"; ``` @@ -475,6 +476,91 @@ search.execute((candidate) => { --- +## Chain + +Chain engine for the deductive system. + +Similar to `Search`, but matches all premises of a rule in a single cycle. + +### Constructor + +```typescript +constructor(limit_size?: number, buffer_size?: number) +``` + +**Parameters:** + +- `limit_size` (optional): Size of the buffer for storing rules/facts (default: 1000) +- `buffer_size` (optional): Size of the buffer for internal operations (default: 10000) + +### Methods + +#### set_limit_size() + +Set the size of the buffer for storing final objects. + +```typescript +set_limit_size(limit_size: number): void +``` + +#### set_buffer_size() + +Set the buffer size for internal operations. + +```typescript +set_buffer_size(buffer_size: number): void +``` + +#### reset() + +Reset the chain engine, clearing all rules and facts. + +```typescript +reset(): void +``` + +#### add() + +Add a rule or fact to the knowledge base. + +```typescript +add(text: string): boolean +``` + +**Returns:** True if successfully added, false otherwise. + +#### execute() + +Execute the chain engine with a callback for each inferred rule. + +```typescript +execute(callback: (candidate: Rule) => boolean): number +``` + +**Parameters:** + +- `callback`: Function called for each candidate rule. Return false to continue, true to stop. + +**Returns:** The number of rules processed. + +**Note:** Unlike `Search.execute()`, `Chain.execute()` matches all premises of a rule completely in a single cycle. + +**Example:** + +```typescript +const chain = new Chain(100, 1000); +chain.add("p q r"); // p and q imply r +chain.add("p"); // fact: p +chain.add("q"); // fact: q + +chain.execute((rule) => { + console.log(rule.toString()); // Will find r in a single cycle + return false; +}); +``` + +--- + ## Complete Example Here's a complete example demonstrating most of the TypeScript API: diff --git a/docs/en/concepts/search.md b/docs/en/concepts/engine.md similarity index 83% rename from docs/en/concepts/search.md rename to docs/en/concepts/engine.md index 3950817b..ec2aac56 100644 --- a/docs/en/concepts/search.md +++ b/docs/en/concepts/engine.md @@ -267,3 +267,49 @@ search.reset(); 4. **Early Termination**: Return `true` from callback to stop as soon as target is found 5. **Deduplication**: The engine automatically deduplicates facts, avoiding redundant computation +## Chain Engine + +In addition to `Search`, the DS library also provides a `Chain` engine type with an identical API interface. + +::: code-group +```typescript [TypeScript] +import { Chain } from "atsds"; + +const chain = new Chain(1000, 10000); +chain.add("p q r"); +chain.add("p"); +chain.add("q"); +chain.execute((rule) => { + console.log(rule.toString()); + return false; +}); +``` +```python [Python] +import apyds + +chain = apyds.Chain(1000, 10000) +chain.add("p q r") +chain.add("p") +chain.add("q") +chain.execute(lambda rule: print(rule) or False) +``` +```cpp [C++] +#include + +ds::chain_t chain(1000, 10000); +chain.add("p q r"); +chain.add("p"); +chain.add("q"); +chain.execute([](ds::rule_t* rule) { + printf("%s\n", ds::rule_to_text(rule, 1000).get()); + return false; +}); +``` +::: + +The `Chain` engine provides the same methods as `Search`: +- `constructor(limit_size, buffer_size)` / `chain_t(limit_size, buffer_size)` +- `set_limit_size()` / `set_buffer_size()` / `reset()` +- `add()` / `execute()` + +**Key difference:** In a single `execute()` cycle, `Chain` matches **all premises** of each rule completely, while `Search` matches premises one at a time. This means `Chain` can derive conclusions from multi-premise rules in a single cycle. diff --git a/docs/zh/api/cpp.md b/docs/zh/api/cpp.md index 81589fab..d150e990 100644 --- a/docs/zh/api/cpp.md +++ b/docs/zh/api/cpp.md @@ -9,6 +9,7 @@ ```cpp #include // 所有基本类型 #include // 搜索引擎 +#include // 链式引擎 #include // 辅助函数 ``` @@ -487,6 +488,75 @@ length_t execute(const std::function& callback); --- +## chain_t + +链式引擎类。定义在 `` 中。 + +与 `search_t` 类似,但在单轮中会将 rule 的所有 premises 全部匹配完成。 + +### 构造函数 + +```cpp +chain_t(length_t limit_size, length_t buffer_size); +``` + +**参数:** + +- `limit_size`:每个存储的 Rule/事实的最大大小 +- `buffer_size`:操作的内部缓冲区大小 + +### 方法 + +#### set_limit_size() + +设置最大 Rule/事实大小。 + +```cpp +void set_limit_size(length_t limit_size); +``` + +#### set_buffer_size() + +设置内部缓冲区大小。 + +```cpp +void set_buffer_size(length_t buffer_size); +``` + +#### reset() + +清除所有 Rule 和事实。 + +```cpp +void reset(); +``` + +#### add() + +从文本添加 Rule 或事实。 + +```cpp +bool add(std::string_view text); +``` + +#### execute() + +执行一轮链式推理,匹配所有规则的所有前提。 + +```cpp +length_t execute(const std::function& callback); +``` + +**参数:** + +- `callback`:对每个新推理调用的函数。返回 false 继续,返回 true 停止。 + +**返回值:** 生成的新推理数量。 + +**注意:** 与 `search_t::execute()` 不同,`chain_t::execute()` 在单轮中会将 rule 的所有 premises 全部匹配完成。 + +--- + ## 辅助函数 `` 中的辅助函数。 diff --git a/docs/zh/api/python.md b/docs/zh/api/python.md index 6ef902a5..b992c732 100644 --- a/docs/zh/api/python.md +++ b/docs/zh/api/python.md @@ -13,6 +13,7 @@ from apyds import ( Term, Rule, Search, + Chain, ) ``` @@ -487,6 +488,92 @@ search.execute(callback) --- +## Chain + +演绎系统的链式引擎。 + +与 `Search` 类似,但在单轮中会将 rule 的所有 premises 全部匹配完成。 + +### 构造函数 + +```python +def __init__(self, limit_size: int = 1000, buffer_size: int = 10000) +``` + +**参数:** + +- `limit_size` (可选):用于存储 Rule/事实的缓冲区大小(默认值:1000) +- `buffer_size` (可选):用于内部操作的缓冲区大小(默认值:10000) + +### 方法 + +#### set_limit_size() + +设置存储最终对象的缓冲区大小。 + +```python +def set_limit_size(self, limit_size: int) -> None +``` + +#### set_buffer_size() + +设置内部操作的缓冲区大小。 + +```python +def set_buffer_size(self, buffer_size: int) -> None +``` + +#### reset() + +重置链式引擎,清除所有 Rule 和事实。 + +```python +def reset(self) -> None +``` + +#### add() + +向知识库添加 Rule 或事实。 + +```python +def add(self, text: str) -> bool +``` + +**返回值:** 如果添加成功则返回 True,否则返回 False。 + +#### execute() + +执行链式引擎,并为每个推导出的 Rule 调用回调。 + +```python +def execute(self, callback: Callable[[Rule], bool]) -> int +``` + +**参数:** + +- `callback`:对每个候选 Rule 调用的函数。返回 False 继续,返回 True 停止。 + +**返回值:** 处理的 Rule 数量。 + +**注意:** 与 `Search.execute()` 不同,`Chain.execute()` 在单轮中会将 rule 的所有 premises 全部匹配完成。 + +**示例:** + +```python +chain = Chain(100, 1000) +chain.add("p q r") # p 和 q 推出 r +chain.add("p") # 事实:p +chain.add("q") # 事实:q + +def callback(candidate): + print(candidate) + return False # 继续搜索 + +chain.execute(callback) # 将在单轮中找到 r +``` + +--- + ## 完整示例 diff --git a/docs/zh/api/typescript.md b/docs/zh/api/typescript.md index 40965bbc..3151cf22 100644 --- a/docs/zh/api/typescript.md +++ b/docs/zh/api/typescript.md @@ -3,15 +3,16 @@ 本页记录了 `atsds` 包的 TypeScript API。文档由 TypeScript 源代码生成。 ```typescript -import { +import { buffer_size, String_, - Variable, - Item, - List, - Term, - Rule, - Search + Variable, + Item, + List, + Term, + Rule, + Search, + Chain } from "atsds"; ``` @@ -476,6 +477,91 @@ search.execute((candidate) => { --- +## Chain + +演绎系统的链式引擎。 + +与 `Search` 类似,但在单轮中会将 rule 的所有 premises 全部匹配完成。 + +### 构造函数 + +```typescript +constructor(limit_size?: number, buffer_size?: number) +``` + +**参数:** + +- `limit_size` (可选):用于存储 Rule/事实的缓冲区大小(默认值:1000) +- `buffer_size` (可选):用于内部操作的缓冲区大小(默认值:10000) + +### 方法 + +#### set_limit_size() + +设置存储最终对象的缓冲区大小。 + +```typescript +set_limit_size(limit_size: number): void +``` + +#### set_buffer_size() + +设置内部操作的缓冲区大小。 + +```typescript +set_buffer_size(buffer_size: number): void +``` + +#### reset() + +重置链式引擎,清除所有 Rule 和事实。 + +```typescript +reset(): void +``` + +#### add() + +向知识库添加 Rule 或事实。 + +```typescript +add(text: string): boolean +``` + +**返回值:** 如果添加成功则返回 true,否则返回 false。 + +#### execute() + +执行链式引擎,并为每个推导出的 Rule 调用回调。 + +```typescript +execute(callback: (candidate: Rule) => boolean): number +``` + +**参数:** + +- `callback`:对每个候选 Rule 调用的函数。返回 false 继续,返回 true 停止。 + +**返回值:** 处理的 Rule 数量。 + +**注意:** 与 `Search.execute()` 不同,`Chain.execute()` 在单轮中会将 rule 的所有 premises 全部匹配完成。 + +**示例:** + +```typescript +const chain = new Chain(100, 1000); +chain.add("p q r"); // p 和 q 推出 r +chain.add("p"); // 事实:p +chain.add("q"); // 事实:q + +chain.execute((rule) => { + console.log(rule.toString()); // 将在单轮中找到 r + return false; +}); +``` + +--- + ## 完整示例 这是一个演示大多数 TypeScript API 的完整示例: diff --git a/docs/zh/concepts/search.md b/docs/zh/concepts/engine.md similarity index 83% rename from docs/zh/concepts/search.md rename to docs/zh/concepts/engine.md index 2bdf12bb..43cbee65 100644 --- a/docs/zh/concepts/search.md +++ b/docs/zh/concepts/engine.md @@ -266,3 +266,50 @@ search.reset(); 3. **迭代执行**:循环调用 `execute()` 以继续推理直到收敛 4. **提前终止**:从回调返回 `true` 以在找到目标后立即停止 5. **去重**:引擎自动对事实进行去重,避免冗余计算 + +## Chain 引擎 + +除了 `Search` 之外,DS 库还提供了一个 `Chain` 引擎类型,其 API 接口与 `Search` 完全相同。 + +::: code-group +```typescript [TypeScript] +import { Chain } from "atsds"; + +const chain = new Chain(1000, 10000); +chain.add("p q r"); +chain.add("p"); +chain.add("q"); +chain.execute((rule) => { + console.log(rule.toString()); + return false; +}); +``` +```python [Python] +import apyds + +chain = apyds.Chain(1000, 10000) +chain.add("p q r") +chain.add("p") +chain.add("q") +chain.execute(lambda rule: print(rule) or False) +``` +```cpp [C++] +#include + +ds::chain_t chain(1000, 10000); +chain.add("p q r"); +chain.add("p"); +chain.add("q"); +chain.execute([](ds::rule_t* rule) { + printf("%s\n", ds::rule_to_text(rule, 1000).get()); + return false; +}); +``` +::: + +`Chain` 引擎提供与 `Search` 相同的方法: +- `constructor(limit_size, buffer_size)` / `chain_t(limit_size, buffer_size)` +- `set_limit_size()` / `set_buffer_size()` / `reset()` +- `add()` / `execute()` + +**主要区别:** 在单次 `execute()` 循环中,`Chain` 会**完全匹配**每个规则的所有 premises,而 `Search` 一次只匹配一个 premise。这意味着 `Chain` 可以在单轮中从多前提规则推导出结论。 diff --git a/include/ds/chain.hh b/include/ds/chain.hh new file mode 100644 index 00000000..c2625fcb --- /dev/null +++ b/include/ds/chain.hh @@ -0,0 +1,76 @@ +#ifndef DS_CHAIN_HH +#define DS_CHAIN_HH + +#include +#include +#include +#include + +#include + +namespace ds { + /// @brief 用于进行链式推理搜索的类。 + /// @note 与 search_t 不同,chain_t 在单轮中会将 rule 的所有 premises 全部匹配完成。 + class chain_t { + /// @brief 用于比较rule_t的智能指针大小的类型,用于将其存储在map中。 + /// @note 该类型比较的是rule_t对象的大小,而不是指针地址。 + struct less_t { + /// @brief 判断两个rule_t的智能指针的大小关系。 + /// @param lhs 第一个rule_t的智能指针。 + /// @param rhs 第二个rule_t的智能指针。 + /// @return 如果第一个rule_t的智能指针小于第二个,则返回true;否则返回false。 + /// @note 该比较函数比较的是rule_t对象的大小,而不是指针地址。 + bool operator()(const std::unique_ptr& lhs, const std::unique_ptr& rhs) const; + }; + + /// @brief 每个有效rule_t的最大长度。 + length_t limit_size; + /// @brief 在搜索过程中使用的缓冲区最大长度。 + length_t buffer_size; + + /// @brief 已经完成的cycle,表示在此之前的所有rules都已经被处理过。 + /// @note 如果高于last_fact_cycle,则说明所有的facts都已经被处理过。 + length_t done_cycle; + /// @brief rules库中最大的cycle,此变量在更新rules前设置。 + /// @note 也会在添加fact时设置给fact,但chain不会利用fact的cycle进行判断。 + length_t current_cycle; + /// @brief facts库的最后更新时间,用于避免重复计算。 + length_t last_fact_cycle; + /// @brief 用于存储规则的map,键为rule_t的智能指针,值为其对应的cycle。 + std::map, length_t, less_t> rules; + /// @brief 用于存储事实的map,键为rule_t的智能指针,值为其对应的cycle。 + std::map, length_t, less_t> facts; + + /// @brief 用于存储搜索过程中使用的缓冲区。 + std::unique_ptr buffer; + public: + /// @brief 构造函数,用于初始化搜索对象 + /// @param _limit_size 每个有效rule_t的最大长度。 + /// @param _buffer_size 在搜索过程中使用的缓冲区最大长度。 + chain_t(length_t _limit_size, length_t _buffer_size); + + /// @brief 设置每个有效rule_t的最大长度。 + /// @param _limit_size 每个有效rule_t的最大长度。 + void set_limit_size(length_t _limit_size); + + /// @brief 设置在搜索过程中使用的缓冲区最大长度。 + /// @param _buffer_size 在搜索过程中使用的缓冲区最大长度。 + void set_buffer_size(length_t _buffer_size); + + /// @brief 重置搜索过程中的所有状态。 + void reset(); + + /// @brief 向本搜索对象添加一个rule或fact。 + /// @param text 描述rule或fact的文本。 + /// @return 如果添加成功则返回true,否则返回false。 + bool add(std::string_view text); + + /// @brief 执行一轮搜索操作,遍历所有规则和事实,并对每个匹配的规则执行回调函数。 + /// @param callback 回调函数,每个新中找到的结果都会调用此函数。 + /// @return 搜索到新的结果的数量。 + /// @note 如果回调函数返回false,则继续搜索;如果回调函数返回true,则停止搜索。 + length_t execute(const std::function& callback); + }; +} // namespace ds + +#endif diff --git a/src/chain.cc b/src/chain.cc new file mode 100644 index 00000000..2a463429 --- /dev/null +++ b/src/chain.cc @@ -0,0 +1,121 @@ +#include +#include + +#include +#include + +namespace ds { + bool chain_t::less_t::operator()(const std::unique_ptr& lhs, const std::unique_ptr& rhs) const { + const length_t lhs_size = lhs->data_size(); + const length_t rhs_size = rhs->data_size(); + if (lhs_size < rhs_size) { + return true; + } + if (lhs_size > rhs_size) { + return false; + } + if (std::memcmp(lhs->head(), rhs->head(), lhs_size) < 0) { + return true; + } + return false; + } + + chain_t::chain_t(length_t _limit_size, length_t _buffer_size) { + set_limit_size(_limit_size); + set_buffer_size(_buffer_size); + reset(); + } + + void chain_t::set_limit_size(length_t _limit_size) { + limit_size = _limit_size; + done_cycle = 0; + } + + void chain_t::set_buffer_size(length_t _buffer_size) { + buffer_size = _buffer_size; + buffer = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + done_cycle = 0; + } + + void chain_t::reset() { + done_cycle = 0; + current_cycle = 0; + last_fact_cycle = 0; + rules.clear(); + facts.clear(); + } + + bool chain_t::add(std::string_view text) { + auto candidate = text_to_rule(text.data(), limit_size); + if (candidate) { + if (done_cycle == current_cycle) { + ++current_cycle; + } + if (candidate->premises_count() != 0) { + rules.emplace(std::move(candidate), current_cycle); + } else { + facts.emplace(std::move(candidate), current_cycle); + last_fact_cycle = current_cycle; + } + return true; + } else { + return false; + } + } + + length_t chain_t::execute(const std::function& callback) { + std::set, less_t> temp_facts; + + bool break_all = false; + + std::function chain_recursive; + chain_recursive = [&](rule_t* rule, rule_t* workspace, std::byte* tail) -> void { + if (rule->premises_count() == 0) { + if (rule->data_size() > limit_size) { + return; + } + auto new_fact = std::unique_ptr(reinterpret_cast(operator new(rule->data_size()))); + memcpy(new_fact->head(), rule->head(), rule->data_size()); + if (facts.find(new_fact) != facts.end() || temp_facts.find(new_fact) != temp_facts.end()) { + return; + } + temp_facts.emplace(std::move(new_fact)); + if (callback(rule)) { + break_all = true; + } + return; + } + + for (auto& [fact, facts_cycle] : facts) { + workspace->match(rule, fact.get(), tail); + if (!workspace->valid()) { + continue; + } + chain_recursive(workspace, reinterpret_cast(workspace->tail()), tail); + } + }; + + for (auto& [rule, rules_cycle] : rules) { + if (rules_cycle <= done_cycle && last_fact_cycle <= done_cycle) { + continue; + } + + chain_recursive(rule.get(), buffer.get(), reinterpret_cast(buffer.get()) + buffer_size); + + if (break_all) { + break; + } + } + + if (!break_all) { + done_cycle = current_cycle; + } + ++current_cycle; + length_t count = temp_facts.size(); + for (auto it = temp_facts.begin(); it != temp_facts.end();) { + auto node = temp_facts.extract(it++); + facts.emplace(std::move(node.value()), current_cycle); + } + return count; + } +} // namespace ds diff --git a/tests/test_chain.cc b/tests/test_chain.cc new file mode 100644 index 00000000..a4d9351d --- /dev/null +++ b/tests/test_chain.cc @@ -0,0 +1,148 @@ +#include +#include +#include + +class TestChain : public ::testing::Test { + protected: + const ds::length_t limit_size = 100; + const ds::length_t buffer_size = 1000; + + TestChain() { } + ~TestChain() override { } + void SetUp() override { + chain = new ds::chain_t(limit_size, buffer_size); + } + void TearDown() override { + delete chain; + } + + ds::chain_t* chain; +}; + +TEST_F(TestChain, reset_parameters) { + chain->set_limit_size(50); + chain->set_buffer_size(500); + chain->reset(); +} + +TEST_F(TestChain, add_rule_and_fact) { + EXPECT_TRUE(chain->add("test rule")); + EXPECT_TRUE(chain->add("fact")); +} + +TEST_F(TestChain, add_fail) { + chain->set_limit_size(10); + EXPECT_FALSE(chain->add("a-long-facts-that-exceeds-limit")); +} + +TEST_F(TestChain, execute_single_premise) { + chain->add("p q"); + chain->add("p"); + auto target = ds::text_to_rule("q", limit_size); + bool success = false; + auto count = chain->execute([&success, &target](ds::rule_t* rule) { + if (memcmp(rule, target.get(), rule->data_size()) == 0) { + success = true; + return true; + } + return false; + }); + EXPECT_EQ(count, 1); + EXPECT_TRUE(success); +} + +TEST_F(TestChain, execute_multiple_premises_chain) { + chain->add("p q r"); + chain->add("p"); + chain->add("q"); + auto target = ds::text_to_rule("r", limit_size); + bool success = false; + auto count = chain->execute([&success, &target](ds::rule_t* rule) { + if (memcmp(rule, target.get(), rule->data_size()) == 0) { + success = true; + } + return false; + }); + EXPECT_EQ(count, 1); + EXPECT_TRUE(success); +} + +TEST_F(TestChain, execute_multiple_premises_partial) { + chain->add("p q r"); + chain->add("p"); + auto count = chain->execute([](ds::rule_t* rule) { return false; }); + EXPECT_EQ(count, 0); +} + +TEST_F(TestChain, execute_three_premises) { + chain->add("p q r s"); + chain->add("p"); + chain->add("q"); + chain->add("r"); + auto target = ds::text_to_rule("s", limit_size); + bool success = false; + auto count = chain->execute([&success, &target](ds::rule_t* rule) { + if (memcmp(rule, target.get(), rule->data_size()) == 0) { + success = true; + } + return false; + }); + EXPECT_EQ(count, 1); + EXPECT_TRUE(success); +} + +TEST_F(TestChain, execute_duplicated_fact) { + chain->add("p r"); + chain->add("q r"); + chain->add("p"); + chain->add("q"); + auto count = chain->execute([](ds::rule_t* rule) { return false; }); + EXPECT_EQ(count, 1); +} + +TEST_F(TestChain, execute_exceed) { + chain->set_limit_size(100); + EXPECT_TRUE(chain->add("(2 `x) (`x `x`)")); + EXPECT_TRUE(chain->add("(2 a-very-long-fact-that-exceeds-half-of-the-limit-size)")); + auto count = chain->execute([](ds::rule_t* rule) { return false; }); + EXPECT_EQ(count, 0); +} + +TEST_F(TestChain, dont_generate_duplicated_fact) { + EXPECT_TRUE(chain->add("aaaaa bbbbb")); + EXPECT_TRUE(chain->add("aaaaa")); + EXPECT_EQ(chain->execute([](ds::rule_t* rule) { return false; }), 1); + EXPECT_EQ(chain->execute([](ds::rule_t* rule) { return false; }), 0); +} + +TEST_F(TestChain, execute_exceed_by_too_many_premises) { + chain->set_limit_size(100); + chain->set_buffer_size(1000); + EXPECT_TRUE(chain->add("aaaaa bbbbb ccccc ddddd eeeee fffff")); + EXPECT_TRUE(chain->add("aaaaa")); + EXPECT_TRUE(chain->add("bbbbb")); + EXPECT_TRUE(chain->add("ccccc")); + EXPECT_TRUE(chain->add("ddddd")); + EXPECT_TRUE(chain->add("eeeee")); + EXPECT_EQ(chain->execute([](ds::rule_t* rule) { return false; }), 1); + chain->reset(); + chain->set_limit_size(100); + chain->set_buffer_size(1000); + EXPECT_TRUE(chain->add("aaaaa bbbbb ccccc ddddd eeeee fffff")); + EXPECT_TRUE(chain->add("aaaaa")); + EXPECT_TRUE(chain->add("bbbbb")); + EXPECT_TRUE(chain->add("ccccc")); + EXPECT_TRUE(chain->add("ddddd")); + EXPECT_TRUE(chain->add("eeeee")); + EXPECT_EQ(chain->execute([](ds::rule_t* rule) { return false; }), 1); + chain->reset(); + chain->set_limit_size(100); + chain->set_buffer_size(100); + EXPECT_TRUE(chain->add("aaaaa bbbbb ccccc ddddd eeeee fffff")); + EXPECT_TRUE(chain->add("aaaaa")); + EXPECT_TRUE(chain->add("bbbbb")); + EXPECT_TRUE(chain->add("ccccc")); + EXPECT_TRUE(chain->add("ddddd")); + EXPECT_TRUE(chain->add("eeeee")); + EXPECT_EQ(chain->execute([](ds::rule_t* rule) { return false; }), 0); +} diff --git a/tests/test_chain.mjs b/tests/test_chain.mjs new file mode 100644 index 00000000..3171cdab --- /dev/null +++ b/tests/test_chain.mjs @@ -0,0 +1,136 @@ +import { Chain, Rule } from "../atsds/index.mts"; + +let chain = null; + +beforeEach(() => { + chain = new Chain(100, 1000); +}); + +test("reset_parameters", () => { + chain.set_limit_size(50); + chain.set_buffer_size(500); + chain.reset(); +}); + +test("add_rule_and_fact", () => { + expect(chain.add("test rule")).toBe(true); + expect(chain.add("fact")).toBe(true); +}); + +test("add_fail", () => { + chain.set_limit_size(10); + expect(chain.add("a-long-facts-that-exceeds-limit")).toBe(false); +}); + +test("execute_single_premise", () => { + chain.add("p q"); + chain.add("p"); + const target = new Rule("q"); + let success = false; + const count = chain.execute((rule) => { + if (rule.key() === target.key()) { + success = true; + return true; + } + return false; + }); + expect(count).toBe(1); + expect(success).toBe(true); +}); + +test("execute_multiple_premises_chain", () => { + chain.add("p q r"); + chain.add("p"); + chain.add("q"); + const target = new Rule("r"); + let success = false; + const count = chain.execute((rule) => { + if (rule.key() === target.key()) { + success = true; + } + return false; + }); + expect(count).toBe(1); + expect(success).toBe(true); +}); + +test("execute_multiple_premises_partial", () => { + chain.add("p q r"); + chain.add("p"); + const count = chain.execute((rule) => false); + expect(count).toBe(0); +}); + +test("execute_three_premises", () => { + chain.add("p q r s"); + chain.add("p"); + chain.add("q"); + chain.add("r"); + const target = new Rule("s"); + let success = false; + const count = chain.execute((rule) => { + if (rule.key() === target.key()) { + success = true; + } + return false; + }); + expect(count).toBe(1); + expect(success).toBe(true); +}); + +test("execute_duplicated_fact", () => { + chain.add("p r"); + chain.add("q r"); + chain.add("p"); + chain.add("q"); + const count = chain.execute((rule) => false); + expect(count).toBe(1); +}); + +test("execute_exceed", () => { + chain.set_limit_size(100); + expect(chain.add("(2 `x) (`x `x`)")).toBe(true); + expect(chain.add("(2 a-very-long-fact-that-exceeds-half-of-the-limit-size)")).toBe(true); + const count = chain.execute((rule) => false); + expect(count).toBe(0); +}); + +test("dont_generate_duplicated_fact", () => { + expect(chain.add("aaaaa bbbbb")).toBe(true); + expect(chain.add("aaaaa")).toBe(true); + expect(chain.execute((rule) => false)).toBe(1); + expect(chain.execute((rule) => false)).toBe(0); +}); + +test("execute_exceed_by_too_many_premises", () => { + const newChain = new Chain(100, 1000); + expect(newChain.add("aaaaa bbbbb ccccc ddddd eeeee fffff")).toBe(true); + expect(newChain.add("aaaaa")).toBe(true); + expect(newChain.add("bbbbb")).toBe(true); + expect(newChain.add("ccccc")).toBe(true); + expect(newChain.add("ddddd")).toBe(true); + expect(newChain.add("eeeee")).toBe(true); + expect(newChain.execute((rule) => false)).toBe(1); + + newChain.reset(); + newChain.set_limit_size(100); + newChain.set_buffer_size(1000); + expect(newChain.add("aaaaa bbbbb ccccc ddddd eeeee fffff")).toBe(true); + expect(newChain.add("aaaaa")).toBe(true); + expect(newChain.add("bbbbb")).toBe(true); + expect(newChain.add("ccccc")).toBe(true); + expect(newChain.add("ddddd")).toBe(true); + expect(newChain.add("eeeee")).toBe(true); + expect(newChain.execute((rule) => false)).toBe(1); + + newChain.reset(); + newChain.set_limit_size(100); + newChain.set_buffer_size(100); + expect(newChain.add("aaaaa bbbbb ccccc ddddd eeeee fffff")).toBe(true); + expect(newChain.add("aaaaa")).toBe(true); + expect(newChain.add("bbbbb")).toBe(true); + expect(newChain.add("ccccc")).toBe(true); + expect(newChain.add("ddddd")).toBe(true); + expect(newChain.add("eeeee")).toBe(true); + expect(newChain.execute((rule) => false)).toBe(0); +}); diff --git a/tests/test_chain.py b/tests/test_chain.py new file mode 100644 index 00000000..7c83edbf --- /dev/null +++ b/tests/test_chain.py @@ -0,0 +1,142 @@ +import pytest +import apyds + + +@pytest.fixture +def chain() -> apyds.Chain: + return apyds.Chain(100, 1000) + + +def test_reset_parameters(chain: apyds.Chain) -> None: + chain.set_limit_size(50) + chain.set_buffer_size(500) + chain.reset() + + +def test_add_rule_and_fact(chain: apyds.Chain) -> None: + assert chain.add("test rule") + assert chain.add("fact") + + +def test_add_fail(chain: apyds.Chain) -> None: + chain.set_limit_size(10) + assert not chain.add("a-long-facts-that-exceeds-limit") + + +def test_execute_single_premise(chain: apyds.Chain) -> None: + chain.add("p q") + chain.add("p") + target = apyds.Rule("q") + success = False + + def callback(rule: apyds.Rule) -> bool: + nonlocal success + if rule == target: + success = True + return True + return False + + count = chain.execute(callback) + assert count == 1 + assert success + + +def test_execute_multiple_premises_chain(chain: apyds.Chain) -> None: + chain.add("p q r") + chain.add("p") + chain.add("q") + target = apyds.Rule("r") + success = False + + def callback(rule: apyds.Rule) -> bool: + nonlocal success + if rule == target: + success = True + return False + + count = chain.execute(callback) + assert count == 1 + assert success + + +def test_execute_multiple_premises_partial(chain: apyds.Chain) -> None: + chain.add("p q r") + chain.add("p") + count = chain.execute(lambda rule: False) + assert count == 0 + + +def test_execute_three_premises(chain: apyds.Chain) -> None: + chain.add("p q r s") + chain.add("p") + chain.add("q") + chain.add("r") + target = apyds.Rule("s") + success = False + + def callback(rule: apyds.Rule) -> bool: + nonlocal success + if rule == target: + success = True + return False + + count = chain.execute(callback) + assert count == 1 + assert success + + +def test_execute_duplicated_fact(chain: apyds.Chain) -> None: + chain.add("p r") + chain.add("q r") + chain.add("p") + chain.add("q") + count = chain.execute(lambda rule: False) + assert count == 1 + + +def test_execute_exceed(chain: apyds.Chain) -> None: + chain.set_limit_size(100) + assert chain.add("(2 `x) (`x `x`)") + assert chain.add("(2 a-very-long-fact-that-exceeds-half-of-the-limit-size)") + count = chain.execute(lambda rule: False) + assert count == 0 + + +def test_dont_generate_duplicated_fact(chain: apyds.Chain) -> None: + assert chain.add("aaaaa bbbbb") + assert chain.add("aaaaa") + assert chain.execute(lambda rule: False) == 1 + assert chain.execute(lambda rule: False) == 0 + + +def test_execute_exceed_by_too_many_premises() -> None: + chain = apyds.Chain(100, 1000) + assert chain.add("aaaaa bbbbb ccccc ddddd eeeee fffff") + assert chain.add("aaaaa") + assert chain.add("bbbbb") + assert chain.add("ccccc") + assert chain.add("ddddd") + assert chain.add("eeeee") + assert chain.execute(lambda rule: False) == 1 + + chain.reset() + chain.set_limit_size(100) + chain.set_buffer_size(1000) + assert chain.add("aaaaa bbbbb ccccc ddddd eeeee fffff") + assert chain.add("aaaaa") + assert chain.add("bbbbb") + assert chain.add("ccccc") + assert chain.add("ddddd") + assert chain.add("eeeee") + assert chain.execute(lambda rule: False) == 1 + + chain.reset() + chain.set_limit_size(100) + chain.set_buffer_size(100) + assert chain.add("aaaaa bbbbb ccccc ddddd eeeee fffff") + assert chain.add("aaaaa") + assert chain.add("bbbbb") + assert chain.add("ccccc") + assert chain.add("ddddd") + assert chain.add("eeeee") + assert chain.execute(lambda rule: False) == 0