18 个版本 (6 个重大更新)
0.7.0 | 2024年4月2日 |
---|---|
0.6.0 | 2023年10月26日 |
0.5.2 | 2023年10月26日 |
0.1.5 | 2023年7月10日 |
0.1.2 | 2022年8月26日 |
#1 in #理论
在 2 个crate中使用 (通过 eqlog-eqlog)
2.5MB
68K 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 eqlog-runtime
。我们可以在 Cargo.toml
中指定它们作为依赖项,通过添加以下行来实现
[dependencies]
eqlog-runtime = "0"
[build-dependencies]
eqlog = "0"
为了使我们的Rust代码与eqlog生成的代码集成,我们需要在构建crate之前运行eqlog。这可以通过在包根目录中 Cargo.toml
旁边添加以下 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.rs
或 main.rs
),并使用 eqlog_runtime::eqlog_mod!
宏。
Eqlog 生成文档化的 Rust 代码。要构建和查看此文档,请运行
cargo doc --document-private-items --open
生成的 Rust 模块的公共 API 由以下符号组成
- 对于每个 Eqlog 类型,一个 Rust 类型
<TypeName>
,用于此类型的元素 ID。 - 模型结构。其名称通过将文件名转换为驼峰式命名法生成。例如,对于
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
如果lhs
和rhs
表示相同的元素,则返回 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
表中所有行(x, y_0)
和(y_1, z)
,使得y_0 = y_1
,然后将行(x, z)
添加到Le
。最终,Le
表将包含我们找到的所有新行(x, z)
,这意味着我们已经达到不动点,可以停止:现在谓词le
是传递的。
if语句实例的枚举称为SQL术语中的(内)连接。SQL数据库使用索引加速内连接,eqlog自动选择并维护索引以加速eqlog文件中列出的规则所需的连接。在每次迭代中,没有必要枚举在前一个迭代中已经枚举的if语句。这种优化称为半朴素评估,eqlog再次使用它来加速close
函数。
等式
除了前面讨论的标准Datalog功能外,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将合并x
和y
的等价类。
为了加快匹配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 的工作原理,请参阅以下论文:
还可以查看 egglog 项目 以及相应的 论文。尽管其表面语言略有不同,但 Egglog 的基础思想与 Eqlog 非常相似,可以用于许多相同的应用。
依赖关系
~7-10MB
~168K SLoC