From c7acdb42e6fec65fc33d619c3ddcd247c0bae377 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Wed, 11 Mar 2026 22:54:50 +0800 Subject: [PATCH 01/11] feat: add chain_t for multi-premise matching in single cycle - Add chain_t class (include/ds/chain.hh, src/chain.cc) - Add tests for chain_t (tests/test_chain.cc) - chain_t matches all premises of a rule in a single cycle vs search_t which only matches one premise per cycle --- include/ds/chain.hh | 74 ++++++++++++++++++++ src/chain.cc | 167 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_chain.cc | 119 +++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 include/ds/chain.hh create mode 100644 src/chain.cc create mode 100644 tests/test_chain.cc diff --git a/include/ds/chain.hh b/include/ds/chain.hh new file mode 100644 index 00000000..09effcf6 --- /dev/null +++ b/include/ds/chain.hh @@ -0,0 +1,74 @@ +#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 和 facts 都已经被处理过。 + length_t done_cycle; + /// @brief rules 库和 facts 库中最大的 cycle,此变量在更新 rules 和 facts 前设置。 + length_t current_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; + /// @brief 用于存储链式匹配过程中使用的中间结果缓冲区。 + std::unique_ptr chain_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..fa58727a --- /dev/null +++ b/src/chain.cc @@ -0,0 +1,167 @@ +#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))); + chain_buffer = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + done_cycle = 0; + } + + void chain_t::reset() { + done_cycle = 0; + current_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); + } + return true; + } else { + return false; + } + } + + length_t chain_t::execute(const std::function& callback) { + std::set, less_t> temp_rules; + std::set, less_t> temp_facts; + + bool break_all = false; + for (auto& [rule, rules_cycle] : rules) { + length_t premises_count = rule->premises_count(); + + // 收集所有可能匹配第一个 premise 的 facts,生成初始的 partial rules + std::vector> current_chains; + + for (auto& [fact, facts_cycle] : facts) { + if (rules_cycle <= done_cycle && facts_cycle <= done_cycle) { + continue; + } + buffer->match(rule.get(), fact.get(), reinterpret_cast(buffer.get()) + buffer_size); + if (!buffer->valid()) { + continue; + } + if (buffer->data_size() > limit_size) { + continue; + } + // 复制中间结果 + auto new_rule = std::unique_ptr(reinterpret_cast(operator new(buffer->data_size()))); + memcpy(new_rule.get(), buffer.get(), buffer->data_size()); + current_chains.emplace_back(std::move(new_rule)); + } + + // 链式匹配剩余的 premises (从第 2 个开始) + for (length_t premise_index = 1; premise_index < premises_count; ++premise_index) { + if (current_chains.empty()) { + break; + } + + std::vector> next_chains; + + // 为每个 partial rule 匹配下一个 premise + for (auto& partial_chain : current_chains) { + // 此时 partial_chain 的第一个 premise 是待匹配的下一个 premise + for (auto& [fact, facts_cycle] : facts) { + chain_buffer->match(partial_chain.get(), fact.get(), reinterpret_cast(chain_buffer.get()) + buffer_size); + if (!chain_buffer->valid()) { + continue; + } + if (chain_buffer->data_size() > limit_size) { + continue; + } + // 复制新的中间结果 + auto new_rule = std::unique_ptr(reinterpret_cast(operator new(chain_buffer->data_size()))); + memcpy(new_rule.get(), chain_buffer.get(), chain_buffer->data_size()); + next_chains.emplace_back(std::move(new_rule)); + } + } + + current_chains = std::move(next_chains); + } + + // 处理最终的 chain 结果 + for (auto& result : current_chains) { + if (result->premises_count() != 0) { + // rule - 还有未匹配的 premises + if (rules.find(result) != rules.end() || temp_rules.find(result) != temp_rules.end()) { + continue; + } + auto new_rule = std::unique_ptr(reinterpret_cast(operator new(result->data_size()))); + memcpy(new_rule.get(), result.get(), result->data_size()); + temp_rules.emplace(std::move(new_rule)); + } else { + // fact - 所有 premises 已匹配完毕 + if (facts.find(result) != facts.end() || temp_facts.find(result) != temp_facts.end()) { + continue; + } + auto new_fact = std::unique_ptr(reinterpret_cast(operator new(result->data_size()))); + memcpy(new_fact.get(), result.get(), result->data_size()); + temp_facts.emplace(std::move(new_fact)); + } + if (callback(result.get())) { + break_all = true; + break; + } + } + + if (break_all) { + break; + } + } + + if (!break_all) { + done_cycle = current_cycle; + } + ++current_cycle; + length_t count = temp_rules.size() + temp_facts.size(); + for (auto it = temp_rules.begin(); it != temp_rules.end();) { + auto node = temp_rules.extract(it++); + rules.emplace(std::move(node.value()), current_cycle); + } + 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..eb34c287 --- /dev/null +++ b/tests/test_chain.cc @@ -0,0 +1,119 @@ +#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 { + search = new ds::chain_t(limit_size, buffer_size); + } + void TearDown() override { + delete search; + } + + ds::chain_t* search; +}; + +TEST_F(TestChain, reset_parameters) { + search->set_limit_size(50); + search->set_buffer_size(500); + search->reset(); +} + +TEST_F(TestChain, add_rule_and_fact) { + EXPECT_TRUE(search->add("test rule")); + EXPECT_TRUE(search->add("fact")); +} + +TEST_F(TestChain, add_fail) { + search->set_limit_size(10); + EXPECT_FALSE(search->add("a-long-facts-that-exceeds-limit")); +} + +TEST_F(TestChain, execute_single_premise) { + search->add("p q"); + search->add("p"); + auto target = ds::text_to_rule("q", limit_size); + bool success = false; + auto count = search->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) { + // p q r 表示:p, q |- r (两个 premises) + // 在单轮中应该同时匹配 p 和 q,直接得到 r + search->add("p q r"); + search->add("p"); + search->add("q"); + auto target = ds::text_to_rule("r", limit_size); + bool success = false; + auto count = search->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_partial) { + // p q r 表示:p, q |- r (两个 premises) + // 只有 p,没有 q,在 chain_t 中不会产生部分结果 + // 因为 chain_t 的设计是在单轮内匹配所有 premises + search->add("p q r"); + search->add("p"); + auto count = search->execute([](ds::rule_t* rule) { return false; }); + // 没有匹配完所有 premises,不会产生任何结果 + EXPECT_EQ(count, 0); +} + +TEST_F(TestChain, execute_three_premises) { + // p q r s 表示:p, q, r |- s (三个 premises) + // 在单轮中应该同时匹配 p, q, r,直接得到 s + search->add("p q r s"); + search->add("p"); + search->add("q"); + search->add("r"); + auto target = ds::text_to_rule("s", limit_size); + bool success = false; + auto count = search->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_duplicated_fact) { + search->add("p r"); + search->add("q r"); + search->add("p"); + search->add("q"); + auto count = search->execute([](ds::rule_t* rule) { return false; }); + EXPECT_EQ(count, 1); +} + +TEST_F(TestChain, execute_exceed) { + search->set_limit_size(100); + EXPECT_TRUE(search->add("(2 `x) (`x `x`)")); + EXPECT_TRUE(search->add("(2 a-very-long-fact-that-exceeds-half-of-the-limit-size)")); + auto count = search->execute([](ds::rule_t* rule) { return false; }); + EXPECT_EQ(count, 0); +} From f09de1072ef2d848fb7a9ca69b4274a1ad589808 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Wed, 11 Mar 2026 23:10:09 +0800 Subject: [PATCH 02/11] feat: add Chain class bindings for Python and TypeScript --- apyds/__init__.py | 2 + apyds/_ds.pyi | 54 +++++++++++++++++++++ apyds/chain_t.py | 80 +++++++++++++++++++++++++++++++ apyds/ds.cc | 9 ++++ apyds/ds.py | 3 +- atsds/ds.cc | 18 +++++++ atsds/index.mts | 77 +++++++++++++++++++++++++++++ tests/test_chain.mjs | 106 ++++++++++++++++++++++++++++++++++++++++ tests/test_chain.py | 112 +++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 apyds/chain_t.py create mode 100644 tests/test_chain.mjs create mode 100644 tests/test_chain.py 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..70091df7 --- /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") # p, q |- r (two premises) + >>> 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..cc429ee3 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"); // p, q |- r (two premises) + * 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/tests/test_chain.mjs b/tests/test_chain.mjs new file mode 100644 index 00000000..7f748671 --- /dev/null +++ b/tests/test_chain.mjs @@ -0,0 +1,106 @@ +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", () => { + // p q r means: p, q |- r (two premises) + // In chain_t, both premises are matched in a single cycle + 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 true; + } + return false; + }); + expect(count).toBe(1); + expect(success).toBe(true); +}); + +test("execute_multiple_premises_partial", () => { + // p q r means: p, q |- r (two premises) + // Only p, no q - chain_t won't produce partial results + // because it's designed to match all premises in a single cycle + chain.add("p q r"); + chain.add("p"); + const count = chain.execute((rule) => false); + // No result because not all premises are matched + expect(count).toBe(0); +}); + +test("execute_three_premises", () => { + // p q r s means: p, q, r |- s (three premises) + // In chain_t, all three premises are matched in a single cycle + 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 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); +}); diff --git a/tests/test_chain.py b/tests/test_chain.py new file mode 100644 index 00000000..77e5b259 --- /dev/null +++ b/tests/test_chain.py @@ -0,0 +1,112 @@ +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: + # p q r means: p, q |- r (two premises) + # In chain_t, both premises are matched in a single cycle + 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 True + return False + + count = chain.execute(callback) + assert count == 1 + assert success + + +def test_execute_multiple_premises_partial(chain: apyds.Chain) -> None: + # p q r means: p, q |- r (two premises) + # Only p, no q - chain_t won't produce partial results + # because it's designed to match all premises in a single cycle + chain.add("p q r") + chain.add("p") + count = chain.execute(lambda rule: False) + # No result because not all premises are matched + assert count == 0 + + +def test_execute_three_premises(chain: apyds.Chain) -> None: + # p q r s means: p, q, r |- s (three premises) + # In chain_t, all three premises are matched in a single cycle + 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 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 From 9edf6819fd8a6968fde6c0aadeaa25767fa6ffdb Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Wed, 11 Mar 2026 23:58:57 +0800 Subject: [PATCH 03/11] docs: add Chain class documentation Add documentation for the Chain/chain_t class to README and all API documentation files: - README.md: Add Chain to features and API overview sections - docs/en/api/typescript.md: Add Chain class API reference - docs/en/api/python.md: Add Chain class API reference - docs/en/api/cpp.md: Add chain_t class API reference - docs/zh/api/typescript.md: Add Chinese Chain class API reference - docs/zh/api/python.md: Add Chinese Chain class API reference - docs/zh/api/cpp.md: Add Chinese chain_t class API reference --- README.md | 4 ++ docs/en/api/cpp.md | 82 +++++++++++++++++++++++- docs/en/api/python.md | 92 +++++++++++++++++++++++++++ docs/en/api/typescript.md | 129 +++++++++++++++++++++++++++++++++----- docs/zh/api/cpp.md | 82 +++++++++++++++++++++++- docs/zh/api/python.md | 92 +++++++++++++++++++++++++++ docs/zh/api/typescript.md | 127 ++++++++++++++++++++++++++++++++----- 7 files changed, 573 insertions(+), 35 deletions(-) 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/docs/en/api/cpp.md b/docs/en/api/cpp.md index 8d59f996..0a7d2606 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,73 @@ length_t execute(const std::function& callback); --- +## chain_t + +Chain engine class. Defined in ``. + +Manages a knowledge base and performs logical inference. Unlike `search_t`, `chain_t` 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. + +```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. + +--- + ## Utility Functions Helper functions in ``. @@ -616,7 +684,19 @@ int main() { if (found) { std::cout << "Target found!" << std::endl; } - + + // Chain engine (matches all premises in a single cycle) + ds::chain_t chain(1000, 10000); + chain.add("p q r"); // p, q |- r (two premises) + chain.add("p"); + chain.add("q"); + + std::cout << "\nRunning chain inference:" << std::endl; + chain.execute([&](ds::rule_t* candidate) { + std::cout << " Derived: " << ds::rule_to_text(candidate, buffer_size).get(); + return false; + }); + return 0; } ``` diff --git a/docs/en/api/python.md b/docs/en/api/python.md index c63ed7d0..c2ea5c2c 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,88 @@ 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. + +**Example:** + +```python +chain = Chain() +chain.add("p q r") # p, q |- r (two premises) +chain.add("p") +chain.add("q") + +def callback(candidate): + print(candidate) + return False # Continue searching + +chain.execute(callback) +``` + +--- + ## Complete Example Here's a complete example demonstrating most of the API: @@ -535,4 +618,13 @@ for i in range(3): with apyds.scoped_buffer_size(4096): big_term = apyds.Term("(a b c d e f g h i j)") print(f"\nBig term: {big_term}") + +# Chain engine (matches all premises in a single cycle) +chain = apyds.Chain(1000, 10000) +chain.add("p q r") # p, q |- r (two premises) +chain.add("p") +chain.add("q") + +print("\nRunning chain inference:") +chain.execute(lambda r: print(f" Derived: {r}") or False) ``` diff --git a/docs/en/api/typescript.md b/docs/en/api/typescript.md index ce2a1c55..2d79ac4a 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,20 +476,102 @@ 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. + +**Example:** + +```typescript +const chain = new Chain(); +chain.add("p q r"); // p, q |- r (two premises) +chain.add("p"); +chain.add("q"); + +chain.execute((candidate) => { + console.log(candidate.toString()); + return false; // Continue searching +}); +``` + +--- + ## Complete Example Here's a complete example demonstrating most of the TypeScript API: ```typescript -import { - buffer_size, - String_, - Variable, - Item, - List, - Term, - Rule, - Search +import { + buffer_size, + String_, + Variable, + Item, + List, + Term, + Rule, + Search, + Chain } from "atsds"; // Configure buffer size @@ -547,4 +630,16 @@ for (let i = 0; i < 3; i++) { const rule1 = new Rule("(a b c)"); const rule2 = rule1.copy(); console.log(`\nRule comparison: ${rule1.key() === rule2.key()}`); // true + +// Chain engine (matches all premises in a single cycle) +const chain = new Chain(1000, 10000); +chain.add("p q r"); // p, q |- r (two premises) +chain.add("p"); +chain.add("q"); + +console.log("\nRunning chain inference:"); +chain.execute((r) => { + console.log(` Derived: ${r.toString()}`); + return false; +}); ``` diff --git a/docs/zh/api/cpp.md b/docs/zh/api/cpp.md index 81589fab..c065fff9 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,73 @@ length_t execute(const std::function& callback); --- +## chain_t + +链式引擎类。定义在 `` 中。 + +管理知识库并执行逻辑推理。与 `search_t` 不同,`chain_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 停止。 + +**返回值:** 生成的新推理数量。 + +--- + ## 辅助函数 `` 中的辅助函数。 @@ -616,7 +684,19 @@ int main() { if (found) { std::cout << "Target found!" << std::endl; } - + + // Chain engine (matches all premises in a single cycle) + ds::chain_t chain(1000, 10000); + chain.add("p q r"); // p, q |- r (two premises) + chain.add("p"); + chain.add("q"); + + std::cout << "\nRunning chain inference:" << std::endl; + chain.execute([&](ds::rule_t* candidate) { + std::cout << " Derived: " << ds::rule_to_text(candidate, buffer_size).get(); + return false; + }); + return 0; } ``` diff --git a/docs/zh/api/python.md b/docs/zh/api/python.md index 6ef902a5..9e4c0bb5 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,88 @@ search.execute(callback) --- +## Chain + +Chain engine for the deductive system. Similar to Search, but matches all premises of a rule in a single cycle. + +### 构造函数 + +```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 数量。 + +**示例:** + +```python +chain = Chain() +chain.add("p q r") # p, q |- r (two premises) +chain.add("p") +chain.add("q") + +def callback(candidate): + print(candidate) + return False # Continue searching + +chain.execute(callback) +``` + +--- + ## 完整示例 @@ -544,4 +627,13 @@ for i in range(3): with apyds.scoped_buffer_size(4096): big_term = apyds.Term("(a b c d e f g h i j)") print(f"\nBig term: {big_term}") + +# Chain engine (matches all premises in a single cycle) +chain = apyds.Chain(1000, 10000) +chain.add("p q r") # p, q |- r (two premises) +chain.add("p") +chain.add("q") + +print("\nRunning chain inference:") +chain.execute(lambda r: print(f" Derived: {r}") or False) ``` \ No newline at end of file diff --git a/docs/zh/api/typescript.md b/docs/zh/api/typescript.md index 40965bbc..57a34bc8 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,20 +477,102 @@ search.execute((candidate) => { --- +## Chain + +Chain engine for the deductive system. Similar to Search, but matches all premises of a rule in a single cycle. + +### 构造函数 + +```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 数量。 + +**示例:** + +```typescript +const chain = new Chain(); +chain.add("p q r"); // p, q |- r (two premises) +chain.add("p"); +chain.add("q"); + +chain.execute((candidate) => { + console.log(candidate.toString()); + return false; // Continue searching +}); +``` + +--- + ## 完整示例 这是一个演示大多数 TypeScript API 的完整示例: ```typescript -import { - buffer_size, - String_, - Variable, - Item, - List, - Term, - Rule, - Search +import { + buffer_size, + String_, + Variable, + Item, + List, + Term, + Rule, + Search, + Chain } from "atsds"; // Configure buffer size @@ -550,4 +633,16 @@ for (let i = 0; i < 3; i++) { const rule1 = new Rule("(a b c)"); const rule2 = rule1.copy(); console.log(`\nRule comparison: ${rule1.key() === rule2.key()}`); // true + +// Chain engine (matches all premises in a single cycle) +const chain = new Chain(1000, 10000); +chain.add("p q r"); // p, q |- r (two premises) +chain.add("p"); +chain.add("q"); + +console.log("\nRunning chain inference:"); +chain.execute((r) => { + console.log(` Derived: ${r.toString()}`); + return false; +}); ``` From f84095b2dfd80885aa005eb49716ebced6c2cfea Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 12:36:46 +0800 Subject: [PATCH 04/11] feat: fix chain_t execute and add set_max_depth support - Refactor chain_t::execute to use recursive matching for premises - Add max_depth parameter to control maximum recursion depth - Add set_max_depth() method to chain_t class - Reject rules with premises count exceeding max_depth in add() - Remove existing rules exceeding new max_depth when set_max_depth is called - Update cycle tracking: use last_fact_cycle instead of per-fact cycles - Add bindings for Python (apyds) and TypeScript (atsds) - Add tests for set_max_depth functionality - Update documentation for C++, Python, and TypeScript APIs --- apyds/chain_t.py | 12 +++ apyds/ds.cc | 1 + atsds/ds.cc | 1 + atsds/index.mts | 11 +++ docs/en/api/cpp.md | 12 +++ docs/en/api/python.md | 12 +++ docs/en/api/typescript.md | 12 +++ docs/zh/api/cpp.md | 12 +++ docs/zh/api/python.md | 12 +++ docs/zh/api/typescript.md | 12 +++ include/ds/chain.hh | 26 ++++-- src/chain.cc | 170 ++++++++++++++++++++++++-------------- tests/test_chain.cc | 18 ++++ tests/test_chain.mjs | 19 +++++ tests/test_chain.py | 19 +++++ 15 files changed, 280 insertions(+), 69 deletions(-) diff --git a/apyds/chain_t.py b/apyds/chain_t.py index 70091df7..b88b085b 100644 --- a/apyds/chain_t.py +++ b/apyds/chain_t.py @@ -52,6 +52,18 @@ def set_buffer_size(self, buffer_size: int) -> None: """ self._chain.set_buffer_size(buffer_size) + def set_max_depth(self, max_depth: int) -> None: + """Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). + + Args: + max_depth: The maximum recursion depth. + + Note: + Rules with premises count exceeding this value will be rejected when added. + After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. + """ + self._chain.set_max_depth(max_depth) + def reset(self) -> None: """Reset the chain engine, clearing all rules and facts.""" self._chain.reset() diff --git a/apyds/ds.cc b/apyds/ds.cc index 42b86f99..cb980ead 100644 --- a/apyds/ds.cc +++ b/apyds/ds.cc @@ -174,6 +174,7 @@ PYBIND11_MODULE(_ds, m, py::mod_gil_not_used()) { 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("set_max_depth", &ds::chain_t::set_max_depth); 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/atsds/ds.cc b/atsds/ds.cc index 86a91b8a..7f76255b 100644 --- a/atsds/ds.cc +++ b/atsds/ds.cc @@ -200,6 +200,7 @@ EMSCRIPTEN_BINDINGS(ds) { 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("set_max_depth", &ds::chain_t::set_max_depth); chain_t.function("reset", &ds::chain_t::reset); // 因为 embind 的限制,这里无法使用 string_view 和 function。 chain_t.function("add", &chain_add, em::allow_raw_pointers()); diff --git a/atsds/index.mts b/atsds/index.mts index cc429ee3..852c8e54 100644 --- a/atsds/index.mts +++ b/atsds/index.mts @@ -669,6 +669,17 @@ export class Chain { this._chain.set_buffer_size(buffer_size); } + /** + * Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). + * + * @param max_depth - The maximum recursion depth. + * @note Rules with premises count exceeding this value will be rejected when added. + * @note After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. + */ + set_max_depth(max_depth: number): void { + this._chain.set_max_depth(max_depth); + } + /** * Reset the chain engine, clearing all rules and facts. */ diff --git a/docs/en/api/cpp.md b/docs/en/api/cpp.md index 0a7d2606..c79b0f48 100644 --- a/docs/en/api/cpp.md +++ b/docs/en/api/cpp.md @@ -523,6 +523,18 @@ Set the internal buffer size. void set_buffer_size(length_t buffer_size); ``` +#### set_max_depth() + +Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). + +```cpp +void set_max_depth(length_t max_depth); +``` + +**Notes:** +- Rules with premises count exceeding this value will be rejected when added. +- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. + #### reset() Clear all rules and facts. diff --git a/docs/en/api/python.md b/docs/en/api/python.md index c2ea5c2c..35fc1e45 100644 --- a/docs/en/api/python.md +++ b/docs/en/api/python.md @@ -513,6 +513,18 @@ Set the buffer size for internal operations. def set_buffer_size(self, buffer_size: int) -> None ``` +#### set_max_depth() + +Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). + +```python +def set_max_depth(self, max_depth: int) -> None +``` + +**Notes:** +- Rules with premises count exceeding this value will be rejected when added. +- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. + #### reset() Reset the chain engine, clearing all rules and facts. diff --git a/docs/en/api/typescript.md b/docs/en/api/typescript.md index 2d79ac4a..263605f7 100644 --- a/docs/en/api/typescript.md +++ b/docs/en/api/typescript.md @@ -509,6 +509,18 @@ Set the buffer size for internal operations. set_buffer_size(buffer_size: number): void ``` +#### set_max_depth() + +Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). + +```typescript +set_max_depth(max_depth: number): void +``` + +**Notes:** +- Rules with premises count exceeding this value will be rejected when added. +- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. + #### reset() Reset the chain engine, clearing all rules and facts. diff --git a/docs/zh/api/cpp.md b/docs/zh/api/cpp.md index c065fff9..9ef7aa8e 100644 --- a/docs/zh/api/cpp.md +++ b/docs/zh/api/cpp.md @@ -523,6 +523,18 @@ void set_limit_size(length_t limit_size); void set_buffer_size(length_t buffer_size); ``` +#### set_max_depth() + +设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 + +```cpp +void set_max_depth(length_t max_depth); +``` + +**注意:** +- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 +- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 + #### reset() 清除所有 Rule 和事实。 diff --git a/docs/zh/api/python.md b/docs/zh/api/python.md index 9e4c0bb5..4e262e4d 100644 --- a/docs/zh/api/python.md +++ b/docs/zh/api/python.md @@ -521,6 +521,18 @@ def set_limit_size(self, limit_size: int) -> None def set_buffer_size(self, buffer_size: int) -> None ``` +#### set_max_depth() + +设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 + +```python +def set_max_depth(self, max_depth: int) -> None +``` + +**注意:** +- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 +- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 + #### reset() 重置搜索引擎,清除所有 Rule 和事实。 diff --git a/docs/zh/api/typescript.md b/docs/zh/api/typescript.md index 57a34bc8..de2cde46 100644 --- a/docs/zh/api/typescript.md +++ b/docs/zh/api/typescript.md @@ -510,6 +510,18 @@ set_limit_size(limit_size: number): void set_buffer_size(buffer_size: number): void ``` +#### set_max_depth() + +设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 + +```typescript +set_max_depth(max_depth: number): void +``` + +**注意:** +- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 +- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 + #### reset() 重置搜索引擎,清除所有 Rule 和事实。 diff --git a/include/ds/chain.hh b/include/ds/chain.hh index 09effcf6..f5ee4868 100644 --- a/include/ds/chain.hh +++ b/include/ds/chain.hh @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -27,20 +28,26 @@ namespace ds { length_t limit_size; /// @brief 在搜索过程中使用的缓冲区最大长度。 length_t buffer_size; + /// @brief 链式匹配的最大递归深度,即单个 rule 允许的最大 premise 数目。 + /// @note premises 数目超过此值的 rule 将无法被完整匹配,只能匹配前 max_depth 个 premises。 + length_t max_depth; - /// @brief 已经完成的 cycle,表示在此与此之前的所有 rules 和 facts 都已经被处理过。 + /// @brief 已经完成的 cycle,表示在此之前的所有 rules 都已经被处理过。 + /// @note 如果高于 last_fact_cycle,则说明所有的 facts 都已经被处理过。 length_t done_cycle; - /// @brief rules 库和 facts 库中最大的 cycle,此变量在更新 rules 和 facts 前设置。 + /// @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; - /// @brief 用于存储链式匹配过程中使用的中间结果缓冲区。 - std::unique_ptr chain_buffer; + /// @brief 用于存储链式匹配过程中使用的缓冲区(按深度层级存储)。 + /// @note buffers[i] 用于存储匹配第 i 个 premise 时的中间结果。 + std::vector> buffers; public: /// @brief 构造函数,用于初始化搜索对象 /// @param _limit_size 每个有效 rule_t 的最大长度。 @@ -55,12 +62,19 @@ namespace ds { /// @param _buffer_size 在搜索过程中使用的缓冲区最大长度。 void set_buffer_size(length_t _buffer_size); + /// @brief 设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 + /// @param _max_depth 最大递归深度。 + /// @note 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 + /// @note 修改此值后,会检查现有的所有 rules, premises 数目超过新 max_depth 的 rules 会被移除。 + void set_max_depth(length_t _max_depth); + /// @brief 重置搜索过程中的所有状态。 void reset(); /// @brief 向本搜索对象添加一个 rule 或 fact。 /// @param text 描述 rule 或 fact 的文本。 /// @return 如果添加成功则返回 true,否则返回 false。 + /// @note 如果添加的是 rule 且 premises 数目超过 max_depth,则失败。 bool add(std::string_view text); /// @brief 执行一轮搜索操作,遍历所有规则和事实,并对每个匹配的规则执行回调函数。 diff --git a/src/chain.cc b/src/chain.cc index fa58727a..fdc0035f 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -28,21 +28,52 @@ namespace ds { void chain_t::set_limit_size(length_t _limit_size) { limit_size = _limit_size; + max_depth = 1; done_cycle = 0; + last_fact_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))); - chain_buffer = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + buffers.clear(); + buffers.resize(max_depth); + for (auto& buf : buffers) { + buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + } done_cycle = 0; + last_fact_cycle = 0; + } + + void chain_t::set_max_depth(length_t _max_depth) { + max_depth = _max_depth; + // 移除 premises 数目超过新 max_depth 的 rules + for (auto it = rules.begin(); it != rules.end();) { + if (it->first->premises_count() > max_depth) { + it = rules.erase(it); + } else { + ++it; + } + } + // 重新分配 buffers + buffers.clear(); + buffers.resize(max_depth); + for (auto& buf : buffers) { + buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + } } void chain_t::reset() { done_cycle = 0; current_cycle = 0; + last_fact_cycle = 0; + max_depth = 1; rules.clear(); facts.clear(); + buffers.clear(); + buffers.resize(max_depth); + for (auto& buf : buffers) { + buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); + } } bool chain_t::add(std::string_view text) { @@ -52,9 +83,15 @@ namespace ds { ++current_cycle; } if (candidate->premises_count() != 0) { + // rule: 检查 premises 数目是否超过 max_depth + if (candidate->premises_count() > max_depth) { + return false; + } rules.emplace(std::move(candidate), current_cycle); } else { + // fact facts.emplace(std::move(candidate), current_cycle); + last_fact_cycle = current_cycle; } return true; } else { @@ -67,92 +104,99 @@ namespace ds { std::set, less_t> temp_facts; bool break_all = false; - for (auto& [rule, rules_cycle] : rules) { - length_t premises_count = rule->premises_count(); - // 收集所有可能匹配第一个 premise 的 facts,生成初始的 partial rules - std::vector> current_chains; + // 递归匹配函数 + // current_rule: 当前要匹配的 rule(随着匹配进行,已匹配的 premise 会被消耗) + // depth: 当前递归深度(即将匹配第 depth 个 premise,从 0 开始) + // match_recursive 会遍历所有 facts 尝试匹配 current_rule 的第一个 premise + // 匹配成功后递归处理剩余 premises + std::function match_recursive; + match_recursive = [&](rule_t* current_rule, length_t depth) -> void { + // 检查是否所有 premises 已匹配完成 + if (current_rule->premises_count() == 0) { + // 所有 premises 已匹配完成,current_rule 是一个 fact + // 这种情况应该在上一层匹配成功后处理,而不是在这里 + return; + } + + // 使用 buffers[depth] 作为当前深度的匹配缓冲区 + rule_t* match_buffer = buffers[depth].get(); + // 遍历所有 facts 匹配当前 rule 的第一个 premise for (auto& [fact, facts_cycle] : facts) { - if (rules_cycle <= done_cycle && facts_cycle <= done_cycle) { - continue; + if (break_all) { + return; } - buffer->match(rule.get(), fact.get(), reinterpret_cast(buffer.get()) + buffer_size); - if (!buffer->valid()) { + + match_buffer->match(current_rule, fact.get(), reinterpret_cast(match_buffer) + buffer_size); + if (!match_buffer->valid()) { continue; } - if (buffer->data_size() > limit_size) { + if (match_buffer->data_size() > limit_size) { continue; } - // 复制中间结果 - auto new_rule = std::unique_ptr(reinterpret_cast(operator new(buffer->data_size()))); - memcpy(new_rule.get(), buffer.get(), buffer->data_size()); - current_chains.emplace_back(std::move(new_rule)); - } - - // 链式匹配剩余的 premises (从第 2 个开始) - for (length_t premise_index = 1; premise_index < premises_count; ++premise_index) { - if (current_chains.empty()) { - break; - } - - std::vector> next_chains; - // 为每个 partial rule 匹配下一个 premise - for (auto& partial_chain : current_chains) { - // 此时 partial_chain 的第一个 premise 是待匹配的下一个 premise - for (auto& [fact, facts_cycle] : facts) { - chain_buffer->match(partial_chain.get(), fact.get(), reinterpret_cast(chain_buffer.get()) + buffer_size); - if (!chain_buffer->valid()) { - continue; - } - if (chain_buffer->data_size() > limit_size) { - continue; - } - // 复制新的中间结果 - auto new_rule = std::unique_ptr(reinterpret_cast(operator new(chain_buffer->data_size()))); - memcpy(new_rule.get(), chain_buffer.get(), chain_buffer->data_size()); - next_chains.emplace_back(std::move(new_rule)); + // 复制中间结果 + auto next_rule = std::unique_ptr(reinterpret_cast(operator new(match_buffer->data_size()))); + memcpy(next_rule.get(), match_buffer.get(), match_buffer->data_size()); + + // 如果 next_rule 没有 premises 了,说明匹配完成,直接处理结果 + if (next_rule->premises_count() == 0) { + // 是 fact + if (facts.find(next_rule) == facts.end() && temp_facts.find(next_rule) == temp_facts.end()) { + auto copied = std::unique_ptr(reinterpret_cast(operator new(next_rule->data_size()))); + memcpy(copied.get(), next_rule.get(), next_rule->data_size()); + temp_facts.emplace(std::move(copied)); } - } - - current_chains = std::move(next_chains); - } - - // 处理最终的 chain 结果 - for (auto& result : current_chains) { - if (result->premises_count() != 0) { - // rule - 还有未匹配的 premises - if (rules.find(result) != rules.end() || temp_rules.find(result) != temp_rules.end()) { - continue; + if (callback(next_rule.get())) { + break_all = true; + return; } - auto new_rule = std::unique_ptr(reinterpret_cast(operator new(result->data_size()))); - memcpy(new_rule.get(), result.get(), result->data_size()); - temp_rules.emplace(std::move(new_rule)); } else { - // fact - 所有 premises 已匹配完毕 - if (facts.find(result) != facts.end() || temp_facts.find(result) != temp_facts.end()) { - continue; + // 还有 premises,检查是否可以继续递归 + if (depth + 1 < max_depth) { + // 可以继续递归匹配下一个 premise + match_recursive(next_rule.get(), depth + 1); + } else { + // 达到最大深度,将中间结果作为未完成的 rule 保存 + if (rules.find(next_rule) == rules.end() && temp_rules.find(next_rule) == temp_rules.end()) { + auto copied = std::unique_ptr(reinterpret_cast(operator new(next_rule->data_size()))); + memcpy(copied.get(), next_rule.get(), next_rule->data_size()); + temp_rules.emplace(std::move(copied)); + } + if (callback(next_rule.get())) { + break_all = true; + return; + } } - auto new_fact = std::unique_ptr(reinterpret_cast(operator new(result->data_size()))); - memcpy(new_fact.get(), result.get(), result->data_size()); - temp_facts.emplace(std::move(new_fact)); - } - if (callback(result.get())) { - break_all = true; - break; } } + }; + // 外层循环:遍历所有 rules + for (auto& [rule, rules_cycle] : rules) { if (break_all) { break; } + + // 只有当 rule 是新的或者 facts 有更新时才处理 + if (rules_cycle <= done_cycle && last_fact_cycle <= done_cycle) { + continue; + } + + // 从第一个 premise 开始递归匹配(depth = 0) + match_recursive(rule.get(), 0); } + // 更新 cycle if (!break_all) { done_cycle = current_cycle; } ++current_cycle; + // 注意:last_fact_cycle 只在有新 fact 加入时更新(在 add 函数中) + // 这里不更新 last_fact_cycle,因为 fact 库本身没有变化 + + // 将新发现的 rules 和 facts 加入库中 length_t count = temp_rules.size() + temp_facts.size(); for (auto it = temp_rules.begin(); it != temp_rules.end();) { auto node = temp_rules.extract(it++); diff --git a/tests/test_chain.cc b/tests/test_chain.cc index eb34c287..e75e0ca6 100644 --- a/tests/test_chain.cc +++ b/tests/test_chain.cc @@ -117,3 +117,21 @@ TEST_F(TestChain, execute_exceed) { auto count = search->execute([](ds::rule_t* rule) { return false; }); EXPECT_EQ(count, 0); } + +TEST_F(TestChain, set_max_depth) { + search->set_max_depth(2); + // rule 有 3 个 premises,超过 max_depth,应该被拒绝 + EXPECT_FALSE(search->add("p q r s")); + // rule 有 2 个 premises,等于 max_depth,应该被接受 + EXPECT_TRUE(search->add("p q r")); +} + +TEST_F(TestChain, set_max_depth_removes_existing_rules) { + search->add("p q r s"); // 3 个 premises + search->add("p q r"); // 2 个 premises + search->set_max_depth(2); + // 现在只有 2 个 premises 的 rule 应该存在 + auto count = search->execute([](ds::rule_t* rule) { return false; }); + // 应该有结果,因为 "p q r" 还存在 + EXPECT_GT(count, 0); +} diff --git a/tests/test_chain.mjs b/tests/test_chain.mjs index 7f748671..39be2c36 100644 --- a/tests/test_chain.mjs +++ b/tests/test_chain.mjs @@ -104,3 +104,22 @@ test("execute_exceed", () => { const count = chain.execute((rule) => false); expect(count).toBe(0); }); + +test("set_max_depth", () => { + chain.set_max_depth(2); + // rule has 3 premises, exceeds max_depth, should be rejected + expect(chain.add("p q r s")).toBe(false); + // rule has 2 premises, equals max_depth, should be accepted + expect(chain.add("p q r")).toBe(true); +}); + +test("set_max_depth_removes_existing_rules", () => { + const newChain = new Chain(100, 1000); + newChain.add("p q r s"); // 3 premises + newChain.add("p q r"); // 2 premises + newChain.set_max_depth(2); + // Now only the rule with 2 premises should exist + const count = newChain.execute((rule) => false); + // Should have results because "p q r" still exists + expect(count).toBeGreaterThan(0); +}); diff --git a/tests/test_chain.py b/tests/test_chain.py index 77e5b259..63e6a13e 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -110,3 +110,22 @@ def test_execute_exceed(chain: apyds.Chain) -> None: 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_set_max_depth(chain: apyds.Chain) -> None: + chain.set_max_depth(2) + # rule 有 3 个 premises,超过 max_depth,应该被拒绝 + assert not chain.add("p q r s") + # rule 有 2 个 premises,等于 max_depth,应该被接受 + assert chain.add("p q r") + + +def test_set_max_depth_removes_existing_rules() -> None: + chain = apyds.Chain(100, 1000) + chain.add("p q r s") # 3 个 premises + chain.add("p q r") # 2 个 premises + chain.set_max_depth(2) + # 现在只有 2 个 premises 的 rule 应该存在 + count = chain.execute(lambda rule: False) + # 应该有结果,因为 "p q r" 还存在 + assert count > 0 From cc936c85f11b8afa3169ae9058de59a62646182b Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 12:38:03 +0800 Subject: [PATCH 05/11] fix: remove incorrect .get() call on raw pointer in chain.cc --- src/chain.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain.cc b/src/chain.cc index fdc0035f..a818ff64 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -138,7 +138,7 @@ namespace ds { // 复制中间结果 auto next_rule = std::unique_ptr(reinterpret_cast(operator new(match_buffer->data_size()))); - memcpy(next_rule.get(), match_buffer.get(), match_buffer->data_size()); + memcpy(next_rule.get(), match_buffer, match_buffer->data_size()); // 如果 next_rule 没有 premises 了,说明匹配完成,直接处理结果 if (next_rule->premises_count() == 0) { From 0ea146a854d0e5114e1d3ed251751ece4e096b9e Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 12:40:52 +0800 Subject: [PATCH 06/11] fix: set default max_depth to 8 in chain_t --- src/chain.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/chain.cc b/src/chain.cc index a818ff64..9ec0e1f2 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -28,7 +28,6 @@ namespace ds { void chain_t::set_limit_size(length_t _limit_size) { limit_size = _limit_size; - max_depth = 1; done_cycle = 0; last_fact_cycle = 0; } @@ -66,7 +65,7 @@ namespace ds { done_cycle = 0; current_cycle = 0; last_fact_cycle = 0; - max_depth = 1; + max_depth = 8; rules.clear(); facts.clear(); buffers.clear(); From ce192c493aea95d0a83b223ddaeac6c8a11af899 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 12:45:30 +0800 Subject: [PATCH 07/11] fix: add facts in set_max_depth_removes_existing_rules tests --- tests/test_chain.cc | 5 ++++- tests/test_chain.mjs | 5 ++++- tests/test_chain.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_chain.cc b/tests/test_chain.cc index e75e0ca6..b5e72039 100644 --- a/tests/test_chain.cc +++ b/tests/test_chain.cc @@ -131,7 +131,10 @@ TEST_F(TestChain, set_max_depth_removes_existing_rules) { search->add("p q r"); // 2 个 premises search->set_max_depth(2); // 现在只有 2 个 premises 的 rule 应该存在 + // 添加 facts 来测试 rule 是否还在 + search->add("p"); + search->add("q"); auto count = search->execute([](ds::rule_t* rule) { return false; }); - // 应该有结果,因为 "p q r" 还存在 + // 应该有结果,因为 "p q r" 还存在(3 个 premises 的 rule 被移除了) EXPECT_GT(count, 0); } diff --git a/tests/test_chain.mjs b/tests/test_chain.mjs index 39be2c36..81dab257 100644 --- a/tests/test_chain.mjs +++ b/tests/test_chain.mjs @@ -119,7 +119,10 @@ test("set_max_depth_removes_existing_rules", () => { newChain.add("p q r"); // 2 premises newChain.set_max_depth(2); // Now only the rule with 2 premises should exist + // Add facts to test if the rule still exists + newChain.add("p"); + newChain.add("q"); const count = newChain.execute((rule) => false); - // Should have results because "p q r" still exists + // Should have results because "p q r" still exists (3 premises rule was removed) expect(count).toBeGreaterThan(0); }); diff --git a/tests/test_chain.py b/tests/test_chain.py index 63e6a13e..b6dc06d4 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -126,6 +126,9 @@ def test_set_max_depth_removes_existing_rules() -> None: chain.add("p q r") # 2 个 premises chain.set_max_depth(2) # 现在只有 2 个 premises 的 rule 应该存在 + # 添加 facts 来测试 rule 是否还在 + chain.add("p") + chain.add("q") count = chain.execute(lambda rule: False) - # 应该有结果,因为 "p q r" 还存在 + # 应该有结果,因为 "p q r" 还存在(3 个 premises 的 rule 被移除了) assert count > 0 From 301354fa68b82b7a7b0067504a1867f54b3bce1f Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 13:53:11 +0800 Subject: [PATCH 08/11] Update the implementation in chain.cc. --- apyds/chain_t.py | 14 +---- apyds/ds.cc | 1 - atsds/ds.cc | 1 - atsds/index.mts | 13 +--- include/ds/chain.hh | 60 ++++++++----------- src/chain.cc | 139 ++++++++----------------------------------- tests/test_chain.cc | 130 +++++++++++++++++++++------------------- tests/test_chain.mjs | 62 ++++++++++--------- tests/test_chain.py | 62 ++++++++++--------- 9 files changed, 190 insertions(+), 292 deletions(-) diff --git a/apyds/chain_t.py b/apyds/chain_t.py index b88b085b..33d99447 100644 --- a/apyds/chain_t.py +++ b/apyds/chain_t.py @@ -16,7 +16,7 @@ class Chain: Example: >>> chain = Chain() - >>> chain.add("p q r") # p, q |- r (two premises) + >>> chain.add("p q r") >>> chain.add("p") >>> chain.add("q") >>> def callback(rule): @@ -52,18 +52,6 @@ def set_buffer_size(self, buffer_size: int) -> None: """ self._chain.set_buffer_size(buffer_size) - def set_max_depth(self, max_depth: int) -> None: - """Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). - - Args: - max_depth: The maximum recursion depth. - - Note: - Rules with premises count exceeding this value will be rejected when added. - After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. - """ - self._chain.set_max_depth(max_depth) - def reset(self) -> None: """Reset the chain engine, clearing all rules and facts.""" self._chain.reset() diff --git a/apyds/ds.cc b/apyds/ds.cc index cb980ead..42b86f99 100644 --- a/apyds/ds.cc +++ b/apyds/ds.cc @@ -174,7 +174,6 @@ PYBIND11_MODULE(_ds, m, py::mod_gil_not_used()) { 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("set_max_depth", &ds::chain_t::set_max_depth); 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/atsds/ds.cc b/atsds/ds.cc index 7f76255b..86a91b8a 100644 --- a/atsds/ds.cc +++ b/atsds/ds.cc @@ -200,7 +200,6 @@ EMSCRIPTEN_BINDINGS(ds) { 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("set_max_depth", &ds::chain_t::set_max_depth); chain_t.function("reset", &ds::chain_t::reset); // 因为 embind 的限制,这里无法使用 string_view 和 function。 chain_t.function("add", &chain_add, em::allow_raw_pointers()); diff --git a/atsds/index.mts b/atsds/index.mts index 852c8e54..a40a36c3 100644 --- a/atsds/index.mts +++ b/atsds/index.mts @@ -629,7 +629,7 @@ export class Search { * @example * ```typescript * const chain = new Chain(); - * chain.add("p q r"); // p, q |- r (two premises) + * chain.add("p q r"); * chain.add("p"); * chain.add("q"); * chain.execute((rule) => { @@ -669,17 +669,6 @@ export class Chain { this._chain.set_buffer_size(buffer_size); } - /** - * Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). - * - * @param max_depth - The maximum recursion depth. - * @note Rules with premises count exceeding this value will be rejected when added. - * @note After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. - */ - set_max_depth(max_depth: number): void { - this._chain.set_max_depth(max_depth); - } - /** * Reset the chain engine, clearing all rules and facts. */ diff --git a/include/ds/chain.hh b/include/ds/chain.hh index f5ee4868..c2625fcb 100644 --- a/include/ds/chain.hh +++ b/include/ds/chain.hh @@ -5,7 +5,6 @@ #include #include #include -#include #include @@ -13,74 +12,63 @@ namespace ds { /// @brief 用于进行链式推理搜索的类。 /// @note 与 search_t 不同,chain_t 在单轮中会将 rule 的所有 premises 全部匹配完成。 class chain_t { - /// @brief 用于比较 rule_t 的智能指针大小的类型,用于将其存储在 map 中。 - /// @note 该类型比较的是 rule_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 对象的大小,而不是指针地址。 + /// @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 的最大长度。 + /// @brief 每个有效rule_t的最大长度。 length_t limit_size; /// @brief 在搜索过程中使用的缓冲区最大长度。 length_t buffer_size; - /// @brief 链式匹配的最大递归深度,即单个 rule 允许的最大 premise 数目。 - /// @note premises 数目超过此值的 rule 将无法被完整匹配,只能匹配前 max_depth 个 premises。 - length_t max_depth; - /// @brief 已经完成的 cycle,表示在此之前的所有 rules 都已经被处理过。 - /// @note 如果高于 last_fact_cycle,则说明所有的 facts 都已经被处理过。 + /// @brief 已经完成的cycle,表示在此之前的所有rules都已经被处理过。 + /// @note 如果高于last_fact_cycle,则说明所有的facts都已经被处理过。 length_t done_cycle; - /// @brief rules 库中最大的 cycle,此变量在更新 rules 前设置。 - /// @note 也会在添加 fact 时设置给 fact,但 chain 模式下不会利用 fact 的 cycle 进行判断。 + /// @brief rules库中最大的cycle,此变量在更新rules前设置。 + /// @note 也会在添加fact时设置给fact,但chain不会利用fact的cycle进行判断。 length_t current_cycle; - /// @brief facts 库的最后更新时间,用于避免重复计算。 + /// @brief facts库的最后更新时间,用于避免重复计算。 length_t last_fact_cycle; - /// @brief 用于存储规则的 map,键为 rule_t 的智能指针,值为其对应的 cycle。 + /// @brief 用于存储规则的map,键为rule_t的智能指针,值为其对应的cycle。 std::map, length_t, less_t> rules; - /// @brief 用于存储事实的 map,键为 rule_t 的智能指针,值为其对应的 cycle。 + /// @brief 用于存储事实的map,键为rule_t的智能指针,值为其对应的cycle。 std::map, length_t, less_t> facts; - /// @brief 用于存储链式匹配过程中使用的缓冲区(按深度层级存储)。 - /// @note buffers[i] 用于存储匹配第 i 个 premise 时的中间结果。 - std::vector> buffers; + /// @brief 用于存储搜索过程中使用的缓冲区。 + std::unique_ptr buffer; public: /// @brief 构造函数,用于初始化搜索对象 - /// @param _limit_size 每个有效 rule_t 的最大长度。 + /// @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 的最大长度。 + /// @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 设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 - /// @param _max_depth 最大递归深度。 - /// @note 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 - /// @note 修改此值后,会检查现有的所有 rules, premises 数目超过新 max_depth 的 rules 会被移除。 - void set_max_depth(length_t _max_depth); - /// @brief 重置搜索过程中的所有状态。 void reset(); - /// @brief 向本搜索对象添加一个 rule 或 fact。 - /// @param text 描述 rule 或 fact 的文本。 - /// @return 如果添加成功则返回 true,否则返回 false。 - /// @note 如果添加的是 rule 且 premises 数目超过 max_depth,则失败。 + /// @brief 向本搜索对象添加一个rule或fact。 + /// @param text 描述rule或fact的文本。 + /// @return 如果添加成功则返回true,否则返回false。 bool add(std::string_view text); /// @brief 执行一轮搜索操作,遍历所有规则和事实,并对每个匹配的规则执行回调函数。 /// @param callback 回调函数,每个新中找到的结果都会调用此函数。 /// @return 搜索到新的结果的数量。 - /// @note 如果回调函数返回 false,则继续搜索;如果回调函数返回 true,则停止搜索。 + /// @note 如果回调函数返回false,则继续搜索;如果回调函数返回true,则停止搜索。 length_t execute(const std::function& callback); }; } // namespace ds diff --git a/src/chain.cc b/src/chain.cc index 9ec0e1f2..2a463429 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -29,50 +29,20 @@ namespace ds { void chain_t::set_limit_size(length_t _limit_size) { limit_size = _limit_size; done_cycle = 0; - last_fact_cycle = 0; } void chain_t::set_buffer_size(length_t _buffer_size) { buffer_size = _buffer_size; - buffers.clear(); - buffers.resize(max_depth); - for (auto& buf : buffers) { - buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); - } + buffer = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); done_cycle = 0; - last_fact_cycle = 0; - } - - void chain_t::set_max_depth(length_t _max_depth) { - max_depth = _max_depth; - // 移除 premises 数目超过新 max_depth 的 rules - for (auto it = rules.begin(); it != rules.end();) { - if (it->first->premises_count() > max_depth) { - it = rules.erase(it); - } else { - ++it; - } - } - // 重新分配 buffers - buffers.clear(); - buffers.resize(max_depth); - for (auto& buf : buffers) { - buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); - } } void chain_t::reset() { done_cycle = 0; current_cycle = 0; last_fact_cycle = 0; - max_depth = 8; rules.clear(); facts.clear(); - buffers.clear(); - buffers.resize(max_depth); - for (auto& buf : buffers) { - buf = std::unique_ptr(reinterpret_cast(operator new(buffer_size))); - } } bool chain_t::add(std::string_view text) { @@ -82,13 +52,8 @@ namespace ds { ++current_cycle; } if (candidate->premises_count() != 0) { - // rule: 检查 premises 数目是否超过 max_depth - if (candidate->premises_count() > max_depth) { - return false; - } rules.emplace(std::move(candidate), current_cycle); } else { - // fact facts.emplace(std::move(candidate), current_cycle); last_fact_cycle = current_cycle; } @@ -99,108 +64,54 @@ namespace ds { } length_t chain_t::execute(const std::function& callback) { - std::set, less_t> temp_rules; std::set, less_t> temp_facts; bool break_all = false; - // 递归匹配函数 - // current_rule: 当前要匹配的 rule(随着匹配进行,已匹配的 premise 会被消耗) - // depth: 当前递归深度(即将匹配第 depth 个 premise,从 0 开始) - // match_recursive 会遍历所有 facts 尝试匹配 current_rule 的第一个 premise - // 匹配成功后递归处理剩余 premises - std::function match_recursive; - match_recursive = [&](rule_t* current_rule, length_t depth) -> void { - // 检查是否所有 premises 已匹配完成 - if (current_rule->premises_count() == 0) { - // 所有 premises 已匹配完成,current_rule 是一个 fact - // 这种情况应该在上一层匹配成功后处理,而不是在这里 - return; - } - - // 使用 buffers[depth] 作为当前深度的匹配缓冲区 - rule_t* match_buffer = buffers[depth].get(); - - // 遍历所有 facts 匹配当前 rule 的第一个 premise - for (auto& [fact, facts_cycle] : facts) { - if (break_all) { + 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; } - - match_buffer->match(current_rule, fact.get(), reinterpret_cast(match_buffer) + buffer_size); - if (!match_buffer->valid()) { - continue; + 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; } - if (match_buffer->data_size() > limit_size) { - continue; + temp_facts.emplace(std::move(new_fact)); + if (callback(rule)) { + break_all = true; } + return; + } - // 复制中间结果 - auto next_rule = std::unique_ptr(reinterpret_cast(operator new(match_buffer->data_size()))); - memcpy(next_rule.get(), match_buffer, match_buffer->data_size()); - - // 如果 next_rule 没有 premises 了,说明匹配完成,直接处理结果 - if (next_rule->premises_count() == 0) { - // 是 fact - if (facts.find(next_rule) == facts.end() && temp_facts.find(next_rule) == temp_facts.end()) { - auto copied = std::unique_ptr(reinterpret_cast(operator new(next_rule->data_size()))); - memcpy(copied.get(), next_rule.get(), next_rule->data_size()); - temp_facts.emplace(std::move(copied)); - } - if (callback(next_rule.get())) { - break_all = true; - return; - } - } else { - // 还有 premises,检查是否可以继续递归 - if (depth + 1 < max_depth) { - // 可以继续递归匹配下一个 premise - match_recursive(next_rule.get(), depth + 1); - } else { - // 达到最大深度,将中间结果作为未完成的 rule 保存 - if (rules.find(next_rule) == rules.end() && temp_rules.find(next_rule) == temp_rules.end()) { - auto copied = std::unique_ptr(reinterpret_cast(operator new(next_rule->data_size()))); - memcpy(copied.get(), next_rule.get(), next_rule->data_size()); - temp_rules.emplace(std::move(copied)); - } - if (callback(next_rule.get())) { - 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); } }; - // 外层循环:遍历所有 rules for (auto& [rule, rules_cycle] : rules) { - if (break_all) { - break; - } - - // 只有当 rule 是新的或者 facts 有更新时才处理 if (rules_cycle <= done_cycle && last_fact_cycle <= done_cycle) { continue; } - // 从第一个 premise 开始递归匹配(depth = 0) - match_recursive(rule.get(), 0); + chain_recursive(rule.get(), buffer.get(), reinterpret_cast(buffer.get()) + buffer_size); + + if (break_all) { + break; + } } - // 更新 cycle if (!break_all) { done_cycle = current_cycle; } ++current_cycle; - // 注意:last_fact_cycle 只在有新 fact 加入时更新(在 add 函数中) - // 这里不更新 last_fact_cycle,因为 fact 库本身没有变化 - - // 将新发现的 rules 和 facts 加入库中 - length_t count = temp_rules.size() + temp_facts.size(); - for (auto it = temp_rules.begin(); it != temp_rules.end();) { - auto node = temp_rules.extract(it++); - rules.emplace(std::move(node.value()), 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); diff --git a/tests/test_chain.cc b/tests/test_chain.cc index b5e72039..a4d9351d 100644 --- a/tests/test_chain.cc +++ b/tests/test_chain.cc @@ -10,37 +10,37 @@ class TestChain : public ::testing::Test { TestChain() { } ~TestChain() override { } void SetUp() override { - search = new ds::chain_t(limit_size, buffer_size); + chain = new ds::chain_t(limit_size, buffer_size); } void TearDown() override { - delete search; + delete chain; } - ds::chain_t* search; + ds::chain_t* chain; }; TEST_F(TestChain, reset_parameters) { - search->set_limit_size(50); - search->set_buffer_size(500); - search->reset(); + chain->set_limit_size(50); + chain->set_buffer_size(500); + chain->reset(); } TEST_F(TestChain, add_rule_and_fact) { - EXPECT_TRUE(search->add("test rule")); - EXPECT_TRUE(search->add("fact")); + EXPECT_TRUE(chain->add("test rule")); + EXPECT_TRUE(chain->add("fact")); } TEST_F(TestChain, add_fail) { - search->set_limit_size(10); - EXPECT_FALSE(search->add("a-long-facts-that-exceeds-limit")); + chain->set_limit_size(10); + EXPECT_FALSE(chain->add("a-long-facts-that-exceeds-limit")); } TEST_F(TestChain, execute_single_premise) { - search->add("p q"); - search->add("p"); + chain->add("p q"); + chain->add("p"); auto target = ds::text_to_rule("q", limit_size); bool success = false; - auto count = search->execute([&success, &target](ds::rule_t* rule) { + auto count = chain->execute([&success, &target](ds::rule_t* rule) { if (memcmp(rule, target.get(), rule->data_size()) == 0) { success = true; return true; @@ -52,17 +52,14 @@ TEST_F(TestChain, execute_single_premise) { } TEST_F(TestChain, execute_multiple_premises_chain) { - // p q r 表示:p, q |- r (两个 premises) - // 在单轮中应该同时匹配 p 和 q,直接得到 r - search->add("p q r"); - search->add("p"); - search->add("q"); + 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 = search->execute([&success, &target](ds::rule_t* rule) { + 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; }); @@ -71,29 +68,22 @@ TEST_F(TestChain, execute_multiple_premises_chain) { } TEST_F(TestChain, execute_multiple_premises_partial) { - // p q r 表示:p, q |- r (两个 premises) - // 只有 p,没有 q,在 chain_t 中不会产生部分结果 - // 因为 chain_t 的设计是在单轮内匹配所有 premises - search->add("p q r"); - search->add("p"); - auto count = search->execute([](ds::rule_t* rule) { return false; }); - // 没有匹配完所有 premises,不会产生任何结果 + 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) { - // p q r s 表示:p, q, r |- s (三个 premises) - // 在单轮中应该同时匹配 p, q, r,直接得到 s - search->add("p q r s"); - search->add("p"); - search->add("q"); - search->add("r"); + 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 = search->execute([&success, &target](ds::rule_t* rule) { + 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; }); @@ -102,39 +92,57 @@ TEST_F(TestChain, execute_three_premises) { } TEST_F(TestChain, execute_duplicated_fact) { - search->add("p r"); - search->add("q r"); - search->add("p"); - search->add("q"); - auto count = search->execute([](ds::rule_t* rule) { return false; }); + 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) { - search->set_limit_size(100); - EXPECT_TRUE(search->add("(2 `x) (`x `x`)")); - EXPECT_TRUE(search->add("(2 a-very-long-fact-that-exceeds-half-of-the-limit-size)")); - auto count = search->execute([](ds::rule_t* rule) { return false; }); + 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, set_max_depth) { - search->set_max_depth(2); - // rule 有 3 个 premises,超过 max_depth,应该被拒绝 - EXPECT_FALSE(search->add("p q r s")); - // rule 有 2 个 premises,等于 max_depth,应该被接受 - EXPECT_TRUE(search->add("p q r")); +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, set_max_depth_removes_existing_rules) { - search->add("p q r s"); // 3 个 premises - search->add("p q r"); // 2 个 premises - search->set_max_depth(2); - // 现在只有 2 个 premises 的 rule 应该存在 - // 添加 facts 来测试 rule 是否还在 - search->add("p"); - search->add("q"); - auto count = search->execute([](ds::rule_t* rule) { return false; }); - // 应该有结果,因为 "p q r" 还存在(3 个 premises 的 rule 被移除了) - EXPECT_GT(count, 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 index 81dab257..3171cdab 100644 --- a/tests/test_chain.mjs +++ b/tests/test_chain.mjs @@ -39,8 +39,6 @@ test("execute_single_premise", () => { }); test("execute_multiple_premises_chain", () => { - // p q r means: p, q |- r (two premises) - // In chain_t, both premises are matched in a single cycle chain.add("p q r"); chain.add("p"); chain.add("q"); @@ -49,7 +47,6 @@ test("execute_multiple_premises_chain", () => { const count = chain.execute((rule) => { if (rule.key() === target.key()) { success = true; - return true; } return false; }); @@ -58,19 +55,13 @@ test("execute_multiple_premises_chain", () => { }); test("execute_multiple_premises_partial", () => { - // p q r means: p, q |- r (two premises) - // Only p, no q - chain_t won't produce partial results - // because it's designed to match all premises in a single cycle chain.add("p q r"); chain.add("p"); const count = chain.execute((rule) => false); - // No result because not all premises are matched expect(count).toBe(0); }); test("execute_three_premises", () => { - // p q r s means: p, q, r |- s (three premises) - // In chain_t, all three premises are matched in a single cycle chain.add("p q r s"); chain.add("p"); chain.add("q"); @@ -80,7 +71,6 @@ test("execute_three_premises", () => { const count = chain.execute((rule) => { if (rule.key() === target.key()) { success = true; - return true; } return false; }); @@ -105,24 +95,42 @@ test("execute_exceed", () => { expect(count).toBe(0); }); -test("set_max_depth", () => { - chain.set_max_depth(2); - // rule has 3 premises, exceeds max_depth, should be rejected - expect(chain.add("p q r s")).toBe(false); - // rule has 2 premises, equals max_depth, should be accepted - expect(chain.add("p q r")).toBe(true); +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("set_max_depth_removes_existing_rules", () => { +test("execute_exceed_by_too_many_premises", () => { const newChain = new Chain(100, 1000); - newChain.add("p q r s"); // 3 premises - newChain.add("p q r"); // 2 premises - newChain.set_max_depth(2); - // Now only the rule with 2 premises should exist - // Add facts to test if the rule still exists - newChain.add("p"); - newChain.add("q"); - const count = newChain.execute((rule) => false); - // Should have results because "p q r" still exists (3 premises rule was removed) - expect(count).toBeGreaterThan(0); + 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 index b6dc06d4..7c83edbf 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -42,8 +42,6 @@ def callback(rule: apyds.Rule) -> bool: def test_execute_multiple_premises_chain(chain: apyds.Chain) -> None: - # p q r means: p, q |- r (two premises) - # In chain_t, both premises are matched in a single cycle chain.add("p q r") chain.add("p") chain.add("q") @@ -54,7 +52,6 @@ def callback(rule: apyds.Rule) -> bool: nonlocal success if rule == target: success = True - return True return False count = chain.execute(callback) @@ -63,19 +60,13 @@ def callback(rule: apyds.Rule) -> bool: def test_execute_multiple_premises_partial(chain: apyds.Chain) -> None: - # p q r means: p, q |- r (two premises) - # Only p, no q - chain_t won't produce partial results - # because it's designed to match all premises in a single cycle chain.add("p q r") chain.add("p") count = chain.execute(lambda rule: False) - # No result because not all premises are matched assert count == 0 def test_execute_three_premises(chain: apyds.Chain) -> None: - # p q r s means: p, q, r |- s (three premises) - # In chain_t, all three premises are matched in a single cycle chain.add("p q r s") chain.add("p") chain.add("q") @@ -87,7 +78,6 @@ def callback(rule: apyds.Rule) -> bool: nonlocal success if rule == target: success = True - return True return False count = chain.execute(callback) @@ -112,23 +102,41 @@ def test_execute_exceed(chain: apyds.Chain) -> None: assert count == 0 -def test_set_max_depth(chain: apyds.Chain) -> None: - chain.set_max_depth(2) - # rule 有 3 个 premises,超过 max_depth,应该被拒绝 - assert not chain.add("p q r s") - # rule 有 2 个 premises,等于 max_depth,应该被接受 - assert chain.add("p q r") +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_set_max_depth_removes_existing_rules() -> None: +def test_execute_exceed_by_too_many_premises() -> None: chain = apyds.Chain(100, 1000) - chain.add("p q r s") # 3 个 premises - chain.add("p q r") # 2 个 premises - chain.set_max_depth(2) - # 现在只有 2 个 premises 的 rule 应该存在 - # 添加 facts 来测试 rule 是否还在 - chain.add("p") - chain.add("q") - count = chain.execute(lambda rule: False) - # 应该有结果,因为 "p q r" 还存在(3 个 premises 的 rule 被移除了) - assert count > 0 + 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 From 7289c3be3b9be73089f8726b6ff2951cef6f2718 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 15:22:26 +0800 Subject: [PATCH 09/11] Recovery all docs about chain engine. --- docs/en/api/cpp.md | 94 +------------------------ docs/en/api/python.md | 104 ---------------------------- docs/en/api/typescript.md | 141 +++++--------------------------------- docs/zh/api/cpp.md | 94 +------------------------ docs/zh/api/python.md | 104 ---------------------------- docs/zh/api/typescript.md | 139 +++++-------------------------------- 6 files changed, 35 insertions(+), 641 deletions(-) diff --git a/docs/en/api/cpp.md b/docs/en/api/cpp.md index c79b0f48..8d59f996 100644 --- a/docs/en/api/cpp.md +++ b/docs/en/api/cpp.md @@ -9,7 +9,6 @@ All classes and functions are in the `ds` namespace. ```cpp #include // All basic types #include // Search engine -#include // Chain engine #include // Helper functions ``` @@ -488,85 +487,6 @@ length_t execute(const std::function& callback); --- -## chain_t - -Chain engine class. Defined in ``. - -Manages a knowledge base and performs logical inference. Unlike `search_t`, `chain_t` 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); -``` - -#### set_max_depth() - -Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). - -```cpp -void set_max_depth(length_t max_depth); -``` - -**Notes:** -- Rules with premises count exceeding this value will be rejected when added. -- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. - -#### 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. - -```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. - ---- - ## Utility Functions Helper functions in ``. @@ -696,19 +616,7 @@ int main() { if (found) { std::cout << "Target found!" << std::endl; } - - // Chain engine (matches all premises in a single cycle) - ds::chain_t chain(1000, 10000); - chain.add("p q r"); // p, q |- r (two premises) - chain.add("p"); - chain.add("q"); - - std::cout << "\nRunning chain inference:" << std::endl; - chain.execute([&](ds::rule_t* candidate) { - std::cout << " Derived: " << ds::rule_to_text(candidate, buffer_size).get(); - return false; - }); - + return 0; } ``` diff --git a/docs/en/api/python.md b/docs/en/api/python.md index 35fc1e45..c63ed7d0 100644 --- a/docs/en/api/python.md +++ b/docs/en/api/python.md @@ -13,7 +13,6 @@ from apyds import ( Term, Rule, Search, - Chain, ) ``` @@ -480,100 +479,6 @@ 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 -``` - -#### set_max_depth() - -Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). - -```python -def set_max_depth(self, max_depth: int) -> None -``` - -**Notes:** -- Rules with premises count exceeding this value will be rejected when added. -- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. - -#### 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. - -**Example:** - -```python -chain = Chain() -chain.add("p q r") # p, q |- r (two premises) -chain.add("p") -chain.add("q") - -def callback(candidate): - print(candidate) - return False # Continue searching - -chain.execute(callback) -``` - ---- - ## Complete Example Here's a complete example demonstrating most of the API: @@ -630,13 +535,4 @@ for i in range(3): with apyds.scoped_buffer_size(4096): big_term = apyds.Term("(a b c d e f g h i j)") print(f"\nBig term: {big_term}") - -# Chain engine (matches all premises in a single cycle) -chain = apyds.Chain(1000, 10000) -chain.add("p q r") # p, q |- r (two premises) -chain.add("p") -chain.add("q") - -print("\nRunning chain inference:") -chain.execute(lambda r: print(f" Derived: {r}") or False) ``` diff --git a/docs/en/api/typescript.md b/docs/en/api/typescript.md index 263605f7..ce2a1c55 100644 --- a/docs/en/api/typescript.md +++ b/docs/en/api/typescript.md @@ -3,16 +3,15 @@ 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, - Chain + String_, + Variable, + Item, + List, + Term, + Rule, + Search } from "atsds"; ``` @@ -476,114 +475,20 @@ 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 -``` - -#### set_max_depth() - -Set the maximum recursion depth (i.e., maximum number of premises allowed for a single rule). - -```typescript -set_max_depth(max_depth: number): void -``` - -**Notes:** -- Rules with premises count exceeding this value will be rejected when added. -- After modifying this value, existing rules with premises count exceeding the new max_depth will be removed. - -#### 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. - -**Example:** - -```typescript -const chain = new Chain(); -chain.add("p q r"); // p, q |- r (two premises) -chain.add("p"); -chain.add("q"); - -chain.execute((candidate) => { - console.log(candidate.toString()); - return false; // Continue searching -}); -``` - ---- - ## Complete Example Here's a complete example demonstrating most of the TypeScript API: ```typescript -import { - buffer_size, - String_, - Variable, - Item, - List, - Term, - Rule, - Search, - Chain +import { + buffer_size, + String_, + Variable, + Item, + List, + Term, + Rule, + Search } from "atsds"; // Configure buffer size @@ -642,16 +547,4 @@ for (let i = 0; i < 3; i++) { const rule1 = new Rule("(a b c)"); const rule2 = rule1.copy(); console.log(`\nRule comparison: ${rule1.key() === rule2.key()}`); // true - -// Chain engine (matches all premises in a single cycle) -const chain = new Chain(1000, 10000); -chain.add("p q r"); // p, q |- r (two premises) -chain.add("p"); -chain.add("q"); - -console.log("\nRunning chain inference:"); -chain.execute((r) => { - console.log(` Derived: ${r.toString()}`); - return false; -}); ``` diff --git a/docs/zh/api/cpp.md b/docs/zh/api/cpp.md index 9ef7aa8e..81589fab 100644 --- a/docs/zh/api/cpp.md +++ b/docs/zh/api/cpp.md @@ -9,7 +9,6 @@ ```cpp #include // 所有基本类型 #include // 搜索引擎 -#include // 链式引擎 #include // 辅助函数 ``` @@ -488,85 +487,6 @@ length_t execute(const std::function& callback); --- -## chain_t - -链式引擎类。定义在 `` 中。 - -管理知识库并执行逻辑推理。与 `search_t` 不同,`chain_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); -``` - -#### set_max_depth() - -设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 - -```cpp -void set_max_depth(length_t max_depth); -``` - -**注意:** -- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 -- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 - -#### 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 停止。 - -**返回值:** 生成的新推理数量。 - ---- - ## 辅助函数 `` 中的辅助函数。 @@ -696,19 +616,7 @@ int main() { if (found) { std::cout << "Target found!" << std::endl; } - - // Chain engine (matches all premises in a single cycle) - ds::chain_t chain(1000, 10000); - chain.add("p q r"); // p, q |- r (two premises) - chain.add("p"); - chain.add("q"); - - std::cout << "\nRunning chain inference:" << std::endl; - chain.execute([&](ds::rule_t* candidate) { - std::cout << " Derived: " << ds::rule_to_text(candidate, buffer_size).get(); - return false; - }); - + return 0; } ``` diff --git a/docs/zh/api/python.md b/docs/zh/api/python.md index 4e262e4d..6ef902a5 100644 --- a/docs/zh/api/python.md +++ b/docs/zh/api/python.md @@ -13,7 +13,6 @@ from apyds import ( Term, Rule, Search, - Chain, ) ``` @@ -488,100 +487,6 @@ search.execute(callback) --- -## Chain - -Chain engine for the deductive system. Similar to Search, but matches all premises of a rule in a single cycle. - -### 构造函数 - -```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 -``` - -#### set_max_depth() - -设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 - -```python -def set_max_depth(self, max_depth: int) -> None -``` - -**注意:** -- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 -- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 - -#### 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 数量。 - -**示例:** - -```python -chain = Chain() -chain.add("p q r") # p, q |- r (two premises) -chain.add("p") -chain.add("q") - -def callback(candidate): - print(candidate) - return False # Continue searching - -chain.execute(callback) -``` - ---- - ## 完整示例 @@ -639,13 +544,4 @@ for i in range(3): with apyds.scoped_buffer_size(4096): big_term = apyds.Term("(a b c d e f g h i j)") print(f"\nBig term: {big_term}") - -# Chain engine (matches all premises in a single cycle) -chain = apyds.Chain(1000, 10000) -chain.add("p q r") # p, q |- r (two premises) -chain.add("p") -chain.add("q") - -print("\nRunning chain inference:") -chain.execute(lambda r: print(f" Derived: {r}") or False) ``` \ No newline at end of file diff --git a/docs/zh/api/typescript.md b/docs/zh/api/typescript.md index de2cde46..40965bbc 100644 --- a/docs/zh/api/typescript.md +++ b/docs/zh/api/typescript.md @@ -3,16 +3,15 @@ 本页记录了 `atsds` 包的 TypeScript API。文档由 TypeScript 源代码生成。 ```typescript -import { +import { buffer_size, String_, - Variable, - Item, - List, - Term, - Rule, - Search, - Chain + Variable, + Item, + List, + Term, + Rule, + Search } from "atsds"; ``` @@ -477,114 +476,20 @@ search.execute((candidate) => { --- -## Chain - -Chain engine for the deductive system. Similar to Search, but matches all premises of a rule in a single cycle. - -### 构造函数 - -```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 -``` - -#### set_max_depth() - -设置链式匹配的最大递归深度(即单个 rule 允许的最大 premise 数目)。 - -```typescript -set_max_depth(max_depth: number): void -``` - -**注意:** -- 当 premises 数目超过此值的 rule 被添加时,会被拒绝添加。 -- 修改此值后,会检查现有的所有 rules,premises 数目超过新 max_depth 的 rules 会被移除。 - -#### 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 数量。 - -**示例:** - -```typescript -const chain = new Chain(); -chain.add("p q r"); // p, q |- r (two premises) -chain.add("p"); -chain.add("q"); - -chain.execute((candidate) => { - console.log(candidate.toString()); - return false; // Continue searching -}); -``` - ---- - ## 完整示例 这是一个演示大多数 TypeScript API 的完整示例: ```typescript -import { - buffer_size, - String_, - Variable, - Item, - List, - Term, - Rule, - Search, - Chain +import { + buffer_size, + String_, + Variable, + Item, + List, + Term, + Rule, + Search } from "atsds"; // Configure buffer size @@ -645,16 +550,4 @@ for (let i = 0; i < 3; i++) { const rule1 = new Rule("(a b c)"); const rule2 = rule1.copy(); console.log(`\nRule comparison: ${rule1.key() === rule2.key()}`); // true - -// Chain engine (matches all premises in a single cycle) -const chain = new Chain(1000, 10000); -chain.add("p q r"); // p, q |- r (two premises) -chain.add("p"); -chain.add("q"); - -console.log("\nRunning chain inference:"); -chain.execute((r) => { - console.log(` Derived: ${r.toString()}`); - return false; -}); ``` From 246f12faae8a5ac0f927e41c1cd8300bd0450782 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 15:27:20 +0800 Subject: [PATCH 10/11] Rename search.md to engine.md. --- .vitepress/config.mts | 4 ++-- docs/en/concepts/{search.md => engine.md} | 0 docs/zh/concepts/{search.md => engine.md} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/en/concepts/{search.md => engine.md} (100%) rename docs/zh/concepts/{search.md => engine.md} (100%) 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/docs/en/concepts/search.md b/docs/en/concepts/engine.md similarity index 100% rename from docs/en/concepts/search.md rename to docs/en/concepts/engine.md diff --git a/docs/zh/concepts/search.md b/docs/zh/concepts/engine.md similarity index 100% rename from docs/zh/concepts/search.md rename to docs/zh/concepts/engine.md From 1289bb0c2c08e21a37edfb644f2ffa8ba0266b49 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Thu, 12 Mar 2026 15:37:31 +0800 Subject: [PATCH 11/11] Update the documents. --- docs/en/api/cpp.md | 70 +++++++++++++++++++++++++ docs/en/api/python.md | 87 +++++++++++++++++++++++++++++++ docs/en/api/typescript.md | 102 ++++++++++++++++++++++++++++++++++--- docs/en/concepts/engine.md | 46 +++++++++++++++++ docs/zh/api/cpp.md | 70 +++++++++++++++++++++++++ docs/zh/api/python.md | 87 +++++++++++++++++++++++++++++++ docs/zh/api/typescript.md | 100 +++++++++++++++++++++++++++++++++--- docs/zh/concepts/engine.md | 47 +++++++++++++++++ 8 files changed, 594 insertions(+), 15 deletions(-) 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/engine.md b/docs/en/concepts/engine.md index 3950817b..ec2aac56 100644 --- a/docs/en/concepts/engine.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/engine.md b/docs/zh/concepts/engine.md index 2bdf12bb..43cbee65 100644 --- a/docs/zh/concepts/engine.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` 可以在单轮中从多前提规则推导出结论。