Help us understand the problem. What is going on with this article?

1 単純な字句解析をRustで実装する

はじめに

@quwaharaさんの1 単純な字句解析をJavaで実装するを参考にさせていただきました。ありがとうございます。
参考にした記事では、字句解析について詳しく解説されているのでこの記事を読む前に、一読してください。この記事ではRustで同じことをできないかなと思い書いてみました。Rust歴は浅いですが、とりあえずエラーなく動きました。Rustを勉強していてやることないと思っている方向けです。

環境

takumi@takumi:~/Desktop/my_compiler$ cargo -V
cargo 1.37.0 (9edd08916 2019-08-02)
takumi@takumi:~/Desktop/my_compiler$ rustup -V
rustup 1.18.3 (435397f48 2019-05-22)

実行結果

Tokenの出力部分の実装です。format!マクロはString型をつくるため戻り値は-> Stringです。self.kindself.valueの出力だけを行うため(値を変更しない)、この関数の引数は&selfでok。&mut selfにする必要はないです。
format!マクロ内の\tで出力結果を整えました。

main.rs
impl Token {
    pub fn to_string(&self) -> String {
        format!("kind: {},\t value: {}", self.kind, self.value)
    }
}
  • 解析した文字列: ans1 = 10 + 20
result
takumi@takumi:~/Desktop/my_compiler$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/my_compiler`
kind: variable,  value: ans1
kind: sign,      value: =
kind: digit,     value: 10
kind: sign,      value: +
kind: digit,     value: 20

main.rs

main.rs
use std::process;

#[derive(PartialEq)]
pub struct Token {
    pub kind:   String,
    pub value:  String
}

impl Token {
    pub fn to_string(&self) -> String {
        format!("kind: {},\t value: {}", self.kind, self.value)
    }
}

pub struct Lexer {
    text:   String,
    i:      usize
}

impl Lexer {
    pub fn init(text: String) -> Self {
        Self {
            text,
            i: 0
        }
    }

    fn is_eot(&self) -> bool {
        self.text.len() <= self.i
    }

    fn c(&self) -> char {
        if self.is_eot() {
            println!("No more character");
            process::exit(1);
        }

        self.text.chars().nth(self.i).unwrap()
    }

    fn next(&mut self) -> char {
        let c = self.c();
        self.i += 1;

        c
    }    

    fn skip_space(&mut self) {
        while !self.is_eot() && self.c().is_whitespace() {
            self.next();
        }
    }

    fn is_sign_start(c: char) -> bool {
        c == '=' || c == '+' || c == '-'|| c == '*'|| c == '/'
    }

    fn is_digit_start(c: char) -> bool {
        c.is_digit(10)
    }

    fn is_variable_start(c: char) -> bool {
        c.is_alphabetic()
    }

    fn sign(&mut self) -> Token {
        Token {
            kind: "sign".to_string(),
            value: self.next().to_string()
        }
    }

    fn digit(&mut self) -> Token {
        let mut s = String::new();
        s.push(self.next());
        while !self.is_eot() && self.c().is_digit(10) {
            s.push(self.next());
        }

        Token {
            kind: "digit".to_string(),
            value: s.to_string()
        }
    }

    fn variable(&mut self) -> Token {
        let mut s = String::new();
        s.push(self.next());
        while !self.is_eot() && (self.c().is_alphabetic() || self.c().is_digit(10)) {
            s.push(self.next());
        }

        Token {
            kind: "variable".to_string(),
            value: s.to_string()
        }
    }

    pub fn next_token(&mut self) -> Option<Token> {
        self.skip_space();
        if self.is_eot() {
            None
        } else if Self::is_sign_start(self.c()) {
            Some(self.sign())
        } else if Self::is_digit_start(self.c()) {
            Some(self.digit())
        } else if Self::is_variable_start(self.c()) {
            Some(self.variable())
        } else {
            println!("Not a character for tokens");
            process::exit(1);
        }
    }

    pub fn tokenize(&mut self) -> Vec<Option<Token>> {
        let mut tokens = Vec::new();
        let mut t = self.next_token();

        while t != None {
            tokens.push(t);
            t = self.next_token();
        }

        tokens
    }
}

fn main() {
    let text = "ans1 = 10 + 20".to_string();
    let tokens = Lexer::init(text).tokenize();

    for token in tokens {
        println!("{}", token.unwrap().to_string());
    }
}

複数ファイルに機能を分割

この先main.rsファイルの肥大化が懸念されるため、ファイルの分割を行います。 lib.rsを作りこのファイルにmod モジュールorpub mod モジュールを書いていく

変更箇所

main.rs

extern crate HOGEHOGE;はすでに外部クレートをインポートする時に使ったことがあるかもしれません。main.rs内ではextern crate my_compilerとしています。このmy_compilerは外部クレートではなくこのディレクトリの名前で、自分が開発したモジュールをこのディレクトリ内で使えるようにするため宣言する必要があります。

use my_compiler::lexer::Lexeruseを使ってモジュールをインポート。lexer.rs内のLexer構造体を指定しています。

main.rs
extern crate my_compiler;
use my_compiler::lexer::Lexer;

fn main() {
    let text = "ans1 = 10 + 20".to_string();
    let tokens = Lexer::init(text).tokenize();

    for token in tokens {
        println!("{}", token.unwrap().to_string());
    }
}

lib.rsを追加

lib.rsは必ずこの名前にしなければなりません。
modの前にpubを付けるとパブリック、付けなければプライベートに設定されます。

mod english;
こうすると、Rustは english.rs ファイルか、 english/mod.rs ファイルのどちらかにモジュールの内容があるだろうと予想します。

引用: クレートとモジュール

というわけなので、pub mod tokentoken.rspub mod lexerlexer.rsに記述されています。

lib.rs
pub mod token;
pub mod lexer;

token.rsを追加

token.rsmain.rsからとってきて貼り付けただけです。

token.rs
#[derive(PartialEq)]
pub struct Token {
    pub kind:   String,
    pub value:  String
}

impl Token {
    pub fn to_string(&self) -> String {
        format!("kind: {},\t value: {}", self.kind, self.value)
    }
}

lexer.rsを追加

use super::token::Token;superを使って相対パスでToken構造体を使えるようにインポート。

main.rsの1ファイルの時は関係なかったが、ファイルを複数にしたことでpub fn tokenizeにする必要があります。pubを付けないとプライベートな関数になるためmain.rsで使えません。

lexer.rs
    pub fn tokenize(&mut self) -> Vec<Option<Token>> {
        let mut tokens = Vec::new();
        let mut t = self.next_token();

        while t != None {
            tokens.push(t);
            t = self.next_token();
        }

        tokens
    }

実行結果

ちゃんと動きました~

takumi@takumi:~/Desktop/my_compiler/qiita_1/my_compiler/src$ tree .
.
├── lexer.rs
├── lib.rs
├── main.rs
└── token.rs

0 directories, 4 files
takumi@takumi:~/Desktop/my_compiler$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 21.88s
     Running `/home/takumi/Desktop/my_compiler/qiita_1/my_compiler/target/debug/my_compiler`
kind: variable,  value: ans1
kind: sign,      value: =
kind: digit,     value: 10
kind: sign,      value: +
kind: digit,     value: 20

tokens ( Vec<Option<Token>> )をもう一度表示させる

次のようにします。これは動きません。

main.rs
extern crate my_compiler;
use my_compiler::lexer::Lexer;

fn main() {
    let text = "ans1 = 10 + 20".to_string();
    let tokens = Lexer::init(text).tokenize();

    for token in tokens {
        println!("{}", token.unwrap().to_string());
    }

    println!("-------- again --------");

    for token in tokens {
        println!("{}", token.unwrap().to_string());   
    }

}
result
error[E0382]: use of moved value: `tokens`
  --> src/main.rs:14:18
   |
6  |     let tokens = Lexer::init(text).tokenize();
   |         ------ move occurs because `tokens` has type `std::vec::Vec<std::option::Option<my_compiler::token::Token>>`, which does not implement the `Copy` trait
7  | 
8  |     for token in tokens {
   |                  ------
   |                  |
   |                  value moved here
   |                  help: consider borrowing to avoid moving into the for loop: `&tokens`
...
14 |     for token in tokens {
   |                  ^^^^^^ value used here after move

error: aborting due to previous error

^^^^^^ value used here after move: 移動後の値を使っているとなり、errorになりました。1回目のfor文で所有権が移動したためこのようになりました。これを回避するにはfor token in &tokensにする必要があります。
その後as_ref()も追加します。println!("{}", token.as_ref().unwrap().to_string());

main.rs
extern crate my_compiler;
use my_compiler::lexer::Lexer;

fn main() {
    let text = "ans1 = 10 + 20".to_string();
    let tokens = Lexer::init(text).tokenize();

    for token in &tokens {
        println!("{}", token.as_ref().unwrap().to_string());
    }

    println!("-------- again --------");

    for token in &tokens {
        println!("{}", token.as_ref().unwrap().to_string());
    }
}
result
takumi@takumi:~/Desktop/my_compiler$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.66s
     Running `target/debug/my_compiler`
kind: variable,  value: ans1
kind: sign,  value: =
kind: digit,     value: 10
kind: sign,  value: +
kind: digit,     value: 20
-------- again --------
kind: variable,  value: ans1
kind: sign,  value: =
kind: digit,     value: 10
kind: sign,  value: +
kind: digit,     value: 20

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away