はじめに
JavaScriptはブラウザやNode.jsのJavaScriptエンジンで処理されてコードが動作します。
そんな大枠で理解していたことを正しく理解してみようと思い、いくつかのステップに分けて書いてみようと思います。
JavaScriptが動く流れ
1.ソースコード
2.実行環境へ入力
3.字句解析(レキサー)
4.構文解析(パーサ)
5.中間表現(IR)生成
6.バイトコード生成
7.JITコンパイル
8.実行
1.ソースコード
開発者が記述するソースコードそのものです。
人間が読める文字の状態で記述されます。
function greet(name) {
console.log("Hello, " + name);
}
2.実行環境へ入力
記述したソースコードがブラウザ(Chrome、Firefox)やNode.jsのJavaScriptエンジンに渡されます。
JavaScriptエンジンとは
JavaScriptコードを処理し、コンピュータが実行できる形式に変換して動作させるためのソフトウェアコンポーネントです。
主要なJavaScriptエンジン
- V8: Google ChromeやNode.jsで使用されているエンジン
- SpiderMonkey: Mozilla Firefoxで使用されているエンジン
- JavaScriptCore(Nitro): AppleのSafariで使用されているエンジン
- Bun: JavaScriptとTypeScriptを実行するための最新エンジン
3.字句解析(レキサー)
ソースコード(人間の理解できるテキストデータ)を意味のある最小単位(トークン)に分解します。
簡易的なレキサーを作成し、字句解析を行ってみる。
検証環境
Apple clang version 16.0.0 (clang-1600.0.26.4)
ソースコード
let x = 10 + 20;
出力
Token(Type: KEYWORD, Value: "let")
Token(Type: IDENTIFIER, Value: "x")
Token(Type: OPERATOR, Value: "=")
Token(Type: NUMBER, Value: "10")
Token(Type: OPERATOR, Value: "+")
Token(Type: NUMBER, Value: "20")
Token(Type: SEMICOLON, Value: ";")
簡易的なレキサー
#include <iostream>
#include <string>
#include <vector>
#include <regex>
using namespace std;
// トークンの種類を列挙型で定義
enum TokenType {
KEYWORD,
IDENTIFIER,
NUMBER,
OPERATOR,
SEMICOLON,
UNKNOWN
};
// トークンを表現する構造体
struct Token {
TokenType type;
string value;
};
// レキサー関数
vector<Token> lex(const string& code) {
vector<Token> tokens;
size_t pos = 0;
// トークンに対応する正規表現
regex keyword_regex(R"(\b(let|const|var)\b)");
regex identifier_regex(R"([a-zA-Z_][a-zA-Z0-9_]*)");
regex number_regex(R"(\b\d+\b)");
regex operator_regex(R"([=+\-*/])");
regex semicolon_regex(R"(;)");
while (pos < code.size()) {
// 現在の位置以降のコードを取得
string remaining_code = code.substr(pos);
smatch match;
if (regex_search(remaining_code, match, keyword_regex) && match.position(0) == 0) {
tokens.push_back({KEYWORD, match.str(0)});
} else if (regex_search(remaining_code, match, identifier_regex) && match.position(0) == 0) {
tokens.push_back({IDENTIFIER, match.str(0)});
} else if (regex_search(remaining_code, match, number_regex) && match.position(0) == 0) {
tokens.push_back({NUMBER, match.str(0)});
} else if (regex_search(remaining_code, match, operator_regex) && match.position(0) == 0) {
tokens.push_back({OPERATOR, match.str(0)});
} else if (regex_search(remaining_code, match, semicolon_regex) && match.position(0) == 0) {
tokens.push_back({SEMICOLON, match.str(0)});
} else if (isspace(remaining_code[0])) {
// 空白文字は無視
pos++;
continue;
} else {
tokens.push_back({UNKNOWN, string(1, remaining_code[0])});
}
// マッチした文字列の長さだけ進める
pos += match.length(0);
}
return tokens;
}
// トークンタイプを文字列で表示する関数
string tokenTypeToString(TokenType type) {
switch (type) {
case KEYWORD: return "KEYWORD";
case IDENTIFIER: return "IDENTIFIER";
case NUMBER: return "NUMBER";
case OPERATOR: return "OPERATOR";
case SEMICOLON: return "SEMICOLON";
default: return "UNKNOWN";
}
}
// メイン関数
int main() {
string code = "let x = 10 + 20;";
// レキサー実行
vector<Token> tokens = lex(code);
// トークンを出力
for (const auto& token : tokens) {
cout << "Token(Type: " << tokenTypeToString(token.type)
<< ", Value: \"" << token.value << "\")" << endl;
}
return 0;
}
プログラムの主要な部分を説明します
インクルード文と名前空間
#include <iostream>
#include <string>
#include <vector>
#include <regex>
using namespace std;
-
#include
文を使用してプログラムで必要なライブラリ(入出力処理、文字列操作、コンテナ、正規表現)を取り込んでいます -
using namespace std
によって標準名前空間を使うことで、コードの記述を簡単にしています
TokenType
列挙型
enum TokenType {
KEYWORD,
IDENTIFIER,
NUMBER,
OPERATOR,
SEMICOLON,
UNKNOWN
};
- トークンの種類を
TokenType
という列挙型で定義しています-
KEYWORD
:予約語(let
、const
、ver
など) -
IDENTIFIER
:識別子(変数など) -
NUMBER
:数値 -
OPERATOR
:演算子(+
、-
、*
、/
、=
など) -
SEMICOLON
:セミコロン(;
) -
UNKNOWN
:上記以外のもの
-
Token
構造体
struct Token {
TokenType type;
string value;
};
- トークンを表現する
Token
構造体を定義しています。Token
はトークンの種類(type
)とその値(value
)を保持します
レキサー関数
vector<Token> lex(const string& code) {
vector<Token> tokens;
size_t pos = 0;
// トークンに対応する正規表現
regex keyword_regex(R"(\b(let|const|var)\b)");
regex identifier_regex(R"([a-zA-Z_][a-zA-Z0-9_]*)");
regex number_regex(R"(\b\d+\b)");
regex operator_regex(R"([=+\-*/])");
regex semicolon_regex(R"(;)");
-
lex
関数は文字列code
を受け取り、トークンに分解する処理を行います -
regex
を使用して、それぞれのトークンに対応する正規表現を定義します
レキシングのループ処理
while (pos < code.size()) {
string remaining_code = code.substr(pos);
smatch match;
if (regex_search(remaining_code, match, keyword_regex) && match.position(0) == 0) {
tokens.push_back({KEYWORD, match.str(0)});
} else if (regex_search(remaining_code, match, identifier_regex) && match.position(0) == 0) {
tokens.push_back({IDENTIFIER, match.str(0)});
} else if (regex_search(remaining_code, match, number_regex) && match.position(0) == 0) {
tokens.push_back({NUMBER, match.str(0)});
} else if (regex_search(remaining_code, match, operator_regex) && match.position(0) == 0) {
tokens.push_back({OPERATOR, match.str(0)});
} else if (regex_search(remaining_code, match, semicolon_regex) && match.position(0) == 0) {
tokens.push_back({SEMICOLON, match.str(0)});
} else if (isspace(remaining_code[0])) {
pos++;
continue;
} else {
tokens.push_back({UNKNOWN, string(1, remaining_code[0])});
}
pos += match.length(0);
}
-
while
を使って、文字列code
の最初から順にトークン化の処理を行います -
regex_search
で各トークンの正規表現に一致するか確認し、一致した場合は該当するTokenType
を持つトークンをtokens
に追加します - 正規表現で一致した文字の分だけ
pos
を進め、次の処理を行います - 空白文字は無視し、一致する正規表現がない文字は
UNKNOWN
として扱います
トークンタイプを文字列に変換する処理
string tokenTypeToString(TokenType type) {
switch (type) {
case KEYWORD: return "KEYWORD";
case IDENTIFIER: return "IDENTIFIER";
case NUMBER: return "NUMBER";
case OPERATOR: return "OPERATOR";
case SEMICOLON: return "SEMICOLON";
default: return "UNKNOWN";
}
}
- トークンの情報を表示するため、トークンの種類を文字列として出力します
メイン関数
int main() {
string code = "let x = 10 + 20;";
// レキサー実行
vector<Token> tokens = lex(code);
// トークンを出力
for (const auto& token : tokens) {
cout << "Token(Type: " << tokenTypeToString(token.type)
<< ", Value: \"" << token.value << "\")" << endl;
}
return 0;
}
-
code
という変数に"let x = 10 + 20;"
を設定します -
lex
関数を呼び出し、コードをトークンに分解します - 分解されたトークンを出力します
まとめ
長くなったので、ここで区切って続きは別記事にしようと思います。
- ソースコードはブラウザやNode.jsなどのJavaScriptエンジンで実行される
- 字句解析ではソースコードをトークンという単位に分解して、それぞれの構成要素に意味を与え、分類する。このプロセスには正規表現を使用されることが多い