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


はじめに

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


『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)が実行される.
まだコードの量も少ないし,字句解析器単体の実装を追いたい場合は有用かもしれない.

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