Share
Embed
Transcript
41 slides
-
Rustコンパイラの構成と特徴 全体的な構成: HIR HIR (High-level Intermediate Representation) と呼ばれる中間表現がまずは生成さ れる。その後THIR (Typed
High-level Intermediate Representation)が生成され、次 のフェーズ(MIR)に渡される。 主には脱糖(desugar)を担う。たとえば、Rustの文法上現れる下記の文法機能 は、コンパイラ内部では違う形で扱われる。 for式やwhile式は、すべてmatch + loopに変換される。 asyncやawaitは、コンパイラ内部表現としてコルーチンに変換される。 11 -
Rustコンパイラの構成と特徴 全体的な構成: HIR HIRは、次のcargoコマンドで確認できる。 cargo rustc -- -Z unpretty=hir-tree #
AST形式で出力 cargo rustc -- -Z unpretty=hir # Rustのコード形式で出力 cargo rustc -- -Z unpretty=thir-tree 13 -
Rustコンパイラの構成と特徴 全体的な構成: HIR 試しに下記のようなコードをHIRとして出力させてみる。 fn main() { let nums =
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let mut ans = 0; for num in nums { ans += num; } assert_eq!(ans, 55); } 14 -
Rustコンパイラの構成と特徴 全体的な構成: HIR 本来のHIR自体はこのようにASTに近しい形だが、少し読みにくいのでRustコードに近い形式 で出力させる。 DefId(0:0 ~ for_hir[8e24]) => OwnerNodes
{ node: ParentedNode { parent: 4294967040, node: Crate( Mod { spans: ModSpans { inner_span: src/bin/for_hir.rs:1:1: 8:2 (#0), inject_use_span: no-location (#0), }, 15 -
すると、for式がmatch, loop, matchの形式で変換されていることを確認できる。 // ... fn main() { let nums
= <[_]>::into_vec(::alloc::boxed::box_new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); let mut ans = 0; { let _t = match #[lang = "into_iter"](nums) { mut iter => loop { match #[lang = "next"](&mut iter) { #[lang = "None"] {} => break, #[lang = "Some"] { 0: num } => { ans += num; } } }, }; _t }; match (&ans, &55) { (left_val, right_val) => { if !(*left_val == *right_val) { let kind = ::core::panicking::AssertKind::Eq; ::core::panicking::assert_failed(kind, &*left_val, &*right_val, ::core::option::Option::None); } } }; } 16 -
Rustコンパイラの構成と特徴 全体的な構成: MIR THIRからMIR(Mid-level Intermediate Representation)が生成される。 MIRはRustコンパイラの根幹をなす中間表現である。主には制御フローグラフに基 づいた中間表現になっており、型付けもなされた状態である。 たとえば下記が含まれる。 ボローチェック(Borrow
Check)。 単相化(Monomorphization): コンパイル時にジェネリクスを具体的に型に置き換えて実装 を生成し、実行速度を最適化する仕組み。 17 -
Rustコンパイラの構成と特徴 全体的な構成: MIR HIRで使用したのと同じコードを使ってMIRを出力させてみると、次のような出力を見ること ができる。 copy などの文字列が見られる。 fn main() ->
() { let mut _0: (); let _1: std::vec::Vec<i32>; // ... bb1: { _6 = ShallowInitBox(move _5, [i32; 10]); _26 = copy ((_6.0: std::ptr::Unique<[i32; 10]>).0: std::ptr::NonNull<[i32; 10]>) as *const [i32; 10] (Transmute); _27 = copy _26 as *const () (PtrToPtr); _28 = copy _27 as usize (Transmute); _29 = AlignOf([i32; 10]); _30 = Sub(copy _29, const 1_usize); _31 = BitAnd(copy _28, copy _30); _32 = Eq(copy _31, const 0_usize); assert(copy _32, "misaligned pointer dereference: address must be a multiple of {} but is {}", copy _29, copy _28) -> [success: bb15, unwind unreachable]; } 19 -
Rustコンパイラの構成と特徴 全体的な構成: LLVM IR(Codegen) MIRからLLVM IRが生成される。バイナリが生成される直前のフェーズであると言 える。 バイナリの生成までにはさらにいくつかのステップを経るが、詳しくは下記の記 事を参考にできる。 Resolving
Rust Symbols: https://blog.shrirambalaji.com/posts/resolving-rust-symbols/ ちなみに、この記事の著者は昨年Rust.Tokyoに登壇してくれた。 20 -
Rustコンパイラの構成と特徴 特徴的な設計 > クエリシステム: クエリシステムとは ソースコードを全部コンパイルするのではなく、最終的に生成されるバイナリ等 に必要になるソースコード部分や機能のみコンパイルする。かつ、問い合わせが あるまではコンパイルは遅延される。 これをRustコンパイラの説明では、Demand-Driven Compilation(要求駆動コンパ
イル)などと呼んでいる。 本筋から外れるが、このシステムを実装できるSalsaというOSSがある。 https://github.com/salsa-rs/salsa 24 -
Rustコンパイラの構成と特徴 特徴的な設計 > クエリシステム: TyCtxt<'tcx> 各クエリの結果は TyCtxt<'tcx> の構造体に実装されている関数を経由して取り 出せる。 TyCtxt
が持つ type_of() などの関数を呼び出すと、 i. キャッシュがあるかないかをまずチェックする。 ii. キャッシュがあればその値を返し、なければデフォルト実装用の関数を呼び 出す。 iii. 結果はキャッシュとして保存される。 25 -
Rustコンパイラの構成と特徴 特徴的な設計 > クエリシステム: クエリの定義 内部的には rustc_queries! というマクロで定義されている。 rustc_queries! {
/// Records the type of every item. query type_of(key: DefId) -> Ty<'tcx> { cache_on_disk_if { key.is_local() } desc { |tcx| "computing the type of `{}`", tcx.def_path_str(key) } } ... } あとは先ほどの TypCtxt<'tcx> 経由で呼び出せる。 fn foo(tcx: TyCtxt<'_>, def_id: DefId) { let ident = tcx.type_of(def_id).instantiate_identity(); } 26 -
Rustコンパイラの構成と特徴 特徴的な設計 > エラーメッセージ: DiagCtxt クエリシステムと似たように、コンパイル時のエラー(診断; Diagnosticsという) については rust_errors::DiagCtxt というコンテクストに保有される。
実装中は rust_errors::DiagCtxtHandle<'a> を経由して呼び出す。 実装時はこれに診断内容を送る。 28 -
Rustコンパイラの構成と特徴 特徴的な設計 > エラーメッセージの管理 エラーメッセージの管理自体は、コンパイラの各クレートに存在する message.ftl というファイルで行われている。 このファイルはキーと値が詰まった形式になっている。 よく見る実装の提案に関するヘルプメッセージなども定義できる。 下記はたとえば、staticな可変参照を作ることはできない、というコンパイルエラ
ーに関するメッセージの定義。 lint_static_mut_refs_lint = creating a {$shared_label}reference to mutable static .label = {$shared_label}reference to mutable static .suggestion = use `&raw const` instead to create a raw pointer .suggestion_mut = use `&raw mut` instead to create a raw pointer``` 29 -
Rustコンパイラの構成と特徴 特徴的な設計 > エラーメッセージの管理 呼び出しは、コンパイルエラーを表現する構造体のアトリビュートに、どのエラ ーメッセージを呼び出すかの情報を記述して行われる。 #[derive(LintDiagnostic)] #[diag(lint_static_mut_refs_lint)] pub(crate) struct
RefOfMutStatic<'a> { #[label] pub span: Span, #[subdiagnostic] pub sugg: Option<MutRefSugg>, pub shared_label: &'a str, #[note(lint_shared_note)] pub shared_note: bool, #[note(lint_mut_note)] pub mut_note: bool, } 30 -
Rustコンパイラ開発の個人的体験 前提として必要そうな知識など Rustそれ自体への理解は必要。 Issueを見て、「ああ、あの機能ね」とあたりがつくのが大事。 コントリビューションを開始する前にまずRustを学ぶのは必須だと思う。 コンパイラへの基礎的な理解は必要。 コンパイラの教科書を一冊軽く読んでおくのをおすすめはする。ただ、公開されている開発 者向けガイドで結構追いつけるといえば追いつける。 英語でのコミュニケーションは必要。 今はLLMを使ってなんとかなるので、最悪大丈夫。
でも、そもそも英語を使えれば心理的障壁なく、臆することなく普段のPull Request上での ノリで会話できるので、できるようになっておくにこしたことはないと思っている。 33