循环改写
lower 在优化之后跑一遍 lower_loops(core/src/compile/loops.rs),把 IR 里的环改写成原生循环节点。for i in a..b 是 Range::next 迭代器调用,形状和计数循环差得远;到 IR 阶段,迭代器已经被内联成「比较 + 分支 + 加一」的环,能直接看出语义。IR 里的环长什么样
初值 i = begin
│
▼
┌→ 头:switch (i < bound) ←── 回边(continue / 一轮结束)
│ ├─ false → 跳出
│ └─ true → 体
│ │
│ ├─ i += 1 ────┘
│ └─ …其它分支…
└──(头有两条控制入边:循环前进入 + 回边)Local(值出口 0 是写句柄、出口 1 是读值)和 SetLocal(值入 0 接写句柄、入 1 接写入的值)。所以「给 i 赋值」在图上就是一个 SetLocal。区域分解
analyze 走可约流图的标准流程:- DFS 找回边(
to是from的祖先) - 支配集合判定可约性 —— 回边的头必须支配尾,否则报「不可约的控制流」
- 回边圈出的自然环 = 一个区域
- 区域按包含关系组成树
有限循环 节点的一对控制脚)。多入口的环没法用一个节点表达,只能报错。三种边
classify 把每条控制边分成四类:| 分类 | 处理 |
Keep | 区域内部的边,不动 |
Drop | 回边 / continue:删掉。一轮走到头链就断了,运行时重新驱动 out[0] |
Entry | 区域入口:改接到有限循环的 in[0] |
Exit | 退出:改接到「置码 + 跳出」链 |
out[0] 再驱动一轮。退出路径
in[1],out[1] 直接连那个目标。set(码) 再 break。跨层的跳出(内层循环里 return)靠串联:内层分派到「设置外层的码并跳出外层」,一层层往上抛。计数循环
i 从 begin 数到 bound,每轮加一」的形状时,有限循环节点会带上真的区间,循环自己数到头就结束 —— 条件比较和分支整个摘掉。detect_counting_loop,条件都在 IR 图上:- 区域的头是
Bool的 switch,值来自<(230)或<=(231)比较节点 - 比较的第一个操作数是局部变量
i,第二个是上界 i全图只被写两次:循环外一次(初值,且支配循环头)、循环内一次- 循环内那次写的是
i + 1(允许经一层拷贝) - 每一条回到头的边都被加一支配 —— 也就是「连回循环开始的边都在
i += 1之后」 - 上界在循环里不变(不依赖
i,也不依赖循环里被改的其它局部变量)
消掉循环变量之前
out[0](当前值)。这一步只有在每一处读取拿到的都还是当前值时才成立,否则差一。for ... in range 的形状里,用户的 i 是每轮顶部从计数器拷出来的那一份,拷的是自增之前的值 —— 正好就是当前值。但:- 那份拷贝全图只许被这一处写。别的地方改了它,消掉的就是错的值;
- 读取者都得在区域内、且都在这次拷贝之后。区域外读到的是循环结束后的那份(差一),拷贝之前读到的还是上一轮的。
while 形状里,体里读的就是计数器本身:- 自增之后才读到的,是自增后的值 —— 不是当前值,不能特化;
- 循环体外读计数器时,退出那一刻的值也得是当前值:出口点要是自增之后才走的(自增之后
break/return),退出时它已经加过了。
循环头的两种来源
- MIR 的
switchInt(bool):case 是[false],所以 false 在第 0 支、true 在 default 支 if语句来的:case 是[true],所以 true 在第 0 支、false 在 default 支
continue 怎么办
continue 一条、正常路径一条。但每一条都得被加一支配,才说明「连回循环开始的边都在 i += 1 之后」,每轮恰好加一次,native 的计数才跟得上循环变量。for 的自增提到体首(continue 跳的是旧值的副本),所以加一在体首还是体尾都行,只要每条回边都在它后面。| Rust | 有限循环区间 |
for i in a..b / while i < b | [a, b - 1] |
i <= b | [a, b] |
b 是常量时直接算;是变量时插入一个减法节点算 b - 1 并接上去。有别的 break 时
break、return)时,正常结束也算一个退出目标:进循环前先把退出码设成正常结束那个码。循环自然跑完时码没变,分派 switch 就落到正常结束那支,不用落到 default。out[1]。认出之后长什么样
有限循环 [begin, end]
├─ out[0](当前值)→ 循环体
├─ out[1] → 循环之后(原来的 false 分支)
└─ 循环体里读 `i` 的地方 → 直接连 out[0]- 比较和分支:区间已经说明了起止,那个条件恒真;
- 循环变量那一套:初值写入、每轮的
i += 1(含+节点)、局部变量本身。有限循环节点的出参就是当前循环值,循环体直接读它; for ... in range的拷贝:MIR 常把迭代变量在体里再拷一份给业务代码读,那种「每轮写一次临时变量」也会被折叠 —— 值直接改接到 out[0],写入节点摘掉。
SetLocal 在控制流上,摘除时要用「前驱直接接后继」的旁路,不能直接删。认不出来的循环
i32::MIN ..= i32::MAX,靠条件跳出结束。out[0] 接的仍然是原来的头节点(比较 + 分支原样保留)。协程状态机里的循环(循环变量是协程字段)、比较不在区域头的循环属于这一类。