@xiaohuo123212/elench 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +53 -0
- package/README.md +298 -2
- package/bin/elench.js +69 -0
- package/package.json +37 -4
- package/scripts/clean-pycache.js +37 -0
- package/scripts/npm_smoke.js +88 -0
- package/src/README-/345/205/274/345/256/271/345/261/202.md +26 -0
- package/src/ast_util.py +111 -0
- package/src/bench.py +376 -0
- package/src/borrow.py +282 -0
- package/src/bound.py +13 -0
- package/src/cfg.py +219 -0
- package/src/chart.py +209 -0
- package/src/check_docs.py +156 -0
- package/src/checker.py +2473 -0
- package/src/contract_metric.py +87 -0
- package/src/cost_check.py +13 -0
- package/src/cost_diff.py +533 -0
- package/src/cost_domain/__init__.py +32 -0
- package/src/cost_domain/bound.py +739 -0
- package/src/cost_domain/cost_check.py +394 -0
- package/src/cost_domain/cost_interp.py +1998 -0
- package/src/cost_domain/symbolic.py +554 -0
- package/src/cost_explain.py +503 -0
- package/src/cost_interp.py +13 -0
- package/src/cxx_map.py +184 -0
- package/src/dispatch.py +314 -0
- package/src/dsh.py +3980 -0
- package/src/dup_scan.py +180 -0
- package/src/elench.py +15 -0
- package/src/emit.py +56 -0
- package/src/ffi_c.py +70 -0
- package/src/fmt.py +115 -0
- package/src/formatter.py +596 -0
- package/src/grammar.py +272 -0
- package/src/guarantees.py +116 -0
- package/src/highlight.py +156 -0
- package/src/interp.py +2119 -0
- package/src/isolated.py +161 -0
- package/src/jsonval.py +236 -0
- package/src/kappa_vs_measured.py +347 -0
- package/src/kinds.py +122 -0
- package/src/lexer.py +559 -0
- package/src/migrate_docs.py +115 -0
- package/src/otel.py +579 -0
- package/src/otel_dashboard.py +218 -0
- package/src/otel_export.py +380 -0
- package/src/otel_local_sink.py +141 -0
- package/src/otel_view.py +156 -0
- package/src/parser.py +1909 -0
- package/src/parser.py.bak +1781 -0
- package/src/policy.py +271 -0
- package/src/rename_ch.py +81 -0
- package/src/requires.py +242 -0
- package/src/segment_plan.py +150 -0
- package/src/shape.py +130 -0
- package/src/stdlib.py +1856 -0
- package/src/stdlib.py.bak +1751 -0
- package/src/symbolic.py +13 -0
- package/src/sync_docs.py +225 -0
- package/src/test_checker.py.bak +443 -0
- package/src/test_formatter.py.bak +201 -0
- package/src/test_invalid.py.bak +342 -0
- package/src/test_parser.py.bak +423 -0
- package/src/typecheck.py +865 -0
- package/src/typecheck.py.bak +741 -0
- package/src/typenames.py +56 -0
- package/src/walk.py +197 -0
package/src/borrow.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# Copyright 2026 Elench contributors
|
|
4
|
+
"""borrow.py —— 借用检查(分支敏感,基于 CFG + 活跃性)
|
|
5
|
+
|
|
6
|
+
**为什么有这个文件**
|
|
7
|
+
|
|
8
|
+
我们选了 C++ 后端(决定清单 §10.47)。**Rust 会替我们证借用安全,C++ 不会。**
|
|
9
|
+
没有这一层,`const u64& r = xs.at(0); xs.push(1); return r;` 会静默编译通过,
|
|
10
|
+
运行时读越界——**那是在交付 UB,比不做工具更糟。**
|
|
11
|
+
|
|
12
|
+
**算法(分支敏感)**
|
|
13
|
+
|
|
14
|
+
1. 为函数体建 CFG(`cfg.build`);
|
|
15
|
+
2. 算活跃性不动点(`cfg.liveness`):`live_in[n]` = 执行到 n 之前"之后还会用到"的变量;
|
|
16
|
+
3. 收集借用事件:`const T& r = <以 x 为根的表达式>` → 借用 `(x → r)`;
|
|
17
|
+
4. **判定**:若某节点**修改** `x`,且 `r ∈ live_in[该节点]`,则借用仍活着 → 报 `E-Borrow-Conflict`。
|
|
18
|
+
|
|
19
|
+
> **这就是 Rust MIR borrowck 的思路(liveness-based NLL)。**
|
|
20
|
+
> 与"一条时间线"的旧近似相比,它能正确处理**分支**:一支改、另一支不改,照样判得准。
|
|
21
|
+
|
|
22
|
+
**规则覆盖**(对应规格 T7-a)
|
|
23
|
+
|
|
24
|
+
| 规则 | 状态 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| BR-5 结构体不得存引用 | ✅ |
|
|
27
|
+
| BR-1 借用期间不得可变借用 | ✅ 分支敏感 |
|
|
28
|
+
| BR-2 NLL | ✅ **基于活跃性**(分支敏感,非近似) |
|
|
29
|
+
| BR-4 返回引用归属不明 | ✅ |
|
|
30
|
+
| BR-3 方法返回引用默认借自 `self` | ✅ **由 `_base_name` 构造实现**(取表达式里第一个 `path`:`xs.at(0)` → `xs`)。方向保守——`s.field.at(0)` 记成借自 `s`,宁可多报 |
|
|
31
|
+
|
|
32
|
+
**保守方向**:宁可多报,不可漏报——**漏报就是 UB**。
|
|
33
|
+
"""
|
|
34
|
+
from typing import List, Optional, Set, Tuple
|
|
35
|
+
|
|
36
|
+
import cfg as cfgmod
|
|
37
|
+
from cfg import _base_of
|
|
38
|
+
from ast_util import descendants
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _first_path(node) -> Optional[str]:
|
|
42
|
+
"""节点自身或其后代里的第一个 path。"""
|
|
43
|
+
if node is None:
|
|
44
|
+
return None
|
|
45
|
+
if getattr(node, "kind", "") == "path" and getattr(node, "name", None):
|
|
46
|
+
return node.name
|
|
47
|
+
for p in descendants(node, "path"):
|
|
48
|
+
if p.name:
|
|
49
|
+
return p.name
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _base_name(node) -> Optional[str]:
|
|
54
|
+
"""表达式的"被借对象"。
|
|
55
|
+
|
|
56
|
+
**三种形状要分开**(2026-11-06 修):
|
|
57
|
+
|
|
58
|
+
| 表达式 | 被借对象 | 以前 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `xs.at(0)` | `xs`(接收者) | ✅ 对 |
|
|
61
|
+
| `head_ref(v)` | `v`(实参) | ❌ **取成 `head_ref`**——被调函数名也是个 path! |
|
|
62
|
+
| `v` | `v` | ✅ |
|
|
63
|
+
|
|
64
|
+
以前的实现一律"取第一个 path",于是 `head_ref(v)` 被记成借了
|
|
65
|
+
`head_ref` —— 一个永远不被修改的名字,**借用检查彻底失效**(BR-3 的洞)。
|
|
66
|
+
"""
|
|
67
|
+
k = getattr(node, "kind", "")
|
|
68
|
+
if k == "method-call":
|
|
69
|
+
# 接收者在 children[0](`xs.at(0)` → path xs)
|
|
70
|
+
return _first_path(node.children[0] if node.children else None)
|
|
71
|
+
if k == "call":
|
|
72
|
+
# **跳过被调名**,只看实参:`head_ref(v)` → v
|
|
73
|
+
for c in (node.children or []):
|
|
74
|
+
if getattr(c, "kind", "") == "args":
|
|
75
|
+
return _first_path(c)
|
|
76
|
+
return None
|
|
77
|
+
return _first_path(node)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _check_struct_refs(tu_mod, out) -> None:
|
|
81
|
+
"""BR-5:结构体字段不得为引用。"""
|
|
82
|
+
if tu_mod is None:
|
|
83
|
+
return
|
|
84
|
+
for st in descendants(tu_mod, "struct"):
|
|
85
|
+
for f in st.children:
|
|
86
|
+
ty = f.children[0] if f.children else None
|
|
87
|
+
if ty is not None and getattr(ty, "kind", None) == "type-ref":
|
|
88
|
+
out.append((
|
|
89
|
+
"E-Borrow-StructRef",
|
|
90
|
+
f"结构体 `{st.name}` 的字段 `{f.name}` 不能是引用"
|
|
91
|
+
f"(BR-5:v0 不支持生命周期标注,引用存进结构体会逃出作用域)",
|
|
92
|
+
f.line, f.col,
|
|
93
|
+
))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _fn_body(fn):
|
|
97
|
+
return next((c for c in fn.children if getattr(c, "kind", None) == "block"), None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _mutable_ref_params(mod) -> dict:
|
|
102
|
+
"""`函数名 → 可变引用参数的下标集合`。
|
|
103
|
+
|
|
104
|
+
**`const` 记在 `param` 节点上**(不是 type-ref)——查错节点会得出
|
|
105
|
+
"两种写法一模一样"的错误结论。
|
|
106
|
+
`const T&` 只读;`T&` 可写,被调方可能改它,于是调用处要算一次"修改"。
|
|
107
|
+
"""
|
|
108
|
+
out = {}
|
|
109
|
+
if mod is None:
|
|
110
|
+
return out
|
|
111
|
+
for fn in descendants(mod, "fn"):
|
|
112
|
+
params = next((c for c in (fn.children or []) if c.kind == "params"), None)
|
|
113
|
+
idx = set()
|
|
114
|
+
for i, prm in enumerate(params.children if params else []):
|
|
115
|
+
ty = prm.children[0] if prm.children else None
|
|
116
|
+
if getattr(ty, "kind", "") != "type-ref":
|
|
117
|
+
continue
|
|
118
|
+
if (prm.value or {}).get("const"):
|
|
119
|
+
continue # const T& 只读
|
|
120
|
+
idx.add(i)
|
|
121
|
+
if idx and fn.name:
|
|
122
|
+
out[fn.name] = idx
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _check_fn(fn, out, mut_params=None) -> None:
|
|
127
|
+
body = _fn_body(fn)
|
|
128
|
+
if body is None:
|
|
129
|
+
return
|
|
130
|
+
g = cfgmod.build(body)
|
|
131
|
+
live = cfgmod.liveness(g)
|
|
132
|
+
|
|
133
|
+
# 语句 → 节点(简单语句一一对应;控制流节点的 st 是 if/while 本身)
|
|
134
|
+
node_of = {id(n.st): n for n in g.nodes.values() if n.st is not None}
|
|
135
|
+
|
|
136
|
+
# **传可变引用 = 可能被改**:`take(xs)` 里 `xs` 对应 `vector<u64>&` 形参,
|
|
137
|
+
# 被调方可以改它;以前这种调用**完全不算修改**,于是借用还活着也能传出去。
|
|
138
|
+
if mut_params:
|
|
139
|
+
for n in g.nodes.values():
|
|
140
|
+
if n.st is None:
|
|
141
|
+
continue
|
|
142
|
+
for call in descendants(n.st, "call"):
|
|
143
|
+
idx = mut_params.get(call.name or "")
|
|
144
|
+
if not idx:
|
|
145
|
+
continue
|
|
146
|
+
args = next((c for c in (call.children or []) if c.kind == "args"), None)
|
|
147
|
+
for i in idx:
|
|
148
|
+
if args is None or i >= len(args.children):
|
|
149
|
+
continue
|
|
150
|
+
base = _base_of(args.children[i])
|
|
151
|
+
if base:
|
|
152
|
+
n.muts.add(base)
|
|
153
|
+
n.uses.add(base)
|
|
154
|
+
|
|
155
|
+
borrows: List[Tuple[str, str, object]] = [] # (被借对象, 引用变量, 声明语句)
|
|
156
|
+
# **可变引用也算借用**(2026-11-06 修):以前只扫 `const-decl`,
|
|
157
|
+
# 于是 `u64& q = xs.at(0); xs.push(9); return q;` **完全不被检查**——
|
|
158
|
+
# 而可变引用比只读引用更危险(调用方能写穿它)。
|
|
159
|
+
for st in descendants(body, "const-decl") + descendants(body, "var-decl"):
|
|
160
|
+
if not st.children or st.name is None:
|
|
161
|
+
continue
|
|
162
|
+
ty = st.children[0]
|
|
163
|
+
if getattr(ty, "kind", None) != "type-ref":
|
|
164
|
+
continue
|
|
165
|
+
base = _base_name(st.children[-1])
|
|
166
|
+
if base:
|
|
167
|
+
borrows.append((base, st.name, st))
|
|
168
|
+
|
|
169
|
+
for base, ref_name, st in borrows:
|
|
170
|
+
for m in g.nodes.values():
|
|
171
|
+
if base in m.muts and ref_name in live.get(m.id, set()):
|
|
172
|
+
# 用被修改节点自己的位置报错,读者能直接跳过去
|
|
173
|
+
where = m.st if m.st is not None else st
|
|
174
|
+
out.append((
|
|
175
|
+
"E-Borrow-Conflict",
|
|
176
|
+
f"`{base}` 在借用 `{ref_name}` 还活着的时候被修改"
|
|
177
|
+
f"(BR-1/BR-2:借用期间不得可变借用)",
|
|
178
|
+
where.line, where.col,
|
|
179
|
+
))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _check_ambiguous_returns(ch_mod, out) -> None:
|
|
183
|
+
"""BR-4:返回引用但有 ≥2 个引用参数 → 无法唯一确定归属,v0 直接拒绝。"""
|
|
184
|
+
for fn in descendants(ch_mod, "fn"):
|
|
185
|
+
ret = None
|
|
186
|
+
for c in fn.children:
|
|
187
|
+
if getattr(c, "kind", None) == "type-ref":
|
|
188
|
+
ret = c
|
|
189
|
+
if ret is None:
|
|
190
|
+
continue
|
|
191
|
+
params = next((c for c in fn.children if getattr(c, "kind", None) == "params"), None)
|
|
192
|
+
ref_params = [p.name or "?" for p in (params.children if params else [])
|
|
193
|
+
if p.children and getattr(p.children[0], "kind", None) == "type-ref"]
|
|
194
|
+
if len(ref_params) >= 2:
|
|
195
|
+
out.append((
|
|
196
|
+
"E-Borrow-Ambiguous",
|
|
197
|
+
f"`{fn.name}` 返回引用,但有 {len(ref_params)} 个引用参数"
|
|
198
|
+
f"({', '.join(ref_params)})——无法唯一确定借自谁(BR-4)",
|
|
199
|
+
fn.line, fn.col,
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def check_borrows(tu_mod, ch_mod) -> List[Tuple[str, str, int, int]]:
|
|
204
|
+
"""分支敏感版本(默认)。"""
|
|
205
|
+
out = []
|
|
206
|
+
_check_struct_refs(tu_mod, out)
|
|
207
|
+
if ch_mod is not None:
|
|
208
|
+
mut_params = _mutable_ref_params(ch_mod)
|
|
209
|
+
for fn in descendants(ch_mod, "fn"):
|
|
210
|
+
_check_fn(fn, out, mut_params)
|
|
211
|
+
_check_ambiguous_returns(ch_mod, out)
|
|
212
|
+
return out
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------- 旧近似(保留给差分测试) ----------------
|
|
216
|
+
|
|
217
|
+
def check_borrows_conservative(tu_mod, ch_mod):
|
|
218
|
+
"""**旧版**:按源码顺序压成一条时间线(不分支敏感)。
|
|
219
|
+
|
|
220
|
+
保留它是为了**差分测试**:新旧两版对同一段程序给出不同结论时,
|
|
221
|
+
必须逐条归因——"新报旧不报"要么是新抓到的真漏报(好),要么是新误报(坏)。
|
|
222
|
+
"""
|
|
223
|
+
out = []
|
|
224
|
+
_check_struct_refs(tu_mod, out)
|
|
225
|
+
if ch_mod is None:
|
|
226
|
+
return out
|
|
227
|
+
for fn in descendants(ch_mod, "fn"):
|
|
228
|
+
body = _fn_body(fn)
|
|
229
|
+
if body is None:
|
|
230
|
+
continue
|
|
231
|
+
stmts = _flatten(body)
|
|
232
|
+
refs = []
|
|
233
|
+
for idx, st in enumerate(stmts):
|
|
234
|
+
if st.kind in ("const-decl", "var-decl") and st.children:
|
|
235
|
+
ty = st.children[0]
|
|
236
|
+
if getattr(ty, "kind", None) == "type-ref" and st.name:
|
|
237
|
+
base = _base_name(st.children[-1])
|
|
238
|
+
if base:
|
|
239
|
+
refs.append((st.name, base, idx))
|
|
240
|
+
|
|
241
|
+
def last_use(name, after):
|
|
242
|
+
best = after
|
|
243
|
+
for i in range(after + 1, len(stmts)):
|
|
244
|
+
if any(p.name == name for p in descendants(stmts[i], "path")):
|
|
245
|
+
best = i
|
|
246
|
+
return best
|
|
247
|
+
|
|
248
|
+
for ref_name, base, at in refs:
|
|
249
|
+
end = last_use(ref_name, at)
|
|
250
|
+
if end == at:
|
|
251
|
+
continue
|
|
252
|
+
for i in range(at + 1, end + 1):
|
|
253
|
+
st = stmts[i]
|
|
254
|
+
hit = False
|
|
255
|
+
if st.kind == "assign" and st.children:
|
|
256
|
+
lhs = st.children[0]
|
|
257
|
+
hit = getattr(lhs, "kind", None) == "path" and lhs.name == base
|
|
258
|
+
if not hit:
|
|
259
|
+
for mc in descendants(st, "method-call"):
|
|
260
|
+
if (mc.children and getattr(mc.children[0], "kind", None) == "path"
|
|
261
|
+
and mc.children[0].name == base
|
|
262
|
+
and (mc.name or "") in cfgmod.MUT_METHODS):
|
|
263
|
+
hit = True
|
|
264
|
+
break
|
|
265
|
+
if hit:
|
|
266
|
+
out.append((
|
|
267
|
+
"E-Borrow-Conflict",
|
|
268
|
+
f"`{base}` 在借用 `{ref_name}` 还活着的时候被修改",
|
|
269
|
+
st.line, st.col,
|
|
270
|
+
))
|
|
271
|
+
_check_ambiguous_returns(ch_mod, out)
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _flatten(block) -> list:
|
|
276
|
+
out = []
|
|
277
|
+
for st in block.children:
|
|
278
|
+
out.append(st)
|
|
279
|
+
for b in st.children:
|
|
280
|
+
if getattr(b, "kind", None) == "block":
|
|
281
|
+
out.extend(_flatten(b))
|
|
282
|
+
return out
|
package/src/bound.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""兼容层:真正的实现在 `cost_domain.bound`(2026-11-01 合并)。
|
|
3
|
+
|
|
4
|
+
**为什么用 `sys.modules` 替换,而不是把名字 `import *` 过来**:
|
|
5
|
+
`src/test_ops.py` 会 monkeypatch `bound.OP_COST` 造反例。
|
|
6
|
+
只有把本模块对象**换成真模块**,patch 才落在真正被读的那个全局上;
|
|
7
|
+
复制名字会让 patch 打在副本上,测试就变成"测了个寂寞"。
|
|
8
|
+
"""
|
|
9
|
+
import sys as _sys
|
|
10
|
+
|
|
11
|
+
from cost_domain import bound as _real
|
|
12
|
+
|
|
13
|
+
_sys.modules[__name__] = _real
|
package/src/cfg.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# Copyright 2026 Elench contributors
|
|
4
|
+
"""cfg.py —— 控制流图 + 活跃性分析(分支敏感借用分析的基础)
|
|
5
|
+
|
|
6
|
+
**为什么需要它**
|
|
7
|
+
|
|
8
|
+
`borrow.py` 早先的近似把函数体压成"一条时间线":按源码顺序取引用的最后一次使用。
|
|
9
|
+
它不懂分支,所以两类判定都不准:
|
|
10
|
+
|
|
11
|
+
| 类型 | 例子 | 后果 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **误报** | `if (c) { const u64& r = xs.at(0); xs.push(1); }` | 被迫改写法 |
|
|
14
|
+
| **漏报** | `const u64& r = xs.at(0); if (c) { xs.push(1); } return r;` | **交付 UB** |
|
|
15
|
+
|
|
16
|
+
**漏报更严重**,而时间线天然会漏——因为它不知道"这支改、那支不改"。
|
|
17
|
+
|
|
18
|
+
**这里实现的东西**
|
|
19
|
+
|
|
20
|
+
1. **CFG**:节点=语句,边=顺序 / `if` 两支 / `while` 与 `for` 的回边 / `match` 各 arm /
|
|
21
|
+
`return` 与 `break`/`continue` 的跳转;
|
|
22
|
+
2. **def / use / mut**:每个节点写什么、读什么、改谁;
|
|
23
|
+
3. **活跃性不动点**:`live_out[n] = ∪ live_in[succ]`,`live_in[n] = use[n] ∪ (live_out[n] − def[n])`。
|
|
24
|
+
|
|
25
|
+
**这就是 Rust MIR borrowck 的路子(基于 liveness 的 NLL),只是我们做在 AST 上。**
|
|
26
|
+
"""
|
|
27
|
+
from typing import Dict, List, Optional, Set
|
|
28
|
+
|
|
29
|
+
from ast_util import descendants
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CfgNode:
|
|
33
|
+
"""CFG 节点。`st` 是它对应的 AST 语句(合成节点为 None)。"""
|
|
34
|
+
|
|
35
|
+
__slots__ = ("id", "st", "kind", "succ", "uses", "defs", "muts")
|
|
36
|
+
|
|
37
|
+
def __init__(self, nid: int, st=None, kind: str = "stmt"):
|
|
38
|
+
self.id = nid
|
|
39
|
+
self.st = st
|
|
40
|
+
self.kind = kind
|
|
41
|
+
self.succ: List[int] = []
|
|
42
|
+
self.uses: Set[str] = set()
|
|
43
|
+
self.defs: Set[str] = set()
|
|
44
|
+
self.muts: Set[str] = set()
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return f"<{self.id} {self.kind} {getattr(self.st, 'kind', '')}>"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#: 会**改容器内容**的方法。漏一个,借用检查就在那个方法上失效
|
|
51
|
+
#: (2026-11-06 补:`fill` / `extend` / `enqueue` / `dequeue` 都曾漏掉)。
|
|
52
|
+
MUT_METHODS = {"push", "pop", "clear", "set", "sort", "sort_by",
|
|
53
|
+
"reverse", "resize", "insert", "remove",
|
|
54
|
+
"fill", "extend", "enqueue", "dequeue"}
|
|
55
|
+
|
|
56
|
+
#: 会改**第一个实参**的内建函数(不是方法,所以方法扫描抓不到)。
|
|
57
|
+
MUT_FUNCS = {"swap"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _paths(node) -> Set[str]:
|
|
61
|
+
return {p.name for p in descendants(node, "path") if p.name}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _base_of(node) -> Optional[str]:
|
|
65
|
+
for p in descendants(node, "path"):
|
|
66
|
+
if p.name:
|
|
67
|
+
return p.name
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Cfg:
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self.nodes: Dict[int, CfgNode] = {}
|
|
74
|
+
self.entry: Optional[int] = None
|
|
75
|
+
|
|
76
|
+
def new(self, st=None, kind: str = "stmt") -> CfgNode:
|
|
77
|
+
n = CfgNode(len(self.nodes), st, kind)
|
|
78
|
+
self.nodes[n.id] = n
|
|
79
|
+
return n
|
|
80
|
+
|
|
81
|
+
def link(self, a: int, b: Optional[int]) -> None:
|
|
82
|
+
if b is None:
|
|
83
|
+
return
|
|
84
|
+
if b not in self.nodes[a].succ:
|
|
85
|
+
self.nodes[a].succ.append(b)
|
|
86
|
+
|
|
87
|
+
# ---------------- 构建 ----------------
|
|
88
|
+
|
|
89
|
+
def build_block(self, block, succ: Optional[int], loop_ctx=None) -> Optional[int]:
|
|
90
|
+
"""从后往前建,返回入口节点 id。`loop_ctx` = (cond_id, exit_node)。"""
|
|
91
|
+
cur = succ
|
|
92
|
+
stmts = list(block.children)
|
|
93
|
+
for st in reversed(stmts):
|
|
94
|
+
cur = self.build_stmt(st, cur, loop_ctx)
|
|
95
|
+
return cur
|
|
96
|
+
|
|
97
|
+
def build_stmt(self, st, succ: Optional[int], loop_ctx=None) -> Optional[int]:
|
|
98
|
+
k = getattr(st, "kind", None)
|
|
99
|
+
|
|
100
|
+
if k == "block":
|
|
101
|
+
return self.build_block(st, succ, loop_ctx)
|
|
102
|
+
|
|
103
|
+
if k == "if":
|
|
104
|
+
cond = self.new(st, "cond")
|
|
105
|
+
cond.uses |= _paths(st.children[0]) if st.children else set()
|
|
106
|
+
branches = [c for c in st.children[1:] if getattr(c, "kind", None) == "block"]
|
|
107
|
+
for b in branches:
|
|
108
|
+
entry = self.build_block(b, succ, loop_ctx)
|
|
109
|
+
self.link(cond.id, entry if entry is not None else succ)
|
|
110
|
+
if not branches:
|
|
111
|
+
self.link(cond.id, succ)
|
|
112
|
+
return cond.id
|
|
113
|
+
|
|
114
|
+
if k in ("while", "for"):
|
|
115
|
+
cond = self.new(st, "cond")
|
|
116
|
+
cond.uses |= _paths(st.children[0]) if (k == "while" and st.children) else set()
|
|
117
|
+
body = next((c for c in st.children if getattr(c, "kind", None) == "block"), None)
|
|
118
|
+
exit_node = None
|
|
119
|
+
ctx = (cond.id, exit_node)
|
|
120
|
+
entry = self.build_block(body, cond.id, ctx) if body is not None else None
|
|
121
|
+
self.link(cond.id, entry if entry is not None else succ)
|
|
122
|
+
self.link(cond.id, succ) # 条件为假 → 退出
|
|
123
|
+
return cond.id
|
|
124
|
+
|
|
125
|
+
if k == "return":
|
|
126
|
+
n = self.new(st, "return")
|
|
127
|
+
n.uses |= _paths(st)
|
|
128
|
+
return n.id # 无后继
|
|
129
|
+
|
|
130
|
+
if k in ("break", "continue"):
|
|
131
|
+
n = self.new(st, "jump")
|
|
132
|
+
if loop_ctx is not None:
|
|
133
|
+
target = loop_ctx[0] if k == "continue" else loop_ctx[1]
|
|
134
|
+
self.link(n.id, target)
|
|
135
|
+
return n.id
|
|
136
|
+
|
|
137
|
+
if k == "match":
|
|
138
|
+
n = self.new(st, "match")
|
|
139
|
+
n.uses |= _paths(st.children[0]) if st.children else set()
|
|
140
|
+
arms = [c for c in st.children if getattr(c, "kind", None) == "arm"]
|
|
141
|
+
for a in arms:
|
|
142
|
+
for b in a.children:
|
|
143
|
+
if getattr(b, "kind", None) == "block":
|
|
144
|
+
entry = self.build_block(b, succ, loop_ctx)
|
|
145
|
+
self.link(n.id, entry if entry is not None else succ)
|
|
146
|
+
if not arms:
|
|
147
|
+
self.link(n.id, succ)
|
|
148
|
+
return n.id
|
|
149
|
+
|
|
150
|
+
# 普通语句
|
|
151
|
+
n = self.new(st, "stmt")
|
|
152
|
+
if k in ("const-decl", "var-decl") and st.children:
|
|
153
|
+
n.uses |= _paths(st.children[-1])
|
|
154
|
+
if st.name:
|
|
155
|
+
n.defs.add(st.name)
|
|
156
|
+
elif k in ("assign", "assign-op") and st.children:
|
|
157
|
+
lhs = st.children[0]
|
|
158
|
+
n.uses |= _paths(st)
|
|
159
|
+
base = _base_of(lhs)
|
|
160
|
+
if base:
|
|
161
|
+
n.defs.add(base)
|
|
162
|
+
n.muts.add(base)
|
|
163
|
+
else:
|
|
164
|
+
n.uses |= _paths(st)
|
|
165
|
+
for mc in descendants(st, "method-call"):
|
|
166
|
+
if (mc.name or "") not in MUT_METHODS or not mc.children:
|
|
167
|
+
continue
|
|
168
|
+
# **变异归到根变量**(2026-11-06 修):以前只认 `children[0]` 是 path,
|
|
169
|
+
# 于是 `s.items.push(9)` 记不上——接收者是 `field` 节点,条件不成立,
|
|
170
|
+
# **整个字段上的写入都被漏掉**。用 `_base_of` 取根(`s.items` → `s`),
|
|
171
|
+
# 方向保守:改字段就算改结构体。
|
|
172
|
+
base = _base_of(mc.children[0])
|
|
173
|
+
if base:
|
|
174
|
+
n.muts.add(base)
|
|
175
|
+
n.uses.add(base)
|
|
176
|
+
# **内建函数也会改第一个实参**(`swap(xs, i, j)`)——方法扫描抓不到它,
|
|
177
|
+
# 于是"借用还活着就换元素"以前是漏报的。
|
|
178
|
+
for call in descendants(st, "call"):
|
|
179
|
+
if (call.name or "") not in MUT_FUNCS:
|
|
180
|
+
continue
|
|
181
|
+
args = next((c for c in (call.children or []) if c.kind == "args"), None)
|
|
182
|
+
if args is None or not args.children:
|
|
183
|
+
continue
|
|
184
|
+
base = _base_of(args.children[0])
|
|
185
|
+
if base:
|
|
186
|
+
n.muts.add(base)
|
|
187
|
+
n.uses.add(base)
|
|
188
|
+
self.link(n.id, succ)
|
|
189
|
+
return n.id
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build(block) -> Cfg:
|
|
193
|
+
c = Cfg()
|
|
194
|
+
entry = c.build_block(block, None)
|
|
195
|
+
c.entry = entry
|
|
196
|
+
return c
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def liveness(cfg: Cfg) -> Dict[int, Set[str]]:
|
|
200
|
+
"""返回 live_in:节点 id → 该点"之后还会用到"的变量集合(不动点)。"""
|
|
201
|
+
live_in: Dict[int, Set[str]] = {i: set() for i in cfg.nodes}
|
|
202
|
+
live_out: Dict[int, Set[str]] = {i: set() for i in cfg.nodes}
|
|
203
|
+
changed = True
|
|
204
|
+
rounds = 0
|
|
205
|
+
while changed and rounds < 200: # 上界防死循环:图是有限的
|
|
206
|
+
changed = False
|
|
207
|
+
rounds += 1
|
|
208
|
+
for i, n in cfg.nodes.items():
|
|
209
|
+
out = set()
|
|
210
|
+
for s in n.succ:
|
|
211
|
+
out |= live_in.get(s, set())
|
|
212
|
+
if out != live_out[i]:
|
|
213
|
+
live_out[i] = out
|
|
214
|
+
changed = True
|
|
215
|
+
new_in = n.uses | (out - n.defs)
|
|
216
|
+
if new_in != live_in[i]:
|
|
217
|
+
live_in[i] = new_in
|
|
218
|
+
changed = True
|
|
219
|
+
return live_in
|