11.4 IR 中的函数调用与返回
一次源语言调用组合了多项契约:求值实参、转换到形参类型、转移控制、保存规定的机器状态,并接收返回值或异常出口。IR 要把这些义务表达得足够明确,供后续阶段降低到调用约定(calling convention)。
在目标无关层,调用可能写成 %r = call i32 @mix(i32 %a, i32 %b);更低层的 IR 会把参数分配到寄存器或对齐的栈槽,附加调用约定元数据,并描述调用可能修改的内存或寄存器。
函数类型是 call 指令的一部分,而不仅是给人阅读的注释。它告诉 verifier 需要多少参数、哪些转换合法、是否存在返回值,以及一个间接函数值能否按该签名调用。若把 i32(i32, i32) 当成单参数函数调用,或把它的结果当作 f64 使用,IR 应立即被判为非法。
按源语义组装调用包
参数降低应把语义顺序和物理放置分开:
1. 按语言规定的顺序求值实参;
2. 执行隐式类型转换,保存每个结果 IR 值;
3. 再依据函数类型与应用二进制接口(Application Binary Interface,ABI)放入寄存器或栈;
4. 发射调用,并在存在返回值时接收结果。
这种分离能避免寄存器分配意外重排副作用。变参调用、结构体返回、携带隐藏环境参数的闭包、带隐藏 receiver 的方法,都只是对同一调用契约的扩展。
以 obj.mix(read(), 3.5, next()) 为例,假设语言规定 receiver 与实参从左到右求值。目标无关降低先按该顺序产生值:
%recv = lower obj
%a0 = call @read()
%a1 = const.f64 3.5
%a2 = call @next()
%result = call @mix(%recv, %a0, %a1, %a2)后续 ABI lowering 才决定 %recv、%a0、%a1 使用寄存器,而 %a2 进入栈槽。只要搬运的是已经求出的值,物理放置顺序可以不同,但不能因此重排源码中的两个调用。
某些调用还包含隐藏参数。闭包调用可能先传环境指针;方法调用可能把 receiver 作为 self;返回大型结构体时,调用者可能提供一个隐藏的结果存储指针。IR 函数类型或调用元数据必须让 caller 与 callee 对这些约定保持一致。
返回必须封闭所有正常路径
return e 会降低 e,转换成声明的返回类型,执行必要清理,再终结当前基本块。多个源级 return 可以保留为多个 IR return,也可以全部跳到统一 epilogue。统一出口能减少重复清理,但可能需要返回值 phi 或临时槽。
例如,两条源级返回路径可以汇合为:
fast:
br epilogue(%cached)
slow:
%computed = call @compute()
br epilogue(%computed)
epilogue(%answer):
call @release(%resource)
ret %answer块参数或 phi 负责选择传入的 answer;更重要的是,两个正常出口上的 release 都恰好执行一次。如果 compute() 可能抛出异常,其异常边还需要独立清理路径,不能直接复用一个期待返回值的正常 epilogue。
调用会同时改变控制流和数据流
可能抛出异常的调用具有正常后继与异常后继。尾调用只有在调用约定、返回形态、清理义务和语言语义都允许时,才能复用当前栈帧;它是语义与 ABI 证明,不是文本替换。
调用还可以携带供优化器使用的 effect 信息。pure 调用不读写可变内存,readonly 调用可以读取但不能写入,普通调用则可能使已有内存事实失效。若把有副作用的调用错误标成 pure,公共子表达式消除或代码移动就可能造成错误编译,因此属性必须保守且可验证。
接受一段函数 IR 前,应验证:每条可达正常路径最终都有类型兼容的 ret 或其他终结指令;每次调用的参数与结果类型符合签名;每条退出路径上的清理恰好执行一次。