C++でレクサーを作成する方法(本、チュートリアル、ドキュメント)に関する優れたリソースは何ですか?いくつかの優れたテクニックと実践は何ですか?
私はインターネットを調べましたが、誰もがLexのようなレクサージェネレーターを使用すると言っています。私はそれをしたくありません。レクサーを手で書きたいのです。
すべての有限状態機械は正規表現に対応していることに注意してください。これは、if
およびwhile
ステートメントを使用した構造化プログラムに対応しています。
したがって、たとえば、整数を認識するには、ステートマシンを使用できます。
0: digit -> 1
1: digit -> 1
または正規表現:
digit digit*
または構造化コード:
if (isdigit(*pc)){
while(isdigit(*pc)){
pc++;
}
}
個人的には、私は常に後者を使用してレクサーを作成します。なぜなら、IMHOはそれほど明確ではなく、それ以上速くはありません。
レクサーは有限状態機械です。したがって、これらは任意の汎用FSMライブラリで構築できます。しかし、私は自分の教育目的のために、式テンプレートを使用して自分で作成しました。これが私のレクサーです:
static const std::unordered_map<Unicode::String, Wide::Lexer::TokenType> reserved_words(
[]() -> std::unordered_map<Unicode::String, Wide::Lexer::TokenType>
{
// Maps reserved words to TokenType enumerated values
std::unordered_map<Unicode::String, Wide::Lexer::TokenType> result;
// RESERVED Word
result[L"dynamic_cast"] = Wide::Lexer::TokenType::DynamicCast;
result[L"for"] = Wide::Lexer::TokenType::For;
result[L"while"] = Wide::Lexer::TokenType::While;
result[L"do"] = Wide::Lexer::TokenType::Do;
result[L"continue"] = Wide::Lexer::TokenType::Continue;
result[L"auto"] = Wide::Lexer::TokenType::Auto;
result[L"break"] = Wide::Lexer::TokenType::Break;
result[L"type"] = Wide::Lexer::TokenType::Type;
result[L"switch"] = Wide::Lexer::TokenType::Switch;
result[L"case"] = Wide::Lexer::TokenType::Case;
result[L"default"] = Wide::Lexer::TokenType::Default;
result[L"try"] = Wide::Lexer::TokenType::Try;
result[L"catch"] = Wide::Lexer::TokenType::Catch;
result[L"return"] = Wide::Lexer::TokenType::Return;
result[L"static"] = Wide::Lexer::TokenType::Static;
result[L"if"] = Wide::Lexer::TokenType::If;
result[L"else"] = Wide::Lexer::TokenType::Else;
result[L"decltype"] = Wide::Lexer::TokenType::Decltype;
result[L"partial"] = Wide::Lexer::TokenType::Partial;
result[L"using"] = Wide::Lexer::TokenType::Using;
result[L"true"] = Wide::Lexer::TokenType::True;
result[L"false"] = Wide::Lexer::TokenType::False;
result[L"null"] = Wide::Lexer::TokenType::Null;
result[L"int"] = Wide::Lexer::TokenType::Int;
result[L"long"] = Wide::Lexer::TokenType::Long;
result[L"short"] = Wide::Lexer::TokenType::Short;
result[L"module"] = Wide::Lexer::TokenType::Module;
result[L"dynamic"] = Wide::Lexer::TokenType::Dynamic;
result[L"reinterpret_cast"] = Wide::Lexer::TokenType::ReinterpretCast;
result[L"static_cast"] = Wide::Lexer::TokenType::StaticCast;
result[L"enum"] = Wide::Lexer::TokenType::Enum;
result[L"operator"] = Wide::Lexer::TokenType::Operator;
result[L"throw"] = Wide::Lexer::TokenType::Throw;
result[L"public"] = Wide::Lexer::TokenType::Public;
result[L"private"] = Wide::Lexer::TokenType::Private;
result[L"protected"] = Wide::Lexer::TokenType::Protected;
result[L"friend"] = Wide::Lexer::TokenType::Friend;
result[L"this"] = Wide::Lexer::TokenType::This;
return result;
}()
);
std::vector<Wide::Lexer::Token*> Lexer::Context::operator()(Unicode::String* filename, Memory::Arena& arena) {
Wide::IO::TextInputFileOpenArguments args;
args.encoding = Wide::IO::Encoding::UTF16;
args.mode = Wide::IO::OpenMode::OpenExisting;
args.path = *filename;
auto str = arena.Allocate<Unicode::String>(args().AsString());
const wchar_t* begin = str->c_str();
const wchar_t* end = str->c_str() + str->size();
int line = 1;
int column = 1;
std::vector<Token*> tokens;
// Some variables we'll need for semantic actions
Wide::Lexer::TokenType type;
auto multi_line_comment
= MakeEquality(L'/')
>> MakeEquality(L'*')
>> *( !(MakeEquality(L'*') >> MakeEquality(L'/')) >> eps)
>> eps >> eps;
auto single_line_comment
= MakeEquality(L'/')
>> MakeEquality(L'/')
>> *( !MakeEquality(L'\n') >> eps);
auto punctuation
= MakeEquality(L',')[[&]{ type = Wide::Lexer::TokenType::Comma; }]
|| MakeEquality(L';')[[&]{ type = Wide::Lexer::TokenType::Semicolon; }]
|| MakeEquality(L'~')[[&]{ type = Wide::Lexer::TokenType::BinaryNOT; }]
|| MakeEquality(L'(')[[&]{ type = Wide::Lexer::TokenType::OpenBracket; }]
|| MakeEquality(L')')[[&]{ type = Wide::Lexer::TokenType::CloseBracket; }]
|| MakeEquality(L'[')[[&]{ type = Wide::Lexer::TokenType::OpenSquareBracket; }]
|| MakeEquality(L']')[[&]{ type = Wide::Lexer::TokenType::CloseSquareBracket; }]
|| MakeEquality(L'{')[[&]{ type = Wide::Lexer::TokenType::OpenCurlyBracket; }]
|| MakeEquality(L'}')[[&]{ type = Wide::Lexer::TokenType::CloseCurlyBracket; }]
|| MakeEquality(L'>') >> (
MakeEquality(L'>') >> (
MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::RightShiftEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::RightShift; }])
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::GreaterThanOrEqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::GreaterThan; }])
|| MakeEquality(L'<') >> (
MakeEquality(L'<') >> (
MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::LeftShiftEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LeftShift; }] )
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::LessThanOrEqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LessThan; }])
|| MakeEquality(L'-') >> (
MakeEquality(L'-')[[&]{ type = Wide::Lexer::TokenType::Decrement; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::MinusEquals; }]
|| MakeEquality(L'>')[[&]{ type = Wide::Lexer::TokenType::PointerAccess; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Minus; }])
|| MakeEquality(L'.')
>> (MakeEquality(L'.') >> MakeEquality(L'.')[[&]{ type = Wide::Lexer::TokenType::Ellipsis; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Dot; }])
|| MakeEquality(L'+') >> (
MakeEquality(L'+')[[&]{ type = Wide::Lexer::TokenType::Increment; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::PlusEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Plus; }])
|| MakeEquality(L'&') >> (
MakeEquality(L'&')[[&]{ type = Wide::Lexer::TokenType::LogicalAnd; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryANDEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryAND; }])
|| MakeEquality(L'|') >> (
MakeEquality(L'|')[[&]{ type = Wide::Lexer::TokenType::LogicalOr; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryOREquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryOR; }])
|| MakeEquality(L'*') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::MulEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Multiply; }])
|| MakeEquality(L'%') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::ModulusEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Modulus; }])
|| MakeEquality(L'=') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::EqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Assignment; }])
|| MakeEquality(L'!') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::NotEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LogicalNOT; }])
|| MakeEquality(L'/') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::DivEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Divide; }])
|| MakeEquality(L'^') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryXOREquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryXOR; }])
|| MakeEquality(L':') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::VarAssign; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Colon; }]);
auto string
= L'"' >> *( L'\\' >> MakeEquality(L'"') >> eps || !MakeEquality(L'"') >> eps) >> eps;
auto character
= L'\'' >> *( L'\\' >> MakeEquality(L'\'') >> eps || !MakeEquality(L'\'') >> eps);
auto digit
= MakeRange(L'0', L'9');
auto letter
= MakeRange(L'a', L'z') || MakeRange(L'A', L'Z');
auto number
= +digit >> ((L'.' >> +digit) || opt);
auto new_line
= MakeEquality(L'\n')[ [&] { line++; column = 0; } ];
auto whitespace
= MakeEquality(L' ')
|| L'\t'
|| new_line
|| L'\n'
|| L'\r'
|| multi_line_comment
|| single_line_comment;
auto identifier
= (letter || L'_') >> *(letter || digit || (L'_'));
//= *( !(punctuation || string || character || whitespace) >> eps );
bool skip = false;
auto lexer
= whitespace[ [&]{ skip = true; } ] // Do not produce a token for whitespace or comments. Just continue on.
|| punctuation[ [&]{ skip = false; } ] // Type set by individual punctuation
|| string[ [&]{ skip = false; type = Wide::Lexer::TokenType::String; } ]
|| character[ [&]{ skip = false; type = Wide::Lexer::TokenType::Character; } ]
|| number[ [&]{ skip = false; type = Wide::Lexer::TokenType::Number; } ]
|| identifier[ [&]{ skip = false; type = Wide::Lexer::TokenType::Identifier; } ];
auto current = begin;
while(current != end) {
if (!lexer(current, end)) {
throw std::runtime_error("Failed to Lex input.");
}
column += (current - begin);
if (skip) {
begin = current;
continue;
}
Token t(begin, current);
t.columnbegin = column - (current - begin);
t.columnend = column;
t.file = filename;
t.line = line;
if (type == Wide::Lexer::TokenType::Identifier) { // check for reserved Word
if (reserved_words.find(t.Codepoints()) != reserved_words.end())
t.type = reserved_words.find(t.Codepoints())->second;
else
t.type = Wide::Lexer::TokenType::Identifier;
} else {
t.type = type;
}
begin = current;
tokens.Push_back(arena.Allocate<Token>(t));
}
return tokens;
}
これは、長さが400行までのイテレータベースのバックトラッキング有限有限機械ライブラリによって支えられています。ただし、and
、or
、not
などの単純なブール演算と、次のような正規表現スタイルの演算子を作成するだけでよいことがわかります。ゼロ以上の場合は*
、eps
は「すべてに一致」を意味し、opt
は「すべてに一致するが消費しない」ことを意味します。ライブラリは完全に汎用的で、イテレータに基づいています。 MakeEqualityは、*it
と渡された値が等しいかどうかを簡単にテストするものであり、MakeRangeは単純な<= >=
テストです。
最終的には、バックトラックから予測に移行する予定です。
まず、ここではさまざまなことが行われています。
一般に、レクサーは3つのステップすべてを一度に実行することを期待していますが、後者は本質的に困難であり、自動化にいくつかの問題があります(これについては後で詳しく説明します)。
私が知っている最も素晴らしいレクサーは Boost.Spirit.Qi です。式テンプレートを使用してレクサー式を生成します。構文に慣れると、コードは本当にすっきりします。ただし、コンパイルが非常に遅い(重いテンプレート)ため、変更されていないときに再コンパイルしないように、専用ファイルのさまざまな部分を分離することをお勧めします。
パフォーマンスにはいくつかの落とし穴があります。エポックコンパイラの作者は、Qiがどのように機能するかについて徹底的なプロファイリングと調査によって1000倍のスピードアップを実現した方法を説明しています 記事内 。
最後に、外部ツール(Yacc、Bisonなど)によって生成されたコードもあります。
しかし、私は文法検証の自動化で何が問題であったかについての記事を書くと約束しました。
たとえば、Clangをチェックアウトすると、生成されたパーサーやBoost.Spiritなどを使用する代わりに、ジェネリックDescent Parsingテクニックを使用して手動で文法を検証することに気づくでしょう。確かにこれは後ろ向きですか?
実際、非常に単純な理由があります:エラー回復。
C++での典型的な例:
struct Immediate { } instanceOfImmediate;
struct Foo {}
void bar() {
}
エラーに気づきましたか? Foo
の宣言の直後にセミコロンがない。
これは一般的なエラーであり、Clangは単純に欠落しており、void
はFoo
のインスタンスではなく、次の宣言の一部であることを認識することで正常に回復します。これにより、不可解なエラーメッセージの診断が困難になります。
ほとんどの自動化ツールには、可能性のある間違いを特定し、それらから回復する方法が(少なくとも明白な)方法がありません。多くの場合、回復には少し構文的な分析が必要になるため、明白ではありません。
したがって、自動化ツールの使用にはトレードオフが伴います。パーサーをすばやく取得できますが、ユーザーフレンドリーではありません。
レクサーの仕組みを学びたいので、レクサージェネレータの仕組みを実際に知りたいと思います。
レクサージェネレーターは、ルール(正規表現とトークンのペア)のリストである字句仕様を取り、レクサーを生成します。結果として得られるこのレクサーは、この規則のリストに従って、入力(文字)文字列をトークン文字列に変換できます。
最も一般的に使用される方法は、主に、非決定的オートマトン(NFA)を介して正規表現を決定論的有限オートマトン(DFA)に変換することと、いくつかの詳細で構成されます。
この変換を行うための詳細なガイドは here にあります。自分で読んだことはありませんが、見た目はかなり良いことに注意してください。また、コンパイラ構築に関する本のほとんどすべてが、最初のいくつかの章でこの変換を取り上げています。
トピックのコースの講義スライドに興味がある場合は、コンパイラ構築のコースからの無数のスライドが間違いなくあります。私の大学では、そのようなスライド here および here を見つけることができます。
レクサーで一般的に使用されていない、またはテキストで扱われていないものはいくつかありますが、それでも非常に便利です。
まず、Unicodeの扱いはやや重要です。問題は、ASCII入力は8ビット幅しかないことです。つまり、256エントリしかないため、DFAのすべての状態の遷移テーブルを簡単に作成できます。ただし、Unicode、 16ビット幅(UTF-16を使用する場合)では、DFAのすべてのエントリに64kのテーブルが必要です。複雑な文法がある場合、これはかなりのスペースを占有し始める可能性があります。これらのテーブルを埋めるのにもかなりの時間がかかります。
または、間隔ツリーを生成することもできます。範囲ツリーには、たとえば、タプル( 'a'、 'z')、( 'A'、 'Z')を含めることができます。これは、テーブル全体よりもはるかにメモリ効率が良いです。重複しない間隔を維持する場合、この目的のためにバランスのとれたバイナリツリーを使用できます。実行時間は、各文字に必要なビット数に比例するため、O(16)はUnicodeの場合です。ただし、最良の場合は、通常、かなり少なくなります。 。
もう1つの問題は、一般的に生成されるレクサーが実際には最悪の場合の2次パフォーマンスを持っていることです。このワーストケースの振る舞いは一般的には見られませんが、あなたに噛みつくかもしれません。問題に遭遇してそれを解決したい場合は、線形時間を達成する方法を説明した論文 here を参照してください。
通常表示されるように、正規表現を文字列形式で記述できるようにしたいと思うでしょう。ただし、これらの正規表現の説明をNFA(または最初に再帰的な中間構造)に解析することは、ちょっとした鶏卵の問題です。正規表現の説明を解析するには、シャンティングヤードアルゴリズムが非常に適しています。ウィキペディアには アルゴリズム に関する広範なページがあるようです。