7个版本 (4个重大变化)

0.7.0 2024年4月2日
0.6.0 2023年10月26日
0.5.2 2023年10月26日
0.4.1 2023年10月25日
0.3.2 2023年10月22日

#59 in 数据库实现


2 个crate中使用 (通过 eqlog)

MIT 许可证

2.5MB
63K SLoC

Eqlog

带有函数符号和等价性的Datalog扩展。结合了Datalog和等价闭包。编译为专门为Rust项目集成的Rust代码。

示例:半格

半格是部分顺序(即具有自反性、传递性和反对称关系的集合)具有二元交集。它们可以在以下eqlog理论中形式化

// semilattice.eqlog

// The carrier set.
type El;
// The less-equal relation.
pred le: El * El;

// The less-equal relation is reflexive.
rule reflexivity {
    if x: El; 
    then le(x, x);
}
// The less-equal relation is transitive.
rule transitivity {
    if le(x, y);
    if le(y, z);
    then le(x, z);
}
// The less-equal relation is antisymmetric.
rule antisymmetry {
    if le(x, y);
    if le(y, x);
    then x = y;
}

// A function assigning binary meets, i.e. binary greatest lower bound.
func meet(El, El) -> El;

// The meet function is total, i.e. defined for all elements x, y.
rule totality {
    if x: El;
    if y: El;
    then meet(x, y)!;
}
// The meet is a lower bound.
rule lower_bound {
    if m = meet(x, y);
    then le(m, x);
    then le(m, y);
}
// The meet is the greatest lower bound.
rule greatest_lower_bound {
    if le(z, x);
    if le(z, y);
    if m = meet(x, y);
    then le(z, m);
}

Eqlog将此eqlog文件转换为Rust模块,允许对半格理论模型(即半格)进行计算。例如,我们可以验证半格中的交集函数是结合的

// main.rs

use eqlog_runtime::eqlog_mod;
eqlog_mod!(semilattice);
use crate::semilattice::*;

fn main() {
    // Create an empty semilattice structure and add three elements to it.
    let mut sl = Semilattice::new();
    let x = sl.new_el();
    let y = sl.new_el();
    let z = sl.new_el();

    // Close the semilattice structure by matching atoms in if statements of rules
    // and adding then atoms until we reach a fixed point.
    sl.close();
    // sl satisfies all semilattice rules now.

    // Evaluate the left-associated meet xy_z = (x /\ y) /\ z.
    let xy = sl.meet(x, y).unwrap();
    let xy_z = sl.meet(xy, z).unwrap();

    // Evaluate the right-associated meet x_yz = x /\ (y /\ z).
    let yz = sl.meet(y, z).unwrap();
    let x_yz = sl.meet(x, yz).unwrap();

    // Check that the two elements are equal.
    if sl.are_equal_el(xy_z, x_yz) {
        println!("Meet is associative.");
    } else {
        println!("Meet is not associative.");
    }
}

eqlog-test/src 包含更多示例

Rust项目集成

一个完整的项目示例可以在 examples/semilattice 中找到。Eqlog由编译器crate eqlog 组成,该crate只在构建期间需要,以及运行时crate eqlog-runtime。我们可以在 Cargo.toml 中将它们指定为依赖项,方法是在其中添加以下行

[dependencies]
eqlog-runtime = "0"

[build-dependencies]
eqlog = "0"

为了使我们的Rust代码与eqlog生成的代码集成,我们需要在构建crate本身之前运行eqlog。这可以通过在包根目录中添加以下 build.rs 文件来实现

fn main() {
    eqlog::process_root();
}

货物在构建之前会自动执行 build.rs 文件。代码 eqlog::process_root()src 目录下查找文件,并为每个 .eqlog 文件生成一个 rust 模块。要声明从 eqlog 文件生成的 rust 模块,我们使用 eqlog_mod!

use eqlog_runtime::eqlog_mod;
eqlog_mod!(<filename without extension>);

请注意,对于 src 目录下嵌套子目录中的 eqlog 文件,需要使用指定完整路径的特殊调用。

语言

每个 eqlog 文件由一系列类型、谓词、函数和规则声明组成。在数学上,eqlog 是指定 本质代数理论 的一种方式。

类型

类型代表我们理论模型的不同的载体集合。它们的声明如下

type <TypeName>;

谓词

谓词的声明如下

pred <PredName>(<Type_1>, ..., <Type_n>);

每个谓词都有一个项数,即括号中出现的类型的列表。

函数

函数的声明如下

func <FuncName>(<ArgType_1>, ..., <ArgType_n>) -> <ResultType>;

每个函数有一个定义域,即箭头前出现的类型的列表,以及箭头后的值域类型。

空定义域的函数是常量。常量的声明如下

func <ConstantName> : <Type>;

对于 eqlog,函数与 部分 函数同义;它们不需要是全称的。

规则

规则的形式为

rule <name> {
    if <Premise_1>;
    if <Premise_2>;
    ...
    if <Premise_m>;
    then <Conclusion_1>;
    then <Conclusion_2>;
    ...
    then <Conclusion_n>;
}

其中 <Premise_i><Conclusion_j>原子

大多数原子包含 。项要么是变量,要么是将函数符号应用到项上的应用。

Eqlog 支持以下原子

  • 形式为 <PredName>(<arg_1>, ..., <arg_n>) 的原子。这样的原子断言 <arg_1>, ..., <arg_n> 必须满足 <PredName> 谓词。
  • 形式为 <term>! 的原子。注意感叹号。这样的原子断言 <term> 是定义的,即所涉及到的函数在其参数上是定义的。在 then 子句中,<term> 可以像这样命名:<var_name> := <term>!
  • 形如 <tm_1> = <tm_2> 的原子。这样的原子断言,术语 <tm_1><tm_2> 均已定义且相等。
  • 形如 <var_name> : <TypeName> 的原子。这样的原子断言 <var_name> 是类型 <TypeName> 的变量。它们只能出现在 if 子句中。

除了感叹号操作符的 <var> := ... 形式外,在 then 语句中不得引入额外的变量。每个出现在规则 then 语句中的术语都必须等于出现在早期 if 语句中的术语。绕过此限制的唯一方法是添加形式为 then <tm>!; 的语句。之后的 then 语句原子可以自由使用 <tm>

例如,考虑以下关于半格理论的无效和有效规则

// Invalid: Cannot infer type of x.
rule {
    if x = x;
    then x = x;
}
// Valid (but nonsensical):
rule {
    if x: El;
    then x = x;
}

// Invalid: x and y are not introduced in an `if` statement.
rule {
    then meet(x, y)!;
}
// Valid:
rule {
    if x: El;
    if y: El;
    then meet(x, y)!;
}

// Invalid: meet(x, y) is not equal to a term occurring earlier.
rule {
    if le(z, x);
    if le(z, y);
    then le(z, meet(x, y));
}
// Valid: Assert that meet(x, y) exists in a then statement.
rule {
    if le(z, x);
    if le(z, y);
    then meet(x, y)!
    then le(z, meet(x, y));
}
// Valid: Suppose that meet(x, y) exists in if statement.
rule {
    if le(z, x);
    if le(z, y);
    if meet(x, y)!;
    then le(z, meet(x, y));
}

// Invalid: Both Meet(x, y) and Meet(y, x) do not occur earlier.
rule {
    if x: El;
    if y: El;
    then meet(x, y);
    then meet(y, x);
}

// Valid: the term on the left-hand side of the equation is introduced
// in an if statement.
rule {
    if meet(x, y)!;
    then meet(x, y);
    then meet(y, x);
}

// Valid: the term on the right-hand side of the equation is introduced
// earlier in a then statement.
rule {
    if x: El;
    if y: El;
    then meet(x, y)!;
    then meet(y, x) = meet(x, y);
}

// Invalid: meet(x, y) is not equal to a term that occurs earlier.
rule {
    if u = meet(x, meet(y, z))!;
    then meet(meet(x, y), z) = u;
}

// Valid: All of u, Meet(x, y) and z are introduced in an if statement.
rule {
    if u = meet(x, meet(y, z))!;
    if meet(x, y)!;
    then u = meet(meet(x, y), z);
}

生成的 Rust 接口

Eqlog 将每个 .eqlog 转换为一个 .rs 文件。Rust 文件必须在 src 目录内的一个模块中声明(例如 lib.rsmain.rs),使用 eqlog_runtime::eqlog_mod! 宏。

Eqlog 生成带文档的 Rust 代码。要构建和查看此文档,请运行

cargo doc --document-private-items --open

生成的 Rust 模块的公共 API 由以下符号组成

  • 对于每个 Eqlog 类型,都有一个 Rust 类型 <TypeName>,用于表示该类型的元素 ID。
  • 模型结构。其名称通过将文件名转换为 Upper Camel Case 而得。例如,对于 semi_group.eqlog,这将变成 SemiGroup

模型结构具有以下成员函数

  • fn new() -> Self
    创建一个空模型结构。
  • fn close(&mut self)
    关闭模型下的所有规则。
  • pub fn close_until(&mut self, condition:impl Fn(&Self) -> bool) -> bool
    关闭模型下的所有规则,直到满足条件。如果模型可以在所有规则下关闭,但条件仍然不成立,则返回 false。
  • 对于每个类型
    • fn new_<sort_name>(&mut self) -> <TypeName>
      将一个新元素添加到模型结构中。
    • fn equate_<sort_name>(&mut self, lhs: <TypeName>, rhs: <TypeName>)
      在模型结构中强制执行 lhs = rhs 的等价。
    • fn are_equal_<sort_name>(&self, lhs: <TypeName>, rhs: <TypeName>) -> bool
      如果 lhsrhs 表示相同的元素,则返回 true。
    • fn root_<sort_name>(&self, el: <TypeName>) -> <TypeName>
      返回元素的等价类的基本/根元素。
  • 对于每个谓词
    • fn <pred_name>(&self,arg_1: <Type_1>, ...,arg_n: <Type_n>)
      检查给定的参数上谓词是否成立。
    • fn iter_<pred_name>(&self) ->impl'_ + 迭代器<Item =<arity>>
      返回一个满足谓词的元组的迭代器。迭代器产生的项类型由arity确定。
    • fn insert_<pred_name>(&mut self, arg_1: <Type_1>, ..., arg_n: <Type_n>)
      确保给定的参数上谓词成立。
  • 对于每个函数
    • fn <func_name>(&self,arg_1: <Type_1>, ...,arg_n: <Type_n>) -> Option<<ResultType>>
      在给定的参数上评估函数。
    • fn iter_<func_name>(&self) ->impl'_ + 迭代器<Item =<arity>>
      返回一个遍历函数图中的元组的迭代器。迭代器产生的项类型由arity确定。
    • fn define_<func_name>(&mut self, arg_1: <Type_1>, ..., arg_n: <Type_n>) -> <ResultType>
      确保函数在给定的参数上定义,如果需要则添加新元素。
    • fn insert_<func_name>(&mut self, arg_1: <Type_1>, ..., arg_n: <Type_n>, result: <ResultType>)
      将元组插入函数的图中。

例如,以下是从半格理论生成的rust模块的公共声明

struct El;
struct Semilattice;
impl Semilattice {
  fn new() -> Self;
  fn close(&mut self);
  fn close_until(&mut self, condition: impl Fn(&Self) -> bool) -> bool;

  fn new_el(&mut self) -> El;
  fn equate_el(&mut self, lhs: El, rhs: El);
  fn are_equal_el(&self, lhs: El, rhs: El) -> bool;
  fn root_el(&self, el: El) -> El;

  fn le(&self, arg0: El, arg1: El) -> bool;
  fn iter_le(&self) -> impl '_ + Iterator<Item = (El, El)>;
  fn insert_le(&mut self, arg0: El, arg1: El);

  fn meet(&self, arg0: El, arg1: El) -> Option<El>;
  fn iter_meet(&self) -> impl '_ + Iterator<Item = (El, El, El)>;
  fn define_meet(&mut self, arg0: El, arg1: El) -> El;
  fn insert_meet(&mut self, arg0: El, arg1: El, result: El);
}

数据模型和算法

标准Datalog功能

eqlog生成的理论模型结构可以看作是内存中的SQL数据库,其模式由类型、谓词和函数声明给出。给定类型的元素是简单的整数ID,模型结构维护每个类型的有效ID列表。每个谓词 p 都表示为一个表,其行是谓词成立的元素的元组。函数表示为。例如,如果 f 是一个单参数函数,那么 f 的内部表将由以下形式的行组成:(x, f(x))

close 函数重复枚举规则中的if语句的所有匹配,并将规则中then语句对应的数据附加到模型中。最终,达到一个不动点(除非理论包含非满射规则,见下文),算法停止。例如,对于传递性公理

rule {
    if le(x, y);
    if le(y, z);
    then le(x, z);
}

close 函数枚举 Le 表中所有满足 y_0 = y_1(x, y_0)(y_1, z) 行,然后将行 (x, z) 添加到 Le。最终,Le 表将已经包含我们找到的所有新行 (x, z),这意味着我们已经达到不动点,可以停止:现在 le 谓词是传递的。

if语句实例的枚举称为SQL术语中的(内部)连接。SQL数据库使用索引加速内部连接,eqlog自动选择并维护索引以加快eqlog文件中列出的规则所需的连接。在每次迭代中,不需要枚举在上一迭代中已枚举的if语句。这种优化称为半朴素评估,eqlog再次使用它来加速 close 函数。

等式

除了之前讨论的标准日志功能外,eqlog还支持在then语句中的等式。一个例子是偏序的反对称公理。

rule {
    if le(x, y);
    if le(y, x);
    then x = y;
}

then语句中的等式的另一个来源是函数的隐式功能公理:对于函数f,如果我们发现图f中同时存在(x_1, ..., x_n, y)(x1, , ..., x_n, z),那么我们必须推导出y = z。如果我们用g_f表示f的图,那么隐式功能公理可以表述如下。

rule {
    if g_f(x_1, ..., x_n, y);
    if g_f(x_1, ..., x_n, z);
    then y = z;
}

然而,需要注意的是,在eqlog文件中不能直接引用函数的图。

为了处理导出的等式,eqlog模型结构为每种类型维护并查集数据结构。当在调用close过程中推导出等式x = y时,eqlog会合并xy的等价类。

为了加快匹配if语句时所需连接的计算,eqlog为每个表维护索引。然而,只有当元素id可以直接比较相等而不是咨询并查集数据结构时,这些索引才能使用。因此,eqlog维护一个不变性,即所有谓词和函数图表只包含规范类型元素,即只有id是相应并查集数据结构的根节点的元素。

当eqlog将某个元素x的等价类合并到元素y的等价类中时,这个不变性暂时被违反。为了恢复这个不变性,eqlog会从包含x的表中删除所有行,用新的根idy替换x,然后再次插入行。为了加快这个过程,eqlog为每个根id维护一个列表,该列表包含所有出现根id的行。

非全射规则和非终止

回想一下,eqlog规则需要是全射的,除非使用感叹号操作符!:then语句中的每个元素都必须等于if语句中的某个元素。在全射规则下关闭模型结构不会增加模型中元素的数量,这保证了close函数最终会终止。

如果有非全射规则,则这并不保证。考虑以下eqlog理论,该理论形式化自然数

type N;
zero: N;
succ: N -> N;

rule {
    then Zero()!;
}
rule {
    if n: N;
    then Succ(n)!;
}

关闭空模型结构首先将元素zero添加到模型中,然后是元素succ(zero),然后是succ(succ(zero))等等。然而,非全射规则的存在并不一定意味着close函数必须无限期运行。例如,半格理论包含非全射规则

rule {
    if x: El;
    if y : El;
    then meet(x, y)!;
}

close仍然会终止。

如果一个理论包含非满射规则,生成的 close 函数将包含嵌套的 close 循环:外层循环负责非满射规则,内层循环负责满射规则。在伪代码中,close 函数看起来像这样

// Match `if` statements in a rule and change the model such that the
// corresponding `then` statements holds. Returns true if the model was
// changed, i.e. if new facts were concluded.
fn adjoin_conclusions(ax: Axiom) -> bool {
  ...
}

fn close() {
  loop {
    loop {
      let model_changed = surjective_rules.iter().any(adjoin_conclusion);
      if !model_changed {
        break;
      }
    }

    let model_changed = non_surjective_rules.iter().any(adjoin_conclusion);
    if !model_changed {
      break;
    }
  }
}

背景

要更深入地了解 Eqlog 的工作原理,请查看这些论文

  • 带有等价的 Datalog 的评估算法 [Web] [PDF]
  • 带有等价的 Datalog 的代数语义 [Web] [PDF]

还可以查看 egglog 项目 及其相应的 论文。尽管其表面语言略有不同,但 Egglog 基于与 Eqlog 非常相似的思想,可用于许多相同的应用。

依赖项

~2.5–4MB
~67K SLoC