はじめに
現在,Rust
の学習のためにインタプリタを実装してる.
参考にしてる本はおなじみの下記文献.
『Go言語でつくるインタプリタ』
Thorsten Ball著、設樂 洋爾 訳,
オライリー社,2018年06月 発行
第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)が実行される.
まだコードの量も少ないし,字句解析器単体の実装を追いたい場合は有用かもしれない.
願わくばモチベがこのまま持って完走したい.