Open11

[練習]低レイヤを知りたい人のためのCコンパイラ作成入門をRustで書いてみる

Yos_KYos_K

最初のコード

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        panic!("引数の個数が正しくありません")
    }

    println!(".intel_syntax noprefix");
    println!(".globl main");
    println!("main:");
    println!("  mov rax, {}", args[1]);
    println!("  ret");
}

まずは、こんなかんじ?

$ cargo run 123
   Compiling cc9 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 2.65s
     Running `target/debug/cc9 123`
.intel_syntax noprefix
.globl main
main:
  mov rax, 123
  ret
$ cargo run 123 > tmp.s
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/cc9 123`

$ cat tmp.s
.intel_syntax noprefix
.globl main
main:
  mov rax, 123
  ret

$ cc -o tmp tmp.s
$ ./tmp
$ echo $?
123

ひとまず同様の出力が得られた

Yos_KYos_K

CのstrtolをRustでどう表現するかが難しそう・・・

Yos_KYos_K

ステップ2:加減算のできるコンパイラの作成

まずは普通にループさせてみた

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        panic!("引数の個数が正しくありません")
    }

    let mut p = args[1].as_str().chars();

    println!(".intel_syntax noprefix");
    println!(".globl main");
    println!("main:");
    println!("  mov rax, {}", p.next().unwrap() as i32 - 48);

    loop {
        let c = p.next();
        match c {
            Some(a) => {
                match a {
                    '+' => println!("  add rax, {}", p.next().unwrap() as i32 - 48),
                    '-' => println!("  sub rax, {}", p.next().unwrap() as i32 - 48),
                    _ => println!("予期しない文字です: {}", a)
                }
            },
            None => {break;}
        }
    }
    
    println!("  ret");
}
$ cargo run "5+20-4"
   Compiling cc9 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/cc9 5+20-4`
.intel_syntax noprefix
.globl main
main:
  mov rax, 5
  add rax, 2
予期しない文字です: 0
  sub rax, 4
  ret

単純にnextを呼び出すだけだと2桁の整数がうまくパースできない。
もう少し工夫が必要

Yos_KYos_K

いきなり実装するのは無理そうだったのでテストから書く

pub fn str_to_u<T: Iterator<Item=char>>(iterator: &mut Peekable<T>) -> u32 {
    1
}

#[cfg(test)]
extern crate speculate;
extern crate rstest;

use speculate::speculate;
use rstest::*;

speculate! {
    describe "str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数" {
        #[rstest]
        fn _1_を受け取った場合u32型にして返す() {
            let mut c = "1".chars().peekable();
            assert_eq!(1, str_to_u(&mut c));
        }
    }
}
Yos_KYos_K

きれいに書けた!(テストはあとでちゃんと整理)

pub fn str_to_u<T: Iterator<Item=char>>(iter: &mut Peekable<T>) -> u32 {
    let mut result: u32 = 0;

    while let Some(i) = iter.peek() {
        match i.to_digit(10) {
            Some(num) => result = result*10 + num,
            None => break,
        }
        iter.next();
    }
    result
}

#[cfg(test)]
extern crate speculate;
extern crate rstest;

use speculate::speculate;
use rstest::*;

speculate! {
    describe "str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数" {
        #[rstest]
        fn _1_を受け取った場合u32型にして返す() {
            let mut c = "1".chars().peekable();
            assert_eq!(1, str_to_u(&mut c));
        }
        #[rstest]
        fn _2_を受け取った場合u32にして返す() {
            let mut c = "2".chars().peekable();
            assert_eq!(2, str_to_u(&mut c));
        }
        #[rstest]
        fn _10_を受け取った場合u32にして返す() {
            let mut c = "10".chars().peekable();
            assert_eq!(10, str_to_u(&mut c));
        }
    }
}
Yos_KYos_K

main関数を修正

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        panic!("引数の個数が正しくありません")
    }

    let mut p = args[1].chars().peekable();

    println!(".intel_syntax noprefix");
    println!(".globl main");
    println!("main:");
    println!("  mov rax, {}", str_to_u(&mut p));

    while let Some(c) = p.next() {
        match c {
            '+' => println!("  add rax, {}", str_to_u(&mut p)),
            '-' => println!("  sub rax, {}", str_to_u(&mut p)),
            _ => println!("予期しない文字です: {}", c),
        }
    }    
    println!("  ret");
}
$ cargo run -- "5+20-4"
   Compiling cc9 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.24s
     Running `target/debug/cc9 5+20-4`
.intel_syntax noprefix
.globl main
main:
  mov rax, 5
  add rax, 20
  sub rax, 4
  ret
Yos_KYos_K

テストも整理

speculate! {
    describe "str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数" {
        #[rstest(input, expected,
            case::文字列1を受け取った場合u32型の1を返す("1", 1),
            case::文字列10を受け取った場合u32型の10を返す("10", 10),
            case::文字列100を受け取った場合u32型の100を返す("100", 100)
        )]
        fn 数値を文字列で受け取ってu32に変換するテスト(input: &str, expected: u32) {
            let mut c = input.chars().peekable();
            assert_eq!(expected, str_to_u(&mut c));
        }
    }
}
$ cargo test
   Compiling cc9 v0.1.0
    Finished test [unoptimized + debuginfo] target(s) in 2.20s
     Running unittests (target/debug/deps/cc9-b1f3302b0d777d91)

running 3 tests
test speculate_0::str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数::数値を文字列で受け取ってu32に変換するテスト::case_2_文字列10を受け取った場合u32型の10を返す ... ok
test speculate_0::str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数::数値を文字列で受け取ってu32に変換するテスト::case_1_文字列1を受け取った場合u32型の1を返す ... ok
test speculate_0::str_to_uは文字列を受け取って数値に変換できる場合はu32型に変換する関数::数値を文字列で受け取ってu32に変換するテスト::case_3_文字列100を受け取った場合u32型の100を返す ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Yos_KYos_K

不正な文字を受け取った場合にエラーになるようにResult型を返したいね