← 解説「JavaScript の字句解析 — 識別子と予約語、数値、文字列、コメント」に戻る
サンプルコードで身につける: JavaScript の字句解析 — 識別子と予約語、数値、文字列、コメント
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1識別子と予約語、数値
JsLexer の骨組みです。は先頭が英字・_・$、2 文字目以降は数字も可という規則で読んでから、KEYWORDS の表にあるかで と Identifier に振り分けます。数値は数字と '.' の連続を f64 にします。実行すると var count = total + 1.5; が 7 個のになり、return1 や $el は識別子で function だけが予約語になることが分かります。
Rust
// 字句解析(lexer): 文字の列をトークン(意味のある最小単位)の列にする。
// 識別子と予約語は同じ規則で読んでから、予約語の表にあるかどうかで振り分ける。数値は数字と '.' の連続。
enum Token {
Punctuator(char), // + - = ( ) { } ; . ,
Number(f64),
Identifier(String),
Keyword(String), // var let const function return
}
/// 表示用(どの種類で、中身は何か)
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Token::Punctuator(c) => write!(f, "Punctuator({:?})", c),
Token::Number(n) => write!(f, "Number({:?})", n),
Token::Identifier(s) => write!(f, "Identifier({:?})", s),
Token::Keyword(s) => write!(f, "Keyword({:?})", s),
}
}
}
const KEYWORDS: &[&str] = &["var", "let", "const", "function", "return"];
struct JsLexer { chars: Vec<char>, pos: usize }
impl JsLexer {
fn new(src: &str) -> Self {
JsLexer { chars: src.chars().collect(), pos: 0 }
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
/// 条件を満たす間、文字を集める
fn take_while(&mut self, cond: fn(char) -> bool) -> String {
let mut s = String::new();
while let Some(c) = self.peek() {
if !cond(c) {
break;
}
s.push(c);
self.pos += 1;
}
s
}
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '$'
}
fn is_ident_part(c: char) -> bool {
is_ident_start(c) || c.is_ascii_digit() // 2 文字目以降は数字も可
}
impl Iterator for JsLexer {
type Item = Token;
fn next(&mut self) -> Option<Token> {
self.take_while(|c| c.is_whitespace()); // 空白は読み飛ばす
let c = self.peek()?;
if c.is_ascii_digit() {
let s = self.take_while(|c| c.is_ascii_digit() || c == '.');
return Some(Token::Number(s.parse().unwrap_or(0.0)));
}
if is_ident_start(c) {
let s = self.take_while(is_ident_part);
return Some(if KEYWORDS.contains(&s.as_str()) { Token::Keyword(s) } else { Token::Identifier(s) });
}
self.pos += 1;
Some(Token::Punctuator(c))
}
}
fn main() {
for src in ["var count = total + 1.5;", "return1 $el _tmp function"].iter() {
println!("{}", src);
for t in JsLexer::new(src) {
println!(" {}", t);
}
}
}
2文字列のエスケープと // コメント
文字列は開いた引用符と同じ引用符で閉じ、\ が来たら「次の 1 文字」を読んで \n や \" を解釈する read_string です。// から行末までは skip_whitespace_and_comments が空白と同じように捨てるので、トークンになりません。実行すると "He said \"hi\"\n" が 1 つの Str になり、コメントの後ろの 'it\'s' + 'a\tb' が 3 つのトークンとして続きます。
Rust
// 文字列リテラルとコメント。文字列は開いた引用符と同じ引用符で閉じ、\n \t \" のエスケープは「次の 1 文字」を読んで解釈する。
// // から行末までのコメントは、空白と同じように読み飛ばす(トークンにならない)。
enum Token {
Punctuator(char),
Str(String),
Identifier(String),
}
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Token::Punctuator(c) => write!(f, "Punctuator({:?})", c),
Token::Str(s) => write!(f, "Str({:?})", s), // {:?} なので改行は \n と見える
Token::Identifier(s) => write!(f, "Identifier({:?})", s),
}
}
}
struct JsLexer { chars: Vec<char>, pos: usize }
impl JsLexer {
fn new(src: &str) -> Self {
JsLexer { chars: src.chars().collect(), pos: 0 }
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn skip_whitespace_and_comments(&mut self) {
while let Some(c) = self.peek() {
if c.is_whitespace() {
self.pos += 1;
} else if c == '/' && self.chars.get(self.pos + 1) == Some(&'/') {
while self.peek().map_or(false, |c| c != '\n') {
self.pos += 1; // 行末まで捨てる
}
} else {
break;
}
}
}
/// 開いた引用符 quote を読んだ直後から、閉じる引用符まで
fn read_string(&mut self, quote: char) -> String {
let mut s = String::new();
while let Some(d) = self.peek() {
self.pos += 1;
if d == quote {
break;
}
if d == '\\' {
if let Some(e) = self.peek() {
self.pos += 1;
s.push(match e { 'n' => '\n', 't' => '\t', other => other }); // \" や \\ はその文字
}
continue;
}
s.push(d);
}
s
}
}
impl Iterator for JsLexer {
type Item = Token;
fn next(&mut self) -> Option<Token> {
self.skip_whitespace_and_comments();
let c = self.peek()?;
self.pos += 1;
match c {
'"' | '\'' => Some(Token::Str(self.read_string(c))),
c if c.is_ascii_alphabetic() => {
let mut s = c.to_string();
while let Some(d) = self.peek().filter(|d| d.is_ascii_alphanumeric()) {
s.push(d);
self.pos += 1;
}
Some(Token::Identifier(s))
}
other => Some(Token::Punctuator(other)),
}
}
}
fn main() {
let src = "el.textContent = \"He said \\\"hi\\\"\\n\"; // 2 行目に続く\n'it\\'s' + 'a\\tb'";
println!("{}\n--", src);
for t in JsLexer::new(src) {
println!("{}", t);
}
}
3実務に写す: 検索条件の式を字句解析する
status = "open" AND age > 30 のようなフィルタ式の です。予約語の表を and / or / not に替え、コメント記号を # に替えただけで、 と pos で進める形は JavaScript の JsLexer と同じです。実行すると AND が小文字の Keyword("and") になり、# 以降のコメントは消え、改行の先の or not assigned_to も読めていることが分かります。
Rust
// 実務に写す: 検索条件の式(status = "open" and age > 30)を字句解析する。
// 予約語の表を and / or / not に替え、コメント記号を # に替えるだけで、JavaScript の lexer と同じ形になる。
enum Token {
Punctuator(char), // = > < ( )
Number(f64),
Str(String),
Identifier(String),
Keyword(String), // and or not
}
/// 表示用(どの種類で、中身は何か)
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Token::Punctuator(c) => write!(f, "Punctuator({:?})", c),
Token::Number(n) => write!(f, "Number({:?})", n),
Token::Str(s) => write!(f, "Str({:?})", s),
Token::Identifier(s) => write!(f, "Identifier({:?})", s),
Token::Keyword(s) => write!(f, "Keyword({:?})", s),
}
}
}
const KEYWORDS: &[&str] = &["and", "or", "not"];
struct FilterLexer { chars: Vec<char>, pos: usize }
impl FilterLexer {
fn new(src: &str) -> Self {
FilterLexer { chars: src.chars().collect(), pos: 0 }
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn take_while(&mut self, cond: fn(char) -> bool) -> String {
let mut s = String::new();
while let Some(c) = self.peek().filter(|c| cond(*c)) {
s.push(c);
self.pos += 1;
}
s
}
}
impl Iterator for FilterLexer {
type Item = Token;
fn next(&mut self) -> Option<Token> {
loop {
self.take_while(|c| c.is_whitespace());
if self.peek() == Some('#') {
self.take_while(|c| c != '\n'); // # から行末はコメント
} else {
break;
}
}
let c = self.peek()?;
if c.is_ascii_digit() {
let s = self.take_while(|c| c.is_ascii_digit() || c == '.');
return Some(Token::Number(s.parse().unwrap_or(0.0)));
}
if c.is_ascii_alphabetic() || c == '_' {
let s = self.take_while(|c| c.is_ascii_alphanumeric() || c == '_');
let is_keyword = KEYWORDS.contains(&s.to_ascii_lowercase().as_str()); // AND も and も予約語
return Some(if is_keyword { Token::Keyword(s.to_ascii_lowercase()) } else { Token::Identifier(s) });
}
self.pos += 1;
if c == '"' || c == '\'' {
let s = self.take_while(|c| c != '"' && c != '\''); // 簡略: エスケープは扱わない
self.pos += 1; // 閉じる引用符
return Some(Token::Str(s));
}
Some(Token::Punctuator(c))
}
}
fn main() {
let src = "status = \"open\" AND age > 30 # 未対応で 30 日より古いもの\nor not assigned_to";
println!("{}\n--", src);
for t in FilterLexer::new(src) {
println!("{}", t);
}
}