Rustlingsのコードを読んでみる

## はじめに
最近Rustのモチベがあり、ちゃんとRustのコードを読んでみたい！ということでOSSのコードを読んでみることにした。[Rustlings](https://github.com/rust-lang/rustlings)というRustの文法をクイズ形式で学習できるCUIアプリがある。今回はこれを読むことにする。(あまり大規模なものだと挫折しそうなので、小さめのものを選んだ)
Ruslingsがどのようなものかはこちらのブログに説明を譲る。今回はCUIアプリで具体的にどのような処理の流れが行われているかを調べてみる。

https://yiskw713.hatenablog.com/entry/2023/01/04/213718

## デバッグをしよう
今回はCUIアプリということでビルドする際に`cargo install --path .`とする必要がある。これを間違えて`cargo build`とすると環境変数が~/.cargo/binの方を参照しているので.targetの方にバイナリファイルが生成されてしまい意味がないことになる。(1敗)
また、`error[E0635]: unknown feature`proc_macro_span_shrink\` というエラーが出て困った。こちらに関しては

https://ysuzuki19.github.io/post/rust-cargo-error-proc-macro-unknown

を参考にCargo.tomlを
```rust
serde = { version = "1.0.192", features = ["derive"] }
```
とserdeのversionを最新に書き換えたところ上手くいった。
色々デバッグしてみながらコードを読んでいく。main.rsのmain関数を読んでみるとargsが重要なカギを握っていそうなことが分かる。そこでArgsとSubcommandsに`#[derive(Debug)]`マクロを付けると、
```rust
println!("args: {:#?}", args);
```
でargsの中身を見ることができる。試しに`rustlings`を実行してみると
```sh
args: Args {
    nocapture: false,
    command: None,
}
```
となる。
また、自分のような初心者にとってある変数がどのような型をしているのはなかなか難しい。それを知るのに役立つのがtype_of関数だ。

https://scrapbox.io/nwtgck/Rust%E3%81%A7%E5%80%A4%E3%81%8B%E3%82%89%E5%9E%8B%E6%83%85%E5%A0%B1%E3%82%92%E6%96%87%E5%AD%97%E5%88%97%E3%81%A8%E3%81%97%E3%81%A6%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%EF%BC%88stable%EF%BC%89

を参考に変数を入れると型を返すような関数を作ることができる。

## 目的を決める

コードを読むうえで先人たちの教えとして「目的意識をもってコードを読む」というものがある。すべてのコードを読むことを目指すと挫折する。今回は「rustlings runが動く仕組みを理解する」ことを目標にする。


## プログラムを読む
### パース
```rust
let args = Args::parse();
```
この時点でパースは完了している。
Clapがマジで偉いことがわかる。

https://maku77.github.io/p/bdp2doy/

```rust
struct Args {
    /// Show outputs from the test exercises
    #[arg(long)]
    nocapture: bool,
    #[command(subcommand)]
    command: Option<Subcommands>,
}
```
これによって`nocapture`は"--nocapture"というコマンドのみを認識し、それ以外の`command`は全てcommandで処理される。ちなみに
```rust
#[arg(long)]
```
によって長い形式のオプション (`--nocapture`) のみ認識される。試しに`rustlings --nocapture`を実行すると
```sh
args: Args {
    nocapture: true,
    command: None,
}
```
と`nocapture`が`true`になることが分かる。ほかのコマンドが`command`に格納されることは、`rustlings hint primitive_types2`を実行すると
```sh
args: Args {
    nocapture: false,
    command: Some(
        Hint {
            name: "primitive_types1",
        },
    ),
}
```
となることからもわかる。`command`はSome型になっており、その中にHint型の構造体が格納されている。この構造体は
```rust:main.rs
enum Subcommands {
    Verify,
    /// Rerun `verify` when files were edited
    Watch {
        /// Show hints on success
        #[arg(long)]
        success_hints: bool,
    },
    ...
}
```
の部分で定義されている。

### 変数を読む

```rust:main.rs
    let toml_str = &fs::read_to_string("info.toml").unwrap();
    let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
    let verbose = args.nocapture;

    let command = args.command.unwrap_or_else(|| {
        println!("{DEFAULT_OUT}\n");
        std::process::exit(0);
    });
```

fsモジュールでファイル入出力が可能。`toml_str`はinfo.tomlを読み込んだString型。ファイルの中身は以下のような設定が一問ごとに続いている。

```toml:info.toml
[[exercises]]
name = "intro1"
path = "exercises/intro/intro1.rs"
mode = "compile"
hint = """
Remove the I AM NOT DONE comment in the exercises/intro/intro1.rs file
to move on to the next exercise."""
```
恥ずかしながらDeserializeというワードを知らなかった。文字列からあるデータ構造に変換することを言うらしい。RustではSerdeモジュールで変換をすることが可能。structを定義するときに`#[derive(Deserialize)]`とする必要がある。

https://cipepser.hatenablog.com/entry/rust-toml

`toml::from_str()::<ExerciseList>`でStringからExerciseListにDeserializeすることができる。

```rust:excerise.rs
// The mode of the exercise.
#[derive(Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
    // Indicates that the exercise should be compiled as a binary
    Compile,
    // Indicates that the exercise should be compiled as a test harness
    Test,
    // Indicates that the exercise should be linted with clippy
    Clippy,
}

#[derive(Deserialize)]
pub struct ExerciseList {
    pub exercises: Vec<Exercise>,
}

// A representation of a rustlings exercise.
// This is deserialized from the accompanying info.toml file
#[derive(Deserialize, Debug)]
pub struct Exercise {
    // Name of the exercise
    pub name: String,
    // The path to the file containing the exercise's source code
    pub path: PathBuf,
    // The mode of the exercise (Test, Compile, or Clippy)
    pub mode: Mode,
    // The hint text associated with the exercise
    pub hint: String,
}

```
`ExerciseList`の定義はこのようになっている。Rustでは`pub`がついている変数のみ別ファイルから参照可能となる。Pathを表せる`std::path::PathBuf`なんてあるんだ。

`command`は`args.command`の中身を読み込んでいる。

### exerciseの取得

その下にmatch式があり、ここでそれぞれのコマンドに応じた処理が行われる。

```rust:main.rs
match command {
        Subcommands::List {
            paths,
            names,
            filter,
            unsolved,
            solved,
        } => {
            ...
    }
```
commandがrun型の時、すなわちrustlings run hogeと入力したときを見ていく。このとき
```rust
        Subcommands::Run { name } => {
            let exercise = find_exercise(&name, &exercises);

            run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1));
        }
```
となっているので`find_exercise(&name, &exercises)`が呼ばれる。
```rust:main.rs
fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise {
    if name.eq("next") {
        exercises
            .iter()
            .find(|e| !e.looks_done())
            .unwrap_or_else(|| {
                println!("🎉 Congratulations! You have done all the exercises!");
                println!("🔚 There are no more exercises to do next!");
                std::process::exit(1)
            })
    } else {
        exercises
            .iter()
            .find(|e| e.name == name)
            .unwrap_or_else(|| {
                println!("No exercise found for '{name}'!");
                std::process::exit(1)
            })
    }
}
```
`name`がnextであるかによって処理の流れが変わる。もしnextでなければ、exercisesを順番に見ていって、`name`が一致するようなものを返す。もしnextであれば、`find(|e| !e.looks_done())`で`self.state()`が`State::Done`ではない初めてのものを返す。
```rust:excerise.rs
pub fn looks_done(&self) -> bool {
        self.state() == State::Done
    }
```
`self.state()`の実装は以下のようになっている。
```rust:excerise.rs
    pub fn state(&self) -> State {
        let mut source_file = File::open(&self.path).unwrap_or_else(|e| {
            panic!(
                "We were unable to open the exercise file {}! {e}",
                self.path.display()
            )
        });
        let source = {
            let mut s = String::new();
            source_file.read_to_string(&mut s).unwrap_or_else(|e| {
                panic!(
                    "We were unable to read the exercise file {}! {e}",
                    self.path.display()
                )
            });
            s
        };

        let re = Regex::new(I_AM_DONE_REGEX).unwrap();

        if !re.is_match(&source) {
            return State::Done;
        }

        let matched_line_index = source
            .lines()
            .enumerate()
            .find_map(|(i, line)| if re.is_match(line) { Some(i) } else { None })
            .expect("This should not happen at all");

        let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize;
        let max_line = matched_line_index + CONTEXT;

        let context = source
            .lines()
            .enumerate()
            .filter(|&(i, _)| i >= min_line && i <= max_line)
            .map(|(i, line)| ContextLine {
                line: line.to_string(),
                number: i + 1,
                important: i == matched_line_index,
            })
            .collect();

        State::Pending(context)
    }
```
ソースを読んできて、Regexで正規表現にマッチするかの検索を行っている。

```rust
        let re = Regex::new(I_AM_DONE_REGEX).unwrap();

        if !re.is_match(&source) {
            return State::Done;
        }
```
で正規表現にマッチしなかったら(="// I AM NOT DONE"を消していたら)`State::Done`を返す、という処理をしている。正規表現として
```rust
const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
```
と定義されている。

そして持ってきた`exercise`に対して`run(exercise, verbose)`が実行される。

### exerciseの実行
`run(exercise, verbose)`の実装は以下。

```rust:exercise.rs
pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
    match exercise.mode {
        Mode::Test => test(exercise, verbose)?,
        Mode::Compile => compile_and_run(exercise)?,
        Mode::Clippy => compile_and_run(exercise)?,
    }
    Ok(())
}
```
どうやら`mode`の値によって処理が異なるようだ。
```rust:exercise.rs
// The mode of the exercise.
#[derive(Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
    // Indicates that the exercise should be compiled as a binary
    Compile,
    // Indicates that the exercise should be compiled as a test harness
    Test,
    // Indicates that the exercise should be linted with clippy
    Clippy,
}
```
Modeの定義はこのようになっている。今回は`compile_and_run`を見ていく。

```rust:run.rs
// Invoke the rust compiler on the path of the given exercise
// and run the ensuing binary.
// This is strictly for non-test binaries, so output is displayed
fn compile_and_run(exercise: &Exercise) -> Result<(), ()> {
    let progress_bar = ProgressBar::new_spinner();
    progress_bar.set_message(format!("Compiling {exercise}..."));
    progress_bar.enable_steady_tick(Duration::from_millis(100));

    let compilation_result = exercise.compile();
    let compilation = match compilation_result {
        Ok(compilation) => compilation,
        Err(output) => {
            progress_bar.finish_and_clear();
            warn!(
                "Compilation of {} failed!, Compiler error message:\n",
                exercise
            );
            println!("{}", output.stderr);
            return Err(());
        }
    };

    progress_bar.set_message(format!("Running {exercise}..."));
    let result = compilation.run();
    progress_bar.finish_and_clear();

    match result {
        Ok(output) => {
            println!("{}", output.stdout);
            success!("Successfully ran {}", exercise);
            Ok(())
        }
        Err(output) => {
            println!("{}", output.stdout);
            println!("{}", output.stderr);

            warn!("Ran {} with errors", exercise);
            Err(())
        }
    }
}

```
ProgressBarモジュール便利そう。`compilation_result`がコンパイルの結果。compileは以下のように定義されている。

```rust:exercise.rs
pub fn compile(&self) -> Result<CompiledExercise, ExerciseOutput> {
        let cmd = match self.mode {
            Mode::Compile => Command::new("rustc")
                .args([self.path.to_str().unwrap(), "-o", &temp_file()])
                .args(RUSTC_COLOR_ARGS)
                .args(RUSTC_EDITION_ARGS)
                .args(RUSTC_NO_DEBUG_ARGS)
                .output(),
            Mode::Test => Command::new("rustc")
                .args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()])
                .args(RUSTC_COLOR_ARGS)
                .args(RUSTC_EDITION_ARGS)
                .args(RUSTC_NO_DEBUG_ARGS)
                .output(),
            Mode::Clippy => {
                let cargo_toml = format!(
                    r#"[package]
name = "{}"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "{}"
path = "{}.rs""#,
                    self.name, self.name, self.name
                );
                let cargo_toml_error_msg = if env::var("NO_EMOJI").is_ok() {
                    "Failed to write Clippy Cargo.toml file."
                } else {
                    "Failed to write 📎 Clippy 📎 Cargo.toml file."
                };
                fs::write(CLIPPY_CARGO_TOML_PATH, cargo_toml).expect(cargo_toml_error_msg);
                // To support the ability to run the clippy exercises, build
                // an executable, in addition to running clippy. With a
                // compilation failure, this would silently fail. But we expect
                // clippy to reflect the same failure while compiling later.
                Command::new("rustc")
                    .args([self.path.to_str().unwrap(), "-o", &temp_file()])
                    .args(RUSTC_COLOR_ARGS)
                    .args(RUSTC_EDITION_ARGS)
                    .args(RUSTC_NO_DEBUG_ARGS)
                    .output()
                    .expect("Failed to compile!");
                // Due to an issue with Clippy, a cargo clean is required to catch all lints.
                // See https://github.com/rust-lang/rust-clippy/issues/2604
                // This is already fixed on Clippy's master branch. See this issue to track merging into Cargo:
                // https://github.com/rust-lang/rust-clippy/issues/3837
                Command::new("cargo")
                    .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH])
                    .args(RUSTC_COLOR_ARGS)
                    .output()
                    .expect("Failed to run 'cargo clean'");
                Command::new("cargo")
                    .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH])
                    .args(RUSTC_COLOR_ARGS)
                    .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"])
                    .output()
            }
        }
        .expect("Failed to run 'compile' command.");

        if cmd.status.success() {
            Ok(CompiledExercise {
                exercise: self,
                _handle: FileHandle,
            })
        } else {
            clean();
            Err(ExerciseOutput {
                stdout: String::from_utf8_lossy(&cmd.stdout).to_string(),
                stderr: String::from_utf8_lossy(&cmd.stderr).to_string(),
            })
        }
    }
```
Commandモジュールでコマンドラインを通した命令を行うことができる。`arg`でコマンドライン引数を与え、`output()`でプロセスを実行することができる。Clippyの場合はなんかいろいろする必要があって大変そう。

https://keens.github.io/blog/2016/12/02/rustnopurosesu/

```rust
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"];
const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"];
```
定数はこのようになっている。
`--color always`：コンパイル結果に色を付ける。
`--edition　2021`：editionを指定する。あまり意図が分からないが、editionの違いによって必ずしも互換性は保証されないようだ。

https://doc.rust-jp.rs/edition-guide/editions/index.html

`-C strip=debuginfo`:デバッグシンボルのみを除去するコンパイルオプション。バイナリサイズを小さくすることができるらしい。

https://tech-blog.optim.co.jp/entry/2022/01/14/080000#rustc-strip


コンパイルの結果は`cmd.status`によって分けられる。`OK`の場合は`CompiledExercise`が返される。`CompiledExercise`で定義された`run`を呼ぶと、`Execerise`内の`run`が呼ばれる。

```rust:exercise.rs

impl<'a> CompiledExercise<'a> {
    // Run the compiled exercise
    pub fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
        self.exercise.run()
    }
}
```

```rust:exercise.rs
fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
        let arg = match self.mode {
            Mode::Test => "--show-output",
            _ => "",
        };
        let cmd = Command::new(temp_file())
            .arg(arg)
            .output()
            .expect("Failed to run 'run' command");

        let output = ExerciseOutput {
            stdout: String::from_utf8_lossy(&cmd.stdout).to_string(),
            stderr: String::from_utf8_lossy(&cmd.stderr).to_string(),
        };

        if cmd.status.success() {
            Ok(output)
        } else {
            Err(output)
        }
    }
```

`String::from_utf8_lossy`を使うとUTF-8として有効でない部分を「�」に変換することができる。


## 感想
ソフトウェアをいじってて実装どのようになっているんだろう？と思ったときに実際にコードを読んでみると色々学びがあるし、知的好奇心も満たされて楽しかった。関数がどのように呼び出されてるか眺めるだけでこのような流れで処理されているんだな～となるし、チュートリアルで学んだことが出てくるとこれ進研ゼミでやったところだ！となる。それだけでは学べないこともたくさん出てきた。そして、分からないことについての記事を書いている先人が本当に神のように思えてくる。7年前に書いた記事に助けられると時空を超えて助けられているという感じがあってとてもよい。この記事も誰かの助けになれたらうれしい。また、ここが間違っているといった指摘があればぜひお願いしたい。