Rustのコンパイラと戯れる 〜 3日目

## はじめに

[前回](https://qiita.com/ktz_alias/items/65709903b7f0553e76c1)は、1モジュールの構造を眺めたけど、今回はクレート単位で構造を取り扱う方法を考える。

またせっかく抽出するので、`Rust`の`API`を抽出し、引数や戻り値の型から逆引きする足掛かりも考える。

## 方針

まず、クレート全体を周回するために、どのようにしてパラメータを渡すべきか。
これは簡単で、`rustc_interface::interface::Config`の`input`フィールドにクレートのルートモジュールを渡す。
クレートのルートモジュールはたいてい`src/lib.rs`になる。

例えば、標準ライブラリの`std`の収集は以下のように指定する。
ここで、`sys_root`には`Rust`ライブラリパスのプレフィックスが入っているものと想定している。

```rust
let sys_root = "...";
let root_crate = "std";

let file_path: PathBuf = [
    &sysroot, 
    "lib/rustlib/src/rust/library",
    root_crate, "src/lib.rs"
]
.iter().collect();

use rustc_interface::interface;
use rustc_session::config;

let rustc_config = interface::Config {
    input: config::Input::File(file_path),
    // 他は前回と同じ
};
```

上記のように指定して`rustc_interface::interface::run_compiler`関数を走らせると、クレート内の全ての構成要素を操作できるようになる。

## APIの抽出

`API`の関数識別子、引数及び戻り値の型を収集し、`json`フォーマットで出力するサンプルツールを作成した。

https://github.com/ritalin/extract-rust-apis

* クレート名をソース上にベタ書きしてるため、`std`のみ収集します。
* 出力先も決め打ちで、カレントディレクトリの`exp`となってます。

### つらかったところ

#### 型の完全名の解決

構造内で型定義を行っているノードには`rustc_hir::def_id::DefId`が振られており、この値をS`rustc_middle::hir::map::Map`(`rustc_middle::ty::TyCtxt::mir`関数で取得できる)の`def_path`関数に渡すことで`rustc_hir::definitions::DefPath`からの値を取得することができる。

https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/hir/map/struct.Map.html#method.def_path

次いで`DefPath::to_string_no_crate_verbose`関数を呼ぶことで、コロン区切り(`::`)の完全名を取得することができる。

ただし`DefPath`の取得において幾つか制限が見受けられた。

* `self`引数の場合、`xxx::xxx::Trait`とされてしまい、適切に完全名が取得できない。
    * `DefId`を（`LocalDefId`に変換した上で）`TyCtxt`の`hir_node_by_def_id`関数に渡して、`rustc_hir::Node`を取得し、ここから型情報を経由して、`self`の実際の型解決を行なった。
* プリミティブ型(i8とか）は`DefId`が取得できない。
    * `rustc_hir::def::Res`の`PrimTy`ヴァリアントを経由して型名を取得できる。
* いくつかの型(`socketaddr_un`など)は型名を解決できなかった。
    * `rustc_hir::PathSegment`の`ident`フィールドの値を連結してよしとした。

#### コンパイルエラー表示

クレート単体でビルドするため、依存先が解決できずにコンパイルエラーが標準エラーに出力される。
出力はされるが構造の収集には全く問題がないため、ただの邪魔な存在である。

エラー表示は、[rustc_errors::DiagCtxt](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/struct.DiagCtxt.html)を経由して、[rustc_errors::emitter::DynEmitter](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/emitter/type.DynEmitter.html)で行われる。

出力を阻害する方法として、「Rust Compiler Development Guide」に記載されたサンプルのように、`rustc_interface::interface::Config`の`psess_created`に`DiagCtx`の初期値を渡すことで行うことができる。

https://rustc-dev-guide.rust-lang.org/rustc-driver-getting-diagnostics.html?highlight=emitter#getting-diagnostics

### やってないこと

* 再エクスポートは拾っていない
    * `std::string::String`は含まれていない。本体は`alloc::string::String`のため。
* `std`限定
    * 単なる手抜き

## APIの逆引き

APIの逆引きのためにデータベースに投入する。
`json`をそのまま使ってインポートできるため、データベースとして`duckdb`を選択した。

https://duckdb.org/

まず、以下の3つのテーブルを用意した。

* prototype (関数名を保持するテーブル)
* type_symbol (引数や戻り値の型を保持するテーブル）
* type_ref (関数と型情報の関連付けを行うテーブル）
    * 引数か戻り値かを判定は`enum`で用意

```sql
create table prototype (
    id BIGINT not null primary key,
    symbol varchar(64) not null,
    qual_symbol varchar(256) not null
);

create table type_symbol (
    id BIGINT not null primary key,
    symbol varchar(64) not null,
    qual_symbol varchar(256) not null
);

create type type_kind as enum ('arg', 'return');

create or replace table type_ref (
    prototype_id BIGINT not null,
    type_id BIGINT not null,
    kind type_kind not null,
    constraint type_ref_pk primary key (prototype_id, type_id, kind)
);
```

次いで、データを投入する。

```sql
/* 関数プロトタイプを投入 */
insert into prototype
select 
    row_number() over (order by qual_symbol) as id,
    symbol, qual_symbol
from 'exp/prototype.json';

/* 型情報には重複して内容が含まれるため、一旦テンポラリなテーブルに入れる */
create or replace table temp_type as 
select 
    dense_rank() over (order by child.symbol, child.qual_symbol) as id,
    t.kind, t.parent.qual_symbol as qual_psymbol, unnest(child)
from 'exp/type.json' t
join prototype p on t.parent.qual_symbol = p.qual_symbol;

/* 重複を取り除いて型情報を投入する */
insert into type_symbol
select distinct id, symbol, qual_symbol from temp_type;

/* 関数プロトタイプと型情報を関連づける */
insert into type_ref by position
select distinct p.id as pid, t.id as tid, lower(kind)
from temp_type t
join prototype p on t.qual_psymbol = p.qual_symbol
;
```

`json`そのまま指定するだけなので`duckdb`便利。
ネストしていても、`unnest`関数でカジュアルに崩せるので`duckdb`便利。

例えば、以下のようなクエリを投げれば

```sql
/* 戻り値がStringな関数を列挙する */
select t.* from prototype t
where exists (
    select null from type_symbol ts1
    join type_ref ts2 on ts1.id = ts2.type_id
    where 
        ts2.prototype_id = t.id
        and ts2.kind = 'return'
        and ts1.symbol = 'String'
);
```

```
┌───────┬───────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│  id   │      symbol       │                                                qual_symbol                                                 │
│ int64 │      varchar      │                                                  varchar                                                   │
├───────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│  2539 │ into_string_lossy │ std::sys_common::wtf8::Wtf8Buf::into_string_lossy(std::sys_common::wtf8::Wtf8Buf) -> alloc::string::String │
│  2065 │ fill              │ std::panicking::FormatStringPayload::fill(std::panicking::FormatStringPayload) -> alloc::string::String    │
└───────┴───────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```

な結果が得られる。

## まとめ

DuckDBつよい (Rustどこいった・・・）。

[4日目](https://qiita.com/ktz_alias/items/a1956df141deda459b7d)へ続く