循环改写

节点图不允许控制流成环(控制流),但 MIR 里循环就是环。所以 lower 在优化之后跑一遍 lower_loops(core/src/compile/loops.rs),把 IR 里的环改写成原生循环节点。这一步面对的是IR 图,不是 MIR。MIR 里的 for i in a..b 是 Range::next 迭代器调用,形状和计数循环差得远;到 IR 阶段,迭代器已经被内联成「比较 + 分支 + 加一」的环,能直接看出语义。

IR 里的环长什么样

   初值 i = begin
        │
        ▼
 ┌→ 头:switch (i < bound)      ←── 回边(continue / 一轮结束)
 │      ├─ false → 跳出
 │      └─ true  → 体
 │                 │
 │                 ├─ i += 1 ────┘
 │                 └─ …其它分支…
 └──(头有两条控制入边:循环前进入 + 回边)
局部变量在 IR 里是两个节点:Local(值出口 0 是写句柄、出口 1 是读值)和 SetLocal(值入 0 接写句柄、入 1 接写入的值)。所以「给 i 赋值」在图上就是一个 SetLocal。

区域分解

analyze 走可约流图的标准流程:
  1. DFS 找回边(to 是 from 的祖先)
  2. 支配集合判定可约性 —— 回边的头必须支配尾,否则报「不可约的控制流」
  3. 回边圈出的自然环 = 一个区域
  4. 区域按包含关系组成树
一个区域对应一个 Rust 循环。嵌套循环就是嵌套区域。为什么要求可约:native 节点图里一个循环只能有一个入口、一个退出点(有限循环 节点的一对控制脚)。多入口的环没法用一个节点表达,只能报错。

三种边

classify 把每条控制边分成四类:
分类处理
Keep区域内部的边,不动
Drop回边 / continue:删掉。一轮走到头链就断了,运行时重新驱动 out[0]
Entry区域入口:改接到有限循环的 in[0]
Exit退出:改接到「置码 + 跳出」链
「一轮结束」不是显式节点:链走到没有后继的地方,运行时就从 out[0] 再驱动一轮。

退出路径

只有一个退出目标时不用分派:跳出节点(ID 6)直接接有限循环的 in[1],out[1] 直接连那个目标。多个退出目标时,每个区域配一个 int 局部变量当退出码,循环之后接一个 switch 按码分派。每个跳出点先 set(码) 再 break。跨层的跳出(内层循环里 return)靠串联:内层分派到「设置外层的码并跳出外层」,一层层往上抛。

计数循环

不是所有循环都只靠条件跳出。认出「i 从 begin 数到 bound,每轮加一」的形状时,有限循环节点会带上真的区间,循环自己数到头就结束 —— 条件比较和分支整个摘掉。识别在 detect_counting_loop,条件都在 IR 图上:
  1. 区域的头是 Bool 的 switch,值来自 <(230)或 <=(231)比较节点
  2. 比较的第一个操作数是局部变量 i,第二个是上界
  3. i 全图只被写两次:循环外一次(初值,且支配循环头)、循环内一次
  4. 循环内那次写的是 i + 1(允许经一层拷贝)
  5. 每一条回到头的边都被加一支配 —— 也就是「连回循环开始的边都在 i += 1 之后」
  6. 上界在循环里不变(不依赖 i,也不依赖循环里被改的其它局部变量)

消掉循环变量之前

特化会把「循环体里读循环变量」改接成有限循环的 out[0](当前值)。这一步只有在每一处读取拿到的都还是当前值时才成立,否则差一。for ... in range 的形状里,用户的 i 是每轮顶部从计数器拷出来的那一份,拷的是自增之前的值 —— 正好就是当前值。但:while 形状里,体里读的就是计数器本身:读取的位置怎么定:局部变量的读值出口上挂着的就是消费者;纯值节点(加法、调用参数之类)自己不在控制流上,位置要看它最终喂给了哪个控制节点。

循环头的两种来源

分支节点的 case 列表有两种约定,方向相反:两种都得认 —— 只认一种会漏掉一半的循环头。

continue 怎么办

回到头的边可以有多条:continue 一条、正常路径一条。但每一条都得被加一支配,才说明「连回循环开始的边都在 i += 1 之后」,每轮恰好加一次,native 的计数才跟得上循环变量。rustc 常把 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]
摘掉的东西:折叠拷贝有条件:写入的值必须是 out[0]、该局部变量全图只被这一处写、它的读取者不能逃出本区域(区域外读到的是循环结束后的值,语义不同)、读取者都得在这次写入之后(否则读到的是上一轮的那份)。SetLocal 在控制流上,摘除时要用「前驱直接接后继」的旁路,不能直接删。

认不出来的循环

不满足上面条件的仍走通用改写:有限循环区间填满 i32::MIN ..= i32::MAX,靠条件跳出结束。out[0] 接的仍然是原来的头节点(比较 + 分支原样保留)。协程状态机里的循环(循环变量是协程字段)、比较不在区域头的循环属于这一类。「正常结束」可以是函数出口 —— 循环是函数体的最后一句时,条件不成立就直接走函数出口。代价是循环体里每轮都要算一次条件,但语义完全一样。