LoginSignup
7
2

More than 3 years have passed since last update.

『Go言語でつくるインタプリタ』をRustで実装 ①字句解析器

Last updated at Posted at 2020-01-24

はじめに

現在,Rustの学習のためにインタプリタを実装してる.
参考にしてる本はおなじみの下記文献.

picture978-4-87311-822-2.gif
『Go言語でつくるインタプリタ』
Thorsten Ball著、設樂 洋爾 訳,
オライリー社,2018年06月 発行

第1章の『字句解析』の字句解析器を無事に実装を終えたので,
Goで書かれたサンプルコードをRustに移植する際に行なったことをまとめていこうと思う.

なお,字句解析器の解説についてはぜひ原著を購入して参照してほしい(宣伝).

本文

記事執筆時点のレポジトリはこちら

コーディングルール

原著はGoで書かれているので命名規則がRustとは違う.命名規則→GoRust
簡単にいうと,Goでは多くの場合でcamelCaseになるところはRustだとsnake_caseになると思う.
本文中のコードをそのまま移植するとコンパイラからWarning!を大量に投げられるので,
一時的に各コードの先頭に下記を追記することにした(のちに削除する予定).

コンパイラの警告を無視する
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(unused_imports)]

#![allow(unused_imports)]を入れてるのは,
useだけ宣言してまだ実装しない・使用しないなどの事象が多かったためである.
また,デバッグ用に作っただけの関数などは#[allow(dead_code)]をつけたりした.

Rustにはbyte型がない

本文中のtype Lexer structにはメンバとして現在検査中の文字を表すchbyte型で実装してる.
Rustにはbyte型はないので,chu8で実装し,残りのメンバもusizeにするなど工夫をした.

Lexer
pub struct Lexer {
  input: String,
  position: usize,
  read_position: usize,
  ch: u8
}

なんでi32でなくusizeにしたかというと,
position/read_positionは他の箇所で配列のインデックスとして使用するからだ.
必要な箇所では,u8からcharに変換した.
例えば,次の文字を覗き見(peek)するpeek_char()(本文中ではpeekChar())の実装は以下のとおり.

peek_char()の実装
fn peek_char(&self) {
  if self.read_position >= self.input.len() {
    char::from(0)
  } else {
    char::from(self.input.as_bytes()[self.position + 1])
  }
}

switch/caseの代わりのmatch

読み込んだ文字を原著ではswitch/caseを用いて各Tokenとして識別している.
当然Rustにはswitch/caseはないので,代わりにmatchを用いた.

match
let tok: token::Token = match self.ch {
  b'=' => new_token(token::ASSIGN, self.ch),
  b'+' => new_token(token::PLUS, self.ch),
  ...
  _ => token::Token {
    Type: token::EOF.to_string(),
    Literal: "".to_string(),
  },
};

Tokenconst ASSIGN: &str = "=";の形で実装している.

テストについて

原著ではおそらくHashMapのVector?を用いてテストを書いていた(Goなのでわからん).
さらに,何番目のTokenでエラー出たという情報を得るためにrangeを使っていた.
当初,自分もenumerateを使ったりstd::collections::HashMapを使っていたが,
最終的に以下のように落ち着いた.

test
fn lexer_simple_test() {
   use crate::lexer;
   use crate::token;

   let input = "=+(){},;";
   let tests = vec![
       lexer::new_token(token::ASSIGN, "=".as_bytes()),
       lexer::new_token(token::PLUS, "+".as_bytes()),
       lexer::new_token(token::LPAREN, "(".as_bytes()),
       lexer::new_token(token::RPAREN, ")".as_bytes()),
       lexer::new_token(token::LBRACE, "{".as_bytes()),
       lexer::new_token(token::RBRACE, "}".as_bytes()),
       lexer::new_token(token::COMMA, ",".as_bytes()),
       lexer::new_token(token::SEMICOLON, ";".as_bytes()),
       lexer::new_token(token::EOF, "".as_bytes()),
   ];
   let mut l = lexer::new(input);
   for t in tests.iter() {
       let tok = l.next_token();
       assert_eq!(tok.Type, t.Type);
       assert_eq!(tok.Literal, t.Literal);
   }
}

詳しい情報が欲しい場合は以下をforループ内に記述する.

test
println!(
   "tok: [{:#?}:{:#?}]\x1b[30Gt: [{:#?}:{:#?}]",
   tok.Type, tok.Literal, t.Type, t.Literal
);

キーワードマッチにMapを使う

原著ではおしゃれにmapを使用する方法が紹介されていた.
筆者はまだRuststd::iter::Mapを使いこなせてないのでおとなしくmatchで対応した.

matchを使う
let literal = self.read_identifier();
let t = match literal.as_str() {
    "fn" => token::FUNCTION.to_string(),
    "let" => token::LET.to_string(),
    ...
    _ => token::ID.to_string(),
};

この部分もゆくゆくはmapで置き換えたい.

おわり

冒頭のレポジトリをcloneしてcargo buildしてもらえれば動く字句解析器が手に入る.
makeすればREPL(Read-Eval-Print-Loop)ではなくRPL(Read-Print-Loop)が実行される.
まだコードの量も少ないし,字句解析器単体の実装を追いたい場合は有用かもしれない.

願わくばモチベがこのまま持って完走したい.

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2