Skip to content

Commit f3944ba

Browse files
watney1024k1nsom
authored andcommitted
docs(phase1+2): extension exercises for both topics
- CFG exercises: unreachable block deletion, nested loop coloring, post-dominator tree - Peephole exercises: mul->slli, rem->andi, instruction cost model - Each exercise has problem statement, expected behavior, hints, and self-check test case - No solutions provided, exercises only
1 parent e129aa5 commit f3944ba

2 files changed

Lines changed: 464 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# 03 扩展练习
2+
3+
> 这三个练习建立在 demos 01-06 的基础上。每个练习预期 30-60 分钟完成。
4+
> 练习中没有给出完整代码实现,只有思路引导。建议先自己试,卡住再看提示。
5+
6+
---
7+
8+
## 练习 1: 不可达块删除(⭐)
9+
10+
**难度**: ⭐
11+
12+
**问题描述**: 目前 `demo04_visualize.py` 中的 `find_unreachable()` 只能**检测**不可达块——在 DOT 输出中把它们标记为灰色虚线框。但不可达块仍然留在 CFG 数据结构中,后续分析(支配树、循环检测)还是要处理它们。
13+
14+
你需要在 `demo02_build_cfg.py` 或新建的 `demo07_cleanup.py` 中实现一个函数 `remove_unreachable(cfg) -> CFG`,它真正从 CFG 中删除不可达的块和相关的边:
15+
16+
1. 调用 `find_unreachable()` 找出所有不可达块
17+
2. 从 `cfg.blocks` 中删除这些块
18+
3. 从剩余所有块的 `exits_to` 列表中,删除指向已删除块的边
19+
4. 如果某个块删除所有边后变成孤块(没有边指向任何存在的块)且不是出口块?考虑这种情况是否可能发生,以及如何处理
20+
21+
**预期行为**:
22+
23+
```
24+
# 输入:含不可达块的 CFG(unreachable 程序)
25+
Block: entry → ret, exits: []
26+
Block: dead → ret, exits: [] (UNREACHABLE)
27+
28+
# 调用 remove_unreachable(cfg) 后
29+
Block: entry → ret, exits: []
30+
# dead 块已消失
31+
```
32+
33+
- 对 `if_else` 程序调用 `remove_unreachable`,结果应该和原 CFG 完全一样(因为没有不可达块)
34+
- `reachable_from()` 的返回值应该等于 `cfg.blocks` 的 keys 集合
35+
36+
**提示**:
37+
38+
- 提示 1:先调用 `find_unreachable()`(在 `demo04_visualize.py` 里已经有了),拿到不可达块名字的集合
39+
- 提示 2:删除块之后,需要遍历所有剩余块的 `exits_to`,移除指向已删除块的 `(target, edge_label)` 元组
40+
- 提示 3:删除边之后,可能会出现新的不可达块吗?如果会,你可能需要迭代删除直到固定点。想想什么场景下会出现这种情况
41+
- 提示 4:复制 CFG 再修改,不要原地修改——这样方便对比删除前后的差异
42+
43+
**自检用例**:
44+
45+
```python
46+
from toy_cfg.demo01_build_blocks import DEMOS
47+
from toy_cfg.demo02_build_cfg import build_cfg
48+
from toy_cfg.demo04_visualize import find_unreachable
49+
50+
# 1. unreachable 程序:删除前后对比
51+
func = DEMOS["unreachable"].functions[0]
52+
cfg = build_cfg(func)
53+
print("Before:", list(cfg.blocks.keys())) # ["entry", "dead"]
54+
cleaned = remove_unreachable(cfg)
55+
print("After:", list(cleaned.blocks.keys())) # ["entry"] — dead 已删除
56+
assert cleaned.blocks["entry"].exits_to == [] # entry 的边不变
57+
58+
# 2. if_else 程序:没有不可达块,删除后不变
59+
cfg2 = build_cfg(DEMOS["if_else"].functions[0])
60+
cleaned2 = remove_unreachable(cfg2)
61+
assert set(cleaned2.blocks.keys()) == set(cfg2.blocks.keys()) # 完全一致
62+
63+
# 3. 验证所有剩余块的 exits_to 不再指向已删除的块
64+
for name, block in cleaned.blocks.items():
65+
if block.exits_to:
66+
for target, _ in block.exits_to:
67+
assert target in cleaned.blocks # 没有悬空引用
68+
```
69+
70+
---
71+
72+
## 练习 2: 嵌套循环深度高亮(⭐⭐)
73+
74+
**难度**: ⭐⭐
75+
76+
**问题描述**: `demo06_full.py` 已经能检测到嵌套循环——`demo_nested_for()` 会产生两个循环。但 DOT 可视化中,所有循环头(loop header)都被涂成一样的 `lightskyblue`,看不出哪层循环是外层、哪层是内层。
77+
78+
你的任务是增强 `cfg_to_dot_highlight()` 函数,让 DOT 输出根据循环嵌套深度使用不同深浅的蓝色:
79+
80+
- 深度 1(最外层)→ `lightskyblue`
81+
- 深度 2 → `skyblue`
82+
- 深度 3 → `deepskyblue`
83+
- 深度 4+ → `dodgerblue`
84+
85+
你需要先计算每个块的循环嵌套深度。一个块如果在 N 个循环的 body 里,它的嵌套深度就是 N。循环头本身也算在它自己的循环 body 里。
86+
87+
**预期行为**:
88+
89+
```
90+
nested_for 程序的 DOT 输出:
91+
- entry(外层 FOR)→ lightskyblue(深度 1)
92+
- outer_body → ...
93+
- inner_entry(内层 FOR)→ skyblue(深度 2)
94+
- inner_body(同时属于两个循环)→ skyblue(深度 2)
95+
- outer_end → ...
96+
- end → white(不在任何循环中)
97+
98+
非循环程序(if_else)的所有块不受影响,颜色和原来一样。
99+
```
100+
101+
**提示**:
102+
103+
- 提示 1:`demo05_dominators.py` 中的 `find_loops()` 返回一个 `list[tuple[str, set[str]]]`,每个元素是 `(header, body_set)`。需要从中算出每个块在多少个循环的 body_set 里
104+
- 提示 2:遍历所有已检测到的循环,对每个循环 body 里的所有块,嵌套深度加 1。可以用一个 `defaultdict(int)` 来累加
105+
- 提示 3:`cfg_to_dot_highlight()` 中目前用 `if is_loop_header: color = "lightskyblue"`。修改为根据嵌套深度选择颜色深浅
106+
- 提示 4:定义一个深度到颜色的映射表,比如 `DEPTH_COLORS = {1: "lightskyblue", 2: "skyblue", 3: "deepskyblue", 4: "dodgerblue"}`。超过 4 的统一用 `dodgerblue`
107+
108+
**自检用例**:
109+
110+
```python
111+
from toy_cfg.demo01_build_blocks import DEMOS
112+
from toy_cfg.demo02_build_cfg import build_cfg
113+
from toy_cfg.demo05_dominators import compute_dominators, find_loops
114+
from toy_cfg.demo06_full import cfg_to_dot_highlight
115+
116+
# nested_for 程序
117+
func = DEMOS["nested_for"].functions[0]
118+
cfg = build_cfg(func)
119+
dom = compute_dominators(cfg)
120+
loops = find_loops(cfg, dom)
121+
print(f"Detected {len(loops)} loops") # 应该是 2
122+
123+
# 计算嵌套深度
124+
depths = compute_nest_depth(cfg, loops) # 你实现的函数
125+
assert depths.get("entry") == 1 # 外层循环 header
126+
assert depths.get("inner_entry") == 2 # 内层循环 header(深度 2)
127+
assert depths.get("inner_body") == 2 # 同时属于两个循环
128+
assert depths.get("end") == 0 # 不在循环中
129+
130+
# DOT 中深度 2 的块应该是 skyblue(不是 lightskyblue)
131+
dot = cfg_to_dot_highlight(cfg, loop_headers={h for h, _ in loops})
132+
assert 'fillcolor=lightskyblue' in dot # 深度 1
133+
assert 'fillcolor=skyblue' in dot # 深度 2
134+
# 具体颜色可能因你的实现而异,关键是不同的深度用不同的颜色
135+
```
136+
137+
---
138+
139+
## 练习 3: 后支配树(Post-Dominator Tree)(⭐⭐⭐)
140+
141+
**难度**: ⭐⭐⭐
142+
143+
**问题描述**: 支配树(Dominator Tree)回答"从入口出发,哪些块是必经之路"。后支配树(Post-Dominator Tree)回答相反的问题:"**到出口为止**,哪些块是必经之路"。形式上,如果从 B 到**任意出口**(出度为 0 的块)的所有路径都必须经过 A,则 A 后支配(post-dominate)B。
144+
145+
后支配树在以下场景中非常有用:
146+
- **循环出口识别**:循环的"后支配边"可以帮助找到循环的单一出口
147+
- **控制流归约**:识别 if-else 结构中的汇合点
148+
- **死代码消除**:如果一个语句的后支配者是一个不可达块,那这个语句也可能是不可达的
149+
150+
你的任务是实现 `compute_post_dominators(cfg) -> dict[str, set[str]]`,使用和 `demo05_dominators.py` 中 `compute_dominators()` 相同的 CHK 迭代算法,但作用在**反向 CFG** 上。
151+
152+
实现步骤:
153+
1. 构建反向 CFG(revCFG):把原 CFG 的所有边反向,原出口变成入口
154+
2. 如果有多个出口(比如 if-else 的两个分支都 return),需要添加一个虚拟的"exit"节点,让所有真实出口都指向它
155+
3. 在反向 CFG 上运行 CHK 算法计算支配关系——结果就是原 CFG 的后支配关系
156+
4. 实现 `compute_post_idom(cfg, post_dom_sets) -> dict[str, str | None]` 计算立即后支配者
157+
158+
**预期行为**:
159+
160+
```
161+
if_else 程序:
162+
entry: post-dom = {entry, end} (从入口到出口,必须经过 end)
163+
then: post-dom = {then, end} (从 then 到出口,必须经过 end)
164+
else: post-dom = {else, end}
165+
end: post-dom = {end} (出口后支配自身)
166+
167+
while_loop 程序:
168+
entry: post-dom = {entry, end}
169+
body: post-dom = {body, end} (注意 body 的后支配者不包含 entry)
170+
end: post-dom = {end}
171+
```
172+
173+
**提示**:
174+
175+
- 提示 1:反向 CFG 不需要真的改数据结构。你可以先收集所有块的入边(`_compute_predecessors` 已经做了这个),然后对反向图做支配计算时,"前驱"在原反向图中就是后继。更简单的方法:构造一个"虚拟"的 CFG 对象,它的 `blocks` 不变但边全反向
176+
- 提示 2:多出口问题:`compute_dominators()` 假定只有一个入口。反向 CFG 如果有多个出口就会变成多个入口。解决方法是添加一个虚拟的 exit 块,让所有真实出口(出度为 0 的块)都有一条边指向它
177+
- 提示 3:实现时可以先创建反向邻接表。原图中 `block.exits_to` 的每条 `(target, label)` 在反向图中变成一条 `target → name` 的边
178+
- 提示 4:用于调试:如果 `entry` 的后支配集合包含 `end`,说明所有路径都必须经过 `end` 才能退出——这是正确的。但如果 `entry` 的后支配集合只有 `entry` 和 `end`,说明存在不经过其他块也能到出口的路径
179+
180+
**自检用例**:
181+
182+
```python
183+
from toy_cfg.demo01_build_blocks import DEMOS
184+
from toy_cfg.demo02_build_cfg import build_cfg
185+
from toy_cfg.demo05_dominators import compute_dominators
186+
187+
# if_else 程序
188+
cfg = build_cfg(DEMOS["if_else"].functions[0])
189+
post_dom = compute_post_dominators(cfg) # 你实现的函数
190+
post_idom = compute_post_idom(cfg, post_dom)
191+
192+
# end 块后支配所有块(因为所有路径都要经过 end 才能到出口)
193+
for name in cfg.blocks:
194+
assert "end" in post_dom[name], f"{name} 应该被 end 后支配"
195+
196+
# entry 不被 then 或 else 后支配(因为走另一条分支就不经过它们)
197+
assert "then" not in post_dom["entry"]
198+
assert "else" not in post_dom["entry"]
199+
200+
# 立即后支配:end 的 post_idom 是 None(它是虚拟出口)
201+
# then 的 post_idom 应该是 end
202+
assert post_idom["then"] == "end"
203+
assert post_idom["else"] == "end"
204+
```
205+
206+
---
207+
208+
## 延伸思考
209+
210+
做完以上三个练习后,可以继续挑战:
211+
212+
1. **支配边界(Dominance Frontier)**:结合支配树和后支配树,计算每个块的支配边界。这是构建 SSA 形式的基石
213+
2. **循环树(Loop Tree)**:根据循环嵌套关系构建一棵树——每个循环是一个节点,父子关系表示包含关系。这对优化 pass 的排序很有用
214+
3. **控制流归约(Control-Flow Reduction)**:把 CFG 归约为结构化控制流(if-then-else, while-loop),用于反编译或程序理解

0 commit comments

Comments
 (0)