はじめに
現在,Rustの学習のためにインタプリタを実装してる.
参考にしてる本はおなじみの下記文献.
第1章の『字句解析』の字句解析器を無事に実装を終えたので,
Goで書かれたサンプルコードをRustに移植する際に行なったことをまとめていこうと思う.
字句解析器(Lexer)の実装まで,本文でいうところの第1章まで終わり(https://t.co/YKrD9wWH1Z).Goで書かれたサンプルをRustで置き換えててる.次は構文解析器(Parser)の実装.— Scstechr (@Scstechr) January 24, 2020
なお,字句解析器の解説についてはぜひ原著を購入して参照してほしい(宣伝).
本文
記事執筆時点のレポジトリはこちら.
コーディングルール
原著はGoで書かれているので命名規則がRustとは違う.命名規則→Go,Rust
簡単にいうと,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にはメンバとして現在検査中の文字を表すchをbyte型で実装してる.
Rustにはbyte型はないので,chはu8で実装し,残りのメンバもusizeにするなど工夫をした.
pub struct Lexer {
input: String,
position: usize,
read_position: usize,
ch: u8
}
なんでi32でなくusizeにしたかというと,
position/read_positionは他の箇所で配列のインデックスとして使用するからだ.
必要な箇所では,u8からcharに変換した.
例えば,次の文字を覗き見(peek)するpeek_char()(本文中ではpeekChar())の実装は以下のとおり.
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を用いた.
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(),
},
};
各Tokenはconst ASSIGN: &str = “=”;の形で実装している.
テストについて
原著ではおそらくHashMapのVector?を用いてテストを書いていた(Goなのでわからん).
さらに,何番目のTokenでエラー出たという情報を得るためにrangeを使っていた.
当初,自分もenumerateを使ったりstd::collections::HashMapを使っていたが,
最終的に以下のように落ち着いた.
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ループ内に記述する.
println!(
"tok: [{:#?}:{:#?}]\x1b[30Gt: [{:#?}:{:#?}]",
tok.Type, tok.Literal, t.Type, t.Literal
);
キーワードマッチにMapを使う
原著ではおしゃれにmapを使用する方法が紹介されていた.
筆者はまだRustのstd::iter::Mapを使いこなせてないのでおとなしく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)が実行される.
まだコードの量も少ないし,字句解析器単体の実装を追いたい場合は有用かもしれない.
願わくばモチベがこのまま持って完走したい.