@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.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +53 -0
  3. package/README.md +298 -2
  4. package/bin/elench.js +69 -0
  5. package/package.json +37 -4
  6. package/scripts/clean-pycache.js +37 -0
  7. package/scripts/npm_smoke.js +88 -0
  8. package/src/README-/345/205/274/345/256/271/345/261/202.md +26 -0
  9. package/src/ast_util.py +111 -0
  10. package/src/bench.py +376 -0
  11. package/src/borrow.py +282 -0
  12. package/src/bound.py +13 -0
  13. package/src/cfg.py +219 -0
  14. package/src/chart.py +209 -0
  15. package/src/check_docs.py +156 -0
  16. package/src/checker.py +2473 -0
  17. package/src/contract_metric.py +87 -0
  18. package/src/cost_check.py +13 -0
  19. package/src/cost_diff.py +533 -0
  20. package/src/cost_domain/__init__.py +32 -0
  21. package/src/cost_domain/bound.py +739 -0
  22. package/src/cost_domain/cost_check.py +394 -0
  23. package/src/cost_domain/cost_interp.py +1998 -0
  24. package/src/cost_domain/symbolic.py +554 -0
  25. package/src/cost_explain.py +503 -0
  26. package/src/cost_interp.py +13 -0
  27. package/src/cxx_map.py +184 -0
  28. package/src/dispatch.py +314 -0
  29. package/src/dsh.py +3980 -0
  30. package/src/dup_scan.py +180 -0
  31. package/src/elench.py +15 -0
  32. package/src/emit.py +56 -0
  33. package/src/ffi_c.py +70 -0
  34. package/src/fmt.py +115 -0
  35. package/src/formatter.py +596 -0
  36. package/src/grammar.py +272 -0
  37. package/src/guarantees.py +116 -0
  38. package/src/highlight.py +156 -0
  39. package/src/interp.py +2119 -0
  40. package/src/isolated.py +161 -0
  41. package/src/jsonval.py +236 -0
  42. package/src/kappa_vs_measured.py +347 -0
  43. package/src/kinds.py +122 -0
  44. package/src/lexer.py +559 -0
  45. package/src/migrate_docs.py +115 -0
  46. package/src/otel.py +579 -0
  47. package/src/otel_dashboard.py +218 -0
  48. package/src/otel_export.py +380 -0
  49. package/src/otel_local_sink.py +141 -0
  50. package/src/otel_view.py +156 -0
  51. package/src/parser.py +1909 -0
  52. package/src/parser.py.bak +1781 -0
  53. package/src/policy.py +271 -0
  54. package/src/rename_ch.py +81 -0
  55. package/src/requires.py +242 -0
  56. package/src/segment_plan.py +150 -0
  57. package/src/shape.py +130 -0
  58. package/src/stdlib.py +1856 -0
  59. package/src/stdlib.py.bak +1751 -0
  60. package/src/symbolic.py +13 -0
  61. package/src/sync_docs.py +225 -0
  62. package/src/test_checker.py.bak +443 -0
  63. package/src/test_formatter.py.bak +201 -0
  64. package/src/test_invalid.py.bak +342 -0
  65. package/src/test_parser.py.bak +423 -0
  66. package/src/typecheck.py +865 -0
  67. package/src/typecheck.py.bak +741 -0
  68. package/src/typenames.py +56 -0
  69. 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