前回までのあらすじ
Guide to Rustc Developmentという便利なガイドに沿って、Rustの本家コンパイラであるrust-lang/rustのソースコードを読んでみようという試み。なかなか終わらない「Source Code Representations」の、MIRのあたりから。
Source Code Representations
Token列、AST、HIRときたので、今回はMIR生成を見ていく。このあたりからQuery systemがガンガン使われるので、シーケンシャルな感覚でいると流れが追いづらい。内容的にもこれまでより難しいので、前提となる知識から整理していこう。
CFG
MIRの構造はControl-flow graph(CFG)をベースにしている。私は詳しくないが、最適化コンパイラではそれなりに使われるデータ構造と思われる。MIRにおいてCFGがどのように表されるのかの具体例は、ガイドの説明が結構丁寧なのでそちらを参照するのが分かりやすい。基本的には基本ブロック(Basic Block)が並び、典型的には基本ブロックの中には複数のstatementと単独のterminatorが配置される。terminatorは次に進む基本ブロックを指し示しており、場合によっては条件で決まる複数の行き先をもつ。
(<n>、<m>には番号が入る)
bb<n>: {
statement
statement
...
terminater -> bb<m>;
}
データ構造がそれなりに複雑なので、まずはこの構造とソースの対応を確認しておこう。
MIRの生成はrustc_mir_build
で定義されているmir_built
のQueryから開始される。その実体となる関数mir_build()
のシグネチャで出現するデータ型を調べてみる。
crate fn mir_built<'tcx>(
tcx: TyCtxt<'tcx>,
def: ty::WithOptConstParam<LocalDefId>,
) -> &'tcx ty::steal::Steal<Body<'tcx>> {
if let Some(def) = def.try_upgrade(tcx) {
return tcx.mir_built(def);
}
tcx.alloc_steal_mir(mir_build(tcx, def))
}
/// Construct the MIR for a given `DefId`.
fn mir_build(tcx: TyCtxt<'_>, def: ty::WithOptConstParam<LocalDefId>) -> Body<'_> {
...
}
def
としてDefID
を受け取り、Body
という型を返している。
ガイドにもある通り、MIRの生成は関数の本体ブロックや、static
、const
の初期化ブロックなどに対して行われる。考えてみれば、プログラムにおいて実行される記述はそのような部分に絞られるのだろう。そのため、ASTやHIRのようにクレート全体を渡すのではなく、DefID
を渡すことでソースのブロック単位で変換を行うような作りになっている。(Query戦略のほうが理由としては大きいかもしれないが、、)
関数の結果としては、Body
という型を返しており、この型名からもブロック単位で処理が行われていることが読み取れる。では、Body
の構造について掘り下げていこう。
Body
は様々な情報をもっているが、その中にbasic_blocks
というベクタがある。これは、MIRが複数の基本ブロックで構成されているという構造と一致しそうだ。ベクタの要素としてはBasicBlock
とBasicBlockData
をもっている。BasicBlockData
にはStatement
のベクタとTerminater
があり、これも基本ブロックの内部構造と一致していることが分かる。BasicBlock
は、BasicBlockData
に紐づくインデックスを保持しているようで、見る限りBasicBlock
を使ったアクセスのほうが多用されている。(ちなみに執筆時点のガイドには、MIRはMir
という型で表されると記載されているが、そのような型は見当たらなかった)
/// The lowered representation of a single function.
#[derive(Clone, TyEncodable, TyDecodable, Debug, HashStable, TypeFoldable)]
pub struct Body<'tcx> {
/// A list of basic blocks. References to basic block use a newtyped index type `BasicBlock`
/// that indexes into this vector.
basic_blocks: IndexVec<BasicBlock, BasicBlockData<'tcx>>,
...
}
...
rustc_index::newtype_index! {
pub struct BasicBlock {
derive [HashStable]
DEBUG_FORMAT = "bb{}",
const START_BLOCK = 0,
}
}
...
#[derive(Clone, Debug, TyEncodable, TyDecodable, HashStable, TypeFoldable)]
pub struct BasicBlockData<'tcx> {
/// List of statements in this block.
pub statements: Vec<Statement<'tcx>>,
/// Terminator for this block.
///
/// N.B., this should generally ONLY be `None` during construction.
/// Therefore, you should generally access it via the
/// `terminator()` or `terminator_mut()` methods. The only
/// exception is that certain passes, such as `simplify_cfg`, swap
/// out the terminator temporarily with `None` while they continue
/// to recurse over the set of basic blocks.
pub terminator: Option<Terminator<'tcx>>,
...
}
Terminator
の中身についても一応確認しておこう。Terminator
の構造体はTerminatorKind
をもっており、このenumの要素の多くは内部でBasicBlock
をもっている。これは次に向かう基本ブロックを意味していると思われる。条件によって分岐するような場合はBasicBlock
をベクタとして保持している。
#[derive(Clone, TyEncodable, TyDecodable, HashStable, PartialEq)]
pub enum TerminatorKind<'tcx> {
/// Block should have one successor in the graph; we jump there.
Goto { target: BasicBlock },
/// Operand evaluates to an integer; jump depending on its value
/// to one of the targets, and otherwise fallback to `otherwise`.
SwitchInt {
...
/// Possible branch sites. The last element of this vector is used
/// for the otherwise branch, so targets.len() == values.len() + 1
/// should hold.
//
// This invariant is quite non-obvious and also could be improved.
// One way to make this invariant is to have something like this instead:
//
// branches: Vec<(ConstInt, BasicBlock)>,
// otherwise: Option<BasicBlock> // exhaustive if None
//
// However we’ve decided to keep this as-is until we figure a case
// where some other approach seems to be strictly better than other.
targets: Vec<BasicBlock>,
},
...
}
#[derive(Clone, Debug, TyEncodable, TyDecodable, HashStable)]
pub struct Terminator<'tcx> {
pub source_info: SourceInfo,
pub kind: TerminatorKind<'tcx>,
}
これで、CFGを構成する基本ブロックの構造と、その構造のソース内での表現を確認することができた。
THIR
まだMIRには行けない。HIRからMIRに変換する前に、Typed HIR(THIR)へ変換するという処理が入っている。THIRはHigh-level Abstract IR (HAIR)と呼ばれることも多く、また、全体構成だと省略されていることもあったりして、ちょっと混乱する。この変換では、暗黙的な型強制を明示したり、メソッドの呼び出し形式を変更したりするようだ。THIRも、ソース全体をまとめて変換するのではなく、式に対して必要に応じて変換されているようだ。
THIRへの変換処理は主にrustc_mir_build::thir
で実施されており、式に対する処理はhir::Expr
に対するmake_mirror()
メソッドから始まる。
impl<'tcx> Mirror<'tcx> for &'tcx hir::Expr<'tcx> {
type Output = Expr<'tcx>;
fn make_mirror(self, cx: &mut Cx<'_, 'tcx>) -> Expr<'tcx> {
let temp_lifetime = cx.region_scope_tree.temporary_scope(self.hir_id.local_id);
let expr_scope = region::Scope { id: self.hir_id.local_id, data: region::ScopeData::Node };
debug!("Expr::make_mirror(): id={}, span={:?}", self.hir_id, self.span);
let mut expr = make_mirror_unadjusted(cx, self);
// Now apply adjustments, if any.
for adjustment in cx.typeck_results().expr_adjustments(self) {
debug!("make_mirror: expr={:?} applying adjustment={:?}", expr, adjustment);
expr = apply_adjustment(cx, self, expr, adjustment);
}
// Next, wrap this up in the expr's scope.
expr = Expr {
temp_lifetime,
ty: expr.ty,
span: self.span,
kind: ExprKind::Scope {
region_scope: expr_scope,
value: expr.to_ref(),
lint_level: LintLevel::Explicit(self.hir_id),
},
};
...
// OK, all done!
expr
}
}
処理はmake_mirror_unadjusted()
へと続いていき、ここで式の種別ごとの変換に実施される。一例としてメソッド呼び出しの変換処理を見てみると、ソース内のコメントにもあるようにa.b(c)
というメソッドの呼び出しをTrait::b(a, c)
という形式に変換している。変換後の形式はUFCS(Unified Function Call Syntax)と呼ばれるらしい。知らなかった。
fn make_mirror_unadjusted<'a, 'tcx>(
cx: &mut Cx<'a, 'tcx>,
expr: &'tcx hir::Expr<'tcx>,
) -> Expr<'tcx> {
let expr_ty = cx.typeck_results().expr_ty(expr);
let temp_lifetime = cx.region_scope_tree.temporary_scope(expr.hir_id.local_id);
let kind = match expr.kind {
// Here comes the interesting stuff:
hir::ExprKind::MethodCall(_, method_span, ref args, fn_span) => {
// Rewrite a.b(c) into UFCS form like Trait::b(a, c)
let expr = method_callee(cx, expr, method_span, None);
let args = args.iter().map(|e| e.to_ref()).collect();
ExprKind::Call { ty: expr.ty, fun: expr.to_ref(), args, from_hir_call: true, fn_span }
}
...
};
Expr { temp_lifetime, ty: expr_ty, span: expr.span, kind }
}
make_mirror()
はCx
という型のメソッドであるmirror()
から呼び出され、このmirror()
がMIRへの変換中に多用されることになる。
impl<'a, 'tcx> Cx<'a, 'tcx> {
/// Normalizes `ast` into the appropriate "mirror" type.
crate fn mirror<M: Mirror<'tcx>>(&mut self, ast: M) -> M::Output {
ast.make_mirror(self)
}
...
}
THIRへの変換はMIR生成のところどころで現れるため、意識しておくと読みやすくなるだろう。
MIR
ようやくMIRへの変換処理を読む準備が整った。上でシグネチャだけ確認したmir_build()
に戻り、その中身を追っていこう。ここでは、関数定義の本体ブロックを想定して見ていく。
まずはDefID
に基づいてBodyID
を取り出した後、build::construct_fn()
に進んでいく。
/// Construct the MIR for a given `DefId`.
fn mir_build(tcx: TyCtxt<'_>, def: ty::WithOptConstParam<LocalDefId>) -> Body<'_> {
let id = tcx.hir().local_def_id_to_hir_id(def.did);
// Figure out what primary body this item has.
let (body_id, return_ty_span, span_with_body) = match tcx.hir().get(id) {
Node::Expr(hir::Expr { kind: hir::ExprKind::Closure(_, decl, body_id, _, _), .. }) => {
(*body_id, decl.output.span(), None)
}
Node::Item(hir::Item {
kind: hir::ItemKind::Fn(hir::FnSig { decl, .. }, _, body_id),
span,
..
})
| Node::ImplItem(hir::ImplItem {
kind: hir::ImplItemKind::Fn(hir::FnSig { decl, .. }, body_id),
span,
..
})
| Node::TraitItem(hir::TraitItem {
kind: hir::TraitItemKind::Fn(hir::FnSig { decl, .. }, hir::TraitFn::Provided(body_id)),
span,
..
}) => {
// Use the `Span` of the `Item/ImplItem/TraitItem` as the body span,
// since the def span of a function does not include the body
(*body_id, decl.output.span(), Some(*span))
}
...
};
// If we don't have a specialized span for the body, just use the
// normal def span.
let span_with_body = span_with_body.unwrap_or_else(|| tcx.hir().span(id));
tcx.infer_ctxt().enter(|infcx| {
let cx = Cx::new(&infcx, def, id);
let body = if let Some(ErrorReported) = cx.typeck_results().tainted_by_errors {
build::construct_error(cx, body_id)
} else if cx.body_owner_kind.is_fn_or_closure() {
...
let body = tcx.hir().body(body_id);
...
let mut mir = build::construct_fn(
cx,
id,
arguments,
safety,
abi,
return_ty,
return_ty_span,
body,
span_with_body
);
mir.yield_ty = yield_ty;
mir
} else {
...
};
lints::check(tcx, &body, def.did);
...
body
})
}
construct_fn()
では、まず冒頭部分でBuilder::new()
によりBuilder
を生成していることに注目しよう。ついでに、Cx
としてhir
が渡されていることも意識しておく。この関数は後から他の部分も見るので、全体を載せている。
fn construct_fn<'a, 'tcx, A>(
hir: Cx<'a, 'tcx>,
fn_id: hir::HirId,
arguments: A,
safety: Safety,
abi: Abi,
return_ty: Ty<'tcx>,
return_ty_span: Span,
body: &'tcx hir::Body<'tcx>,
span_with_body: Span
) -> Body<'tcx>
where
A: Iterator<Item = ArgInfo<'tcx>>,
{
let arguments: Vec<_> = arguments.collect();
let tcx = hir.tcx();
let tcx_hir = tcx.hir();
let span = tcx_hir.span(fn_id);
let fn_def_id = tcx_hir.local_def_id(fn_id);
let mut builder = Builder::new(
hir,
span_with_body,
arguments.len(),
safety,
return_ty,
return_ty_span,
body.generator_kind,
);
let call_site_scope =
region::Scope { id: body.value.hir_id.local_id, data: region::ScopeData::CallSite };
let arg_scope =
region::Scope { id: body.value.hir_id.local_id, data: region::ScopeData::Arguments };
let mut block = START_BLOCK;
let source_info = builder.source_info(span);
let call_site_s = (call_site_scope, source_info);
unpack!(
block = builder.in_scope(call_site_s, LintLevel::Inherited, |builder| {
if should_abort_on_panic(tcx, fn_def_id, abi) {
builder.schedule_abort();
}
let arg_scope_s = (arg_scope, source_info);
// `return_block` is called when we evaluate a `return` expression, so
// we just use `START_BLOCK` here.
unpack!(
block = builder.in_breakable_scope(
None,
START_BLOCK,
Place::return_place(),
|builder| {
builder.in_scope(arg_scope_s, LintLevel::Inherited, |builder| {
builder.args_and_body(
block,
fn_def_id.to_def_id(),
&arguments,
arg_scope,
&body.value,
)
})
},
)
);
// Attribute epilogue to function's closing brace
let fn_end = span_with_body.shrink_to_hi();
let source_info = builder.source_info(fn_end);
let return_block = builder.return_block();
builder.cfg.goto(block, source_info, return_block);
builder.cfg.terminate(return_block, source_info, TerminatorKind::Return);
// Attribute any unreachable codepaths to the function's closing brace
if let Some(unreachable_block) = builder.cached_unreachable_block {
builder.cfg.terminate(unreachable_block, source_info, TerminatorKind::Unreachable);
}
return_block.unit()
})
);
assert_eq!(block, builder.return_block());
let spread_arg = if abi == Abi::RustCall {
// RustCall pseudo-ABI untuples the last argument.
Some(Local::new(arguments.len()))
} else {
None
};
debug!("fn_id {:?} has attrs {:?}", fn_def_id, tcx.get_attrs(fn_def_id.to_def_id()));
let mut body = builder.finish();
body.spread_arg = spread_arg;
body
}
Builder
のメンバを確認しよう。いろいろあるが、Cx
としてhir
をもつ他、CFG
という型のメンバをもっている。そしてこのCFG
は、basic_blocks
として基本ブロックのベクタを表していることが分かる。これはBody
に含まれるbasic_blocks
と同じ構造だ。
struct Builder<'a, 'tcx> {
hir: Cx<'a, 'tcx>,
cfg: CFG<'tcx>,
...
}
...
struct CFG<'tcx> {
basic_blocks: IndexVec<BasicBlock, BasicBlockData<'tcx>>,
}
CFG
の型には、基本ブロックやその内部データを操作するためのメソッドが定義されている。基本的なところをいくつか示しておくと、新しく基本ブロックを作るstart_new_block()
、Statement
を追加するpush()
、Terminator
を設定するterminate()
などがある。MIRの生成では、これらのメソッドを駆使して、望むような構造を作り上げていくことになる。
impl<'tcx> CFG<'tcx> {
crate fn block_data(&self, blk: BasicBlock) -> &BasicBlockData<'tcx> {
&self.basic_blocks[blk]
}
crate fn block_data_mut(&mut self, blk: BasicBlock) -> &mut BasicBlockData<'tcx> {
&mut self.basic_blocks[blk]
}
// llvm.org/PR32488 makes this function use an excess of stack space. Mark
// it as #[inline(never)] to keep rustc's stack use in check.
#[inline(never)]
crate fn start_new_block(&mut self) -> BasicBlock {
self.basic_blocks.push(BasicBlockData::new(None))
}
...
crate fn push(&mut self, block: BasicBlock, statement: Statement<'tcx>) {
debug!("push({:?}, {:?})", block, statement);
self.block_data_mut(block).statements.push(statement);
}
...
crate fn terminate(
&mut self,
block: BasicBlock,
source_info: SourceInfo,
kind: TerminatorKind<'tcx>,
) {
debug!("terminating block {:?} <- {:?}", block, kind);
debug_assert!(
self.block_data(block).terminator.is_none(),
"terminate: block {:?}={:?} already has a terminator set",
block,
self.block_data(block)
);
self.block_data_mut(block).terminator = Some(Terminator { source_info, kind });
}
/// In the `origin` block, push a `goto -> target` terminator.
crate fn goto(&mut self, origin: BasicBlock, source_info: SourceInfo, target: BasicBlock) {
self.terminate(origin, source_info, TerminatorKind::Goto { target })
}
}
construct_fn()
の最後を見ると、builder
に対してfinish()
というメソッドを呼んでいる。このfinish()
メソッドで、作り上げたcfg
を同じ型であるBody
のbasic_blocks
へと移し、MIRを返すという流れになっている。
impl<'a, 'tcx> Builder<'a, 'tcx> {
...
fn finish(self) -> Body<'tcx> {
for (index, block) in self.cfg.basic_blocks.iter().enumerate() {
if block.terminator.is_none() {
span_bug!(self.fn_span, "no terminator on block {:?}", index);
}
}
Body::new(
self.cfg.basic_blocks,
self.source_scopes,
self.local_decls,
self.canonical_user_type_annotations,
self.arg_count,
self.var_debug_info,
self.fn_span,
self.generator_kind,
)
}
...
}
さて、今度はconstruct_fn()
の中段あたりに注目し、cfg
を作り上げるところを追っていこう。マクロやクロージャが絡み合い複雑になっているが、args_and_body()
というメソッドが呼ばれている。大きく省略しているが、このメソッドの最後のあたりを見る。
impl<'a, 'tcx> Builder<'a, 'tcx> {
...
fn args_and_body(
&mut self,
mut block: BasicBlock,
fn_def_id: DefId,
arguments: &[ArgInfo<'tcx>],
argument_scope: region::Scope,
ast_body: &'tcx hir::Expr<'tcx>,
) -> BlockAnd<()> {
...
let body = self.hir.mirror(ast_body);
self.into(Place::return_place(), block, body)
}
}
上で説明したmirror()
メソッドでTHIRに変換したのち、into()
というメソッドを呼び出している。このinto()
メソッドは、eval_into()
を経由して、into_expr()
を呼び出すことになる。
impl<'a, 'tcx> Builder<'a, 'tcx> {
crate fn into<E>(
&mut self,
destination: Place<'tcx>,
block: BasicBlock,
expr: E,
) -> BlockAnd<()>
where
E: EvalInto<'tcx>,
{
expr.eval_into(self, destination, block)
}
}
impl<'tcx> EvalInto<'tcx> for ExprRef<'tcx> {
fn eval_into(
self,
builder: &mut Builder<'_, 'tcx>,
destination: Place<'tcx>,
block: BasicBlock,
) -> BlockAnd<()> {
let expr = builder.hir.mirror(self);
builder.into_expr(destination, block, expr)
}
}
ではinto_expr()
を見ていく。ここで式の種別ごとに処理が分けられ、それぞれで先ほどのCFG
のメソッドを使い、MIRが生成されていく。いくつか確認してみよう。
impl<'a, 'tcx> Builder<'a, 'tcx> {
/// Compile `expr`, storing the result into `destination`, which
/// is assumed to be uninitialized.
crate fn into_expr(
&mut self,
destination: Place<'tcx>,
mut block: BasicBlock,
expr: Expr<'tcx>,
) -> BlockAnd<()> {
...
let block_and = match expr.kind {
...
ExprKind::Block { body: ast_block } => {
this.ast_block(destination, block, ast_block, source_info)
}
ExprKind::Match { scrutinee, arms } => {
this.match_expr(destination, expr_span, block, scrutinee, arms)
}
...
ExprKind::LogicalOp { op, lhs, rhs } => {
// And:
//
// [block: If(lhs)] -true-> [else_block: If(rhs)] -true-> [true_block]
// | | (false)
// +----------false-----------+------------------> [false_block]
//
// Or:
//
// [block: If(lhs)] -false-> [else_block: If(rhs)] -true-> [true_block]
// | (true) | (false)
// [true_block] [false_block]
let (true_block, false_block, mut else_block, join_block) = (
this.cfg.start_new_block(),
this.cfg.start_new_block(),
this.cfg.start_new_block(),
this.cfg.start_new_block(),
);
let lhs = unpack!(block = this.as_local_operand(block, lhs));
let blocks = match op {
LogicalOp::And => (else_block, false_block),
LogicalOp::Or => (true_block, else_block),
};
let term = TerminatorKind::if_(this.hir.tcx(), lhs, blocks.0, blocks.1);
this.cfg.terminate(block, source_info, term);
let rhs = unpack!(else_block = this.as_local_operand(else_block, rhs));
let term = TerminatorKind::if_(this.hir.tcx(), rhs, true_block, false_block);
this.cfg.terminate(else_block, source_info, term);
this.cfg.push_assign_constant(
true_block,
source_info,
destination,
Constant { span: expr_span, user_ty: None, literal: this.hir.true_literal() },
);
this.cfg.push_assign_constant(
false_block,
source_info,
destination,
Constant { span: expr_span, user_ty: None, literal: this.hir.false_literal() },
);
// Link up both branches:
this.cfg.goto(true_block, source_info, join_block);
this.cfg.goto(false_block, source_info, join_block);
join_block.unit()
}
...
};
...
block_and
}
}
-
ExprKind::Block
:関数の本体などは全体がブロックで囲われているため、最初はここに入ると思われる。詳細なソースは記載しないが、ast_block()
、ast_block_stmts()
と続く。このast_block_stmts()
も、Rustにおける束縛の考え方のようなものが反映されていて、深く読み込んだら面白そうな感じだ。ブロック内の各文はstmt_expr()
へと渡り、いくつか条件はあるものの、特に引っかからないものはas_temp()
、expr_as_temp()
と進んでいき、再度into()
が呼ばれるように見える。つまり、ブロック内の文に対してinto_expr()
を呼び出し、再帰的に処理されていく様子が伺える。 -
ExprKind::Match
:いわゆるプログラムが分岐していくような処理は、大抵ここに入りmatch_expr()
に進んでいくと思われるが、この先は結構処理が大きく複雑なのであまり追えていない。 -
ExprKind::LogicalOp
:その代わり、論理演算の処理はMIRにおける分岐の処理がコンパクトにまとまっていて、分かりやすいと思ったので少し説明しておく。ここではA && B
のような処理を考えてみよう。- 現在は
A
にいる。 - まず、
start_new_block()
により必要な基本ブロックを作る。全体として真になったときのtrue_block
、全体として偽になったときのfalse_block
、B
の処理が入るelse_block
、全てが終わったあとに進んでいくjoin_block
だ。 - 現在地
A
から進む先は条件によりelse_block
かfalse_block
になるため、terminate()
でそのようにTerminator
を設定する。 -
else_block
に進んだ場合、そこから向かう先はtrue_block
かfalse_block
になるため、こちらもterminate()
で設定する。 -
true_block
とfalse_block
にはpush_assign_constant()
によりそれぞれStatement
としてtrueとfalseを設定する。 - 最後に
goto()
によりtrue_block
とfalse_block
のTerminator
をjoin_block
に設定し、作成した基本ブロックをつなぐ流れができあがる。
- 現在は
完成したcfg
を上述した通りBody
の要素として返すことで、MIR生成が完了となる。
つづく
これでソースコードからMIRまでの変換を見ていくことができた。ここからLLVM IRの生成に進んでいくかと思いきや、次章「Analysis」ではRustの特徴的な機構をいろいろと見ていく(たぶん)。全部は深掘れないと思うのでいくつかピックアップしていこうと思います。