本教程的目的是让您即使没有什么 Rust 经验,也能开始使用 egg。如果你还没听说过e-graph,可以先阅读背景教程。如果你有 Rust 相关经验,可以略读本节。
开始使用Rust
Rust 是 egg 快速(系统编程 优化编译器)和灵活(泛型和特质)的原因之一。
Rust 的工作人员为学习 Rust 编写了一本免费的好书,Rust 网站的 "Learn "页面上还收集了大量其他精彩资源。本教程不能替代这些资源,而是旨在让你尽快上手。
首先,安装 Rust,然后使用 Rust 的软件包管理和构建工具 cargo 创建一个项目: cargo new my-first-egg.1
现在,我们可以在 Cargo.toml 中添加一行,将 egg 添加为项目依赖关系:
代码语言:javascript复制[dependencies]
egg = "0.9.5"
下面的代码示例都可以使用,但您必须使用相关类型。您只需在文件顶部添加 use egg::*;,即可将它们全部引入。
现在你说的是我的Language
EGraphs(以及该板块中的几乎所有其他内容)都是根据用户提供的语言进行参数化的。虽然 egg 支持轻松创建自己的语言,但我们将从提供的 SymbolLang 开始。
语言是一种特质,实现语言的类型的值就是 e-node。一个 e-node 可以有任意数量的子节点,这些子节点就是 Id。Id 基本上只是一个数字,egg 用它来协调 e-node 与哪些子节点相关联。在 EGraph 中,e-node的子节点指的是 e-class。在 RecExpr(egg 版本的普通表达式)中,e-node 的子节点指的是该 RecExpr 中的其他 e-node。
包括 SymbolLang 在内的大多数语言都可以进行解析和漂亮打印。这意味着这些语言中的 RecExpr 实现了 Rust 标准库中的 FromStr 和 Display 特性。
代码语言:javascript复制// Since parsing can return an error, `unwrap` just panics if the result doesn't return Ok
let my_expression: RecExpr<SymbolLang> = "(foo a b)".parse().unwrap();
println!("this is my expression {}", my_expression);
// let's try to create an e-node, but hmmm, what do I put as the children?
let my_enode = SymbolLang::new("bar", vec![]);
有些 e-node 只是常量,没有子节点(也称为叶子)。但是,如果单独创建有子节点的 e 节点,就会显得有些别扭,因为你必须添加无意义的 Ids 作为子节点。创建有意义 Ids 的方法是将 e-nodes 添加到 EGraph 或 RecExpr 中:
代码语言:javascript复制let mut expr = RecExpr::default();
let a = expr.add(SymbolLang::leaf("a"));
let b = expr.add(SymbolLang::leaf("b"));
let foo = expr.add(SymbolLang::new("foo", vec![a, b]));
// we can do the same thing with an EGraph
let mut egraph: EGraph<SymbolLang, ()> = Default::default();
let a = egraph.add(SymbolLang::leaf("a"));
let b = egraph.add(SymbolLang::leaf("b"));
let foo = egraph.add(SymbolLang::new("foo", vec![a, b]));
// we can also add RecExprs to an egraph
let foo2 = egraph.add_expr(&expr);
// note that if you add the same thing to an e-graph twice, you'll get back equivalent Ids
assert_eq!(foo, foo2);
使用Patterns搜索 EGraph
既然我们可以在 EGraph 中添加内容,那就让我们看看能否找到它。我们将使用实现了 Searcher 特性的 Pattern 在e-graph中搜索匹配项:
代码语言:javascript复制// let's make an e-graph
let mut egraph: EGraph<SymbolLang, ()> = Default::default();
let a = egraph.add(SymbolLang::leaf("a"));
let b = egraph.add(SymbolLang::leaf("b"));
let foo = egraph.add(SymbolLang::new("foo", vec![a, b]));
// rebuild the e-graph since we modified it
egraph.rebuild();
// we can make Patterns by parsing, similar to RecExprs
// names preceded by ? are parsed as Pattern variables and will match anything
let pat: Pattern<SymbolLang> = "(foo ?x ?x)".parse().unwrap();
// since we use ?x twice, it must match the same thing,
// so this search will return nothing
let matches = pat.search(&egraph);
assert!(matches.is_empty());
egraph.union(a, b);
// recall that rebuild must be called to "see" the effects of adds or unions
egraph.rebuild();
// now we can find a match since a = b
let matches = pat.search(&egraph);
assert!(!matches.is_empty());
使用 Runner 制作优化器
既然我们已经可以创建模式并使用 RecExprs,那么就可以创建一个优化器了!我们将使用 rewrite!宏来轻松创建 Rewrites,其中包括名称、左侧搜索模式和右侧应用模式。然后,我们可以使用 Runner API 运行 egg 的相等饱和算法。最后,我们可以使用提取器获得最佳结果。
代码语言:javascript复制use egg::{*, rewrite as rw};
let rules: &[Rewrite<SymbolLang, ()>] = &[
rw!("commute-add"; "( ?x ?y)" => "( ?y ?x)"),
rw!("commute-mul"; "(* ?x ?y)" => "(* ?y ?x)"),
rw!("add-0"; "( ?x 0)" => "?x"),
rw!("mul-0"; "(* ?x 0)" => "0"),
rw!("mul-1"; "(* ?x 1)" => "?x"),
];
// While it may look like we are working with numbers,
// SymbolLang stores everything as strings.
// We can make our own Language later to work with other types.
let start = "( 0 (* 1 a))".parse().unwrap();
// That's it! We can run equality saturation now.
let runner = Runner::default().with_expr(&start).run(rules);
// Extractors can take a user-defined cost function,
// we'll use the egg-provided AstSize for now
let extractor = Extractor::new(&runner.egraph, AstSize);
// We want to extract the best expression represented in the
// same e-class as our initial expression, not from the whole e-graph.
// Luckily the runner stores the eclass Id where we put the initial expression.
let (best_cost, best_expr) = extractor.find_best(runner.roots[0]);
// we found the best thing, which is just "a" in this case
assert_eq!(best_expr, "a".parse().unwrap());
assert_eq!(best_cost, 1);