0x00 前言

最近看的工具中用到了 Doop,这是一个以 指针分析 算法为中心的 Java 静态分析框架,使用 Datalog 规则形式表达的各种分析的集合。维护两套规则:

  • LogicBlox:使用 LogiQL,Datalog 程序的构建块以 谓词 的形式出现,例如 Person("John")Parent("John", "Johnny jr"),利用 规则(IDB 逻辑)从已知的事实中推断出新的信息,例如 Ancestor(x, y) <- Parent(x, y)
  • souffle-lang/souffle (Systematic, Ontological, Undiscovered Fact Finding Logic Engine):由 Datalog 启发的一种编程语言,可用于静态分析、网络分析和数据分析。默认使用该引擎。

没看到默认用的是开源的 souffle,结果花了一下午入门 LogiQL 还有找 3.10.x 版本的 LogicBlox …

LogicBlox 一直是闭源软件,目前最新版本 4.x.x 都可以下载,Release Archive (LB 3.10.x) 不再提供下载,可以通过 LogicBlox Academic License Request 申请。找了好久没地方下载还要申请,回头仔细看了看 Doop 的说明文档才发现应该去看 souffle 😅😅😅

LogiQL 入门跟着教程 LogiQL in 30 minutes 走就好了,介绍的特别简单,能够让人刚踩到门槛上吧(大概)。

0x01 LogiQL 入门

实验环境

用在线的 LogicBlox REPL (Read-Eval-Print Loop) 进行实验,REPL 是一个基于命令的交互式环境,可以在其中执行 LogiQL 命令并立即检查结果。

术语

工作区(workspace)指代数据库(database),结合了模式(schema)、逻辑(logic)和数据(data)

实验过程

定义一个冰淇淋商店会计系统,记录以下信息:

  • 成本(人工,原料):cost(labor, ingredients)
  • 价格:price
  • 销量/周:sales
  • 每个冰淇淋的收入/周:revenue
  • 所有冰淇淋的收入/周:week revenue
  • 每个冰淇淋的收益/周:profit
  • 所有冰淇淋的收益/周:week profit

定义功能谓词(functional predicates)

  • 谓词类似 SQL 数据库中的表
  • 除了最后一列之外的所有列都形成一个主键,最后一列表示值,因此一个主键最多可以关联一个值。

1. cost 谓词

定义名为 cost 的谓词,键为 icecream,值为 c;指向右侧的箭头表示约束(constraint),icecream 的值必须是字符串、c 的值必须是整数

  • 约束表明如果箭头左侧为真,则右侧必须成立
1
2
cost[icecream] = c ->
string(icecream), int(c).

在 REPL 中执行需要使用相应的命令,addblock 接受一个参数,在引号之间添加的定义的 LogiQL 代码。

1
2
addblock 'cost[icecream] = c ->
string(icecream), int(c).'

添加之后使用 list 命令列出谓词

1
list

2. 事实(facts)

谓词中的数据条目在 LogiQL 中称为事实(facts)。

LogiQL 支持 3 种增量更新操作

  • 插入(insert):插入新事实,使用 + 前缀
  • 删除(remove):删除事实,使用 - 前缀
  • 更新(upsert):添加或更新,使用 ^ 前缀,只能应用在功能谓词上

使用 exec 执行一次性的命令,而 addblock 可以理解为“永久”地将谓词添加到工作区。

添加一个成本为 23 的冰淇淋:

1
exec '+cost["Popsicle Lemon"] = 23.'

print 命令用于打印谓词。

1
print cost

添加冰淇淋(成本):

1
2
3
4
5
exec '+cost["Fruit Sundae"] = 120.
+cost["Mango Sorbet"] = 40.
+cost["Cone Chocolate"] = 50.
+cost["Cone Vanilla"] = 44.
+cost["Cone Chili Fries"] = 200.'

更新冰淇淋的售价:

1
exec '^cost["Popsicle Lemon"] = 25.'

3. price 谓词、week_sales 谓词

定义谓词 price 表示冰淇淋的售价,定义谓词 week_sales 表示每周的销售数量(可以把 week_sales 看作二维映射)。

1
2
3
4
5
6
7
addblock 'price[icecream] = p ->
string(icecream), int(p).

week_sales[icecream, week] = value ->
string(icecream),
int(week),
int(value).'

添加冰淇淋售价及其第 1、2 周的销量(往谓词中添加事实):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exec '+price["Popsicle Lemon"] = 50.
+price["Fruit Sundae"] = 200.
+price["Mango Sorbet"] = 70.
+price["Cone Chocolate"] = 80.
+price["Cone Vanilla"] = 70.

+week_sales["Popsicle Lemon", 1] = 122.
+week_sales["Fruit Sundae", 1] = 88.
+week_sales["Mango Sorbet", 1] = 72.
+week_sales["Cone Chocolate", 1] = 4.
+week_sales["Cone Vanilla", 1] = 257.

+week_sales["Popsicle Lemon", 2] = 112.
+week_sales["Fruit Sundae", 2] = 60.
+week_sales["Mango Sorbet", 2] = 44.
+week_sales["Cone Chocolate", 2] = 9.
+week_sales["Cone Vanilla", 2] = 200.'

打印谓词:

1
2
print price
print week_sales

4. 规则(rules)

规则可用于推导新的事实,添加计算收入 revenue 的规则:

1
2
addblock 'week_revenue[icecream, week] =
price[icecream] * week_sales[icecream, week].'

打印规则,该规则会自动进行计算:

1
print week_revenue

修改一个价格,会自动进行重新计算:

1
2
3
exec '^price["Popsicle Lemon"] = 70.'

print week_revenue

添加计算冰淇淋收益 profit 的规则、计算每周收益的规则 week_profit

1
2
3
4
5
addblock 'profit[icecream] =
price[icecream] - cost[icecream].

week_profit[icecream, week] =
profit[icecream] * week_sales[icecream, week].'

然后打印规则,会自动进行计算:

1
2
print profit
print week_profit

5. 聚合函数(Aggregate)

和 SQL 类似,LogiQL 中也有聚合函数。

定义 agg_profit 规则,使用 total() 聚合函数求和,将 profit 中所有事实的值 p 求和绑定到 value,其中 week 是聚合的周,使用通配符 _ 忽略冰淇淋类型。表示总收益(收入-成本)。

agg_revenue 规则也是类似的定义,只不过把 p 换成了 r,表示总收入。

1
2
3
4
5
agg_profit[week] = value <-
agg<<value=total(p)>> week_profit[_, week] = p.

agg_revenue[week] = value <-
agg<<value=total(r)>> week_revenue[_, week] = r.

使用 addblock 添加规则:

1
2
3
4
5
addblock 'agg_profit[week] = value <-
agg<<value=total(p)>> week_profit[_, week] = p.

agg_revenue[week] = value <-
agg<<value=total(r)>> week_revenue[_, week] = r.'

打印规则,得到聚合的结果:

1
2
print agg_profit
print agg_revenue

定义一个没有键的规则计算 price 谓词中的事实(条目)数量,即冰淇淋类型的数量:

1
2
addblock 'icecream_count[] = value <-
agg<<value=count()>> price[_] = _.'

打印规则,得到冰淇淋类型的数量:

1
print icecream_count

6. 查询(query)

之前使用 print 查看谓词和规则的值,现在可以用 query 执行更高级的查询。

定义一个谓词 _ 用事实填充,通常使用规则实现。

下面的查询与 print week_sales 拥有等价的结果。这里不使用功能谓词语法 predicate[k1, k2] = v,而是使用 predicate(k1, k2, k3),因为这里对键值不感兴趣,只对事实感兴趣。

1
2
query '_(icecream, week, value) <-
week_sales[icecream, week] = value.'

检索 week_sales 谓词中使用的所有冰淇淋,尽管 week_sales 谓词多次包含这些值中的每一个,但每个不同值只返回一次(有 1、2 两周的数据,所以冰淇淋重复出现)

  • 在 LogiQL 中所有谓词都是集合:每一行只能出现一次
1
query '_(icecream) <- week_sales[icecream, _] = _.'

将冰淇淋和每周的销售额、收入、收益整合在一起,就像 SQL 中的 JOIN 操作:

1
2
3
4
query '_(icecream, week, sales, revenue, profit) <-
week_sales[icecream, week] = sales,
week_revenue[icecream, week] = revenue,
week_profit[icecream, week] = profit.'

7. 约束(constraint)

降低冰淇淋的售价,再次查询发现冰淇淋的收益变成了负数:

1
2
exec '^price["Popsicle Lemon"] = 20.'
query '_(profit) <- profit["Popsicle Lemon"] = profit.'

定义一个约束:对于 profit 谓词中的每个事实,value 应该大于等于 0。

1
profit[_] = value -> value >= 0.

使用 addblock 添加约束,因为 profit 谓词中有事实的 value 值小于 0,所以添加约束失败。

1
addblock 'profit[_] = value -> value >= 0.'

修改冰淇淋售价后再添加约束:

1
2
exec '^price["Popsicle Lemon"] = 40.'
addblock 'profit[_] = value -> value >= 0.'

再次修改冰淇淋售价,返回约束错误:

1
exec '^price["Popsicle Lemon"] = 10.'

0x02 结语

有一说一,这个入门完全不足以让人看懂 doop/lb-logic 里面的规则,做都做了就理上来了XD

补充:本地可以使用 PA-Datalog 引擎,是 LogicBlox v3 的改良版本,可用于程序分析。