C ++でレクサーを作成する方法に関する優れたリソース(書籍、チュートリアル、ドキュメント)は何ですか?優れたテクニックとプラクティスは何ですか?
私はインターネットを見ましたが、みんなlexのようなlexerジェネレーターを使うように言っています。私はそれをしたくありません。手で字句解析器を書きたいです。
C ++でレクサーを作成する方法に関する優れたリソース(書籍、チュートリアル、ドキュメント)は何ですか?優れたテクニックとプラクティスは何ですか?
私はインターネットを見ましたが、みんなlexのようなlexerジェネレーターを使うように言っています。私はそれをしたくありません。手で字句解析器を書きたいです。
回答:
すべての有限状態マシンは正規表現に対応し、正規表現はif
and while
ステートメントを使用する構造化プログラムに対応することに注意してください。
したがって、たとえば、整数を認識するために、ステートマシンを使用できます。
0: digit -> 1
1: digit -> 1
または正規表現:
digit digit*
または構造化コード:
if (isdigit(*pc)){
while(isdigit(*pc)){
pc++;
}
}
個人的には、私は常に後者を使用してレクサーを作成します。
*pc
よね?のようなwhile(isdigit(*pc)) { value += pc; pc++; }
。次に}
、値を数値に変換し、それをトークンに割り当てます。
n = n * 10 + (*pc++ - '0');
。浮動小数点と 'e'表記ではもう少し複雑になりますが、悪くはありません。文字をバッファにパックして呼び出しatof
などを行うことで、小さなコードを保存できると確信しています。それは速く実行されません。
レクサーは有限状態マシンです。したがって、汎用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は単純なテストです。
最終的には、バックトラックから予測に移行する予定です。
MakeEquality
スニペットを共有しますか?具体的には、その関数によって返されるオブジェクト。とても面白そうです。
まず、ここでさまざまなことが行われています。
一般に、レクサーは一度に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)に変換することと、いくつかの詳細で構成されます。
この変換の詳細なガイドは、ここにあります。私は自分で読んではいませんが、見た目はとても良いことに注意してください。また、コンパイラの構築に関する本は、最初のいくつかの章でこの変換を取り上げています。
トピックに関するコースの講義スライドに興味がある場合は、コンパイラ構築のコースから無限の量の疑いがないことは間違いありません。私の大学からは、このスライドをこことここで見つけることができます。
レクサーで一般的に使用されたり、テキストで扱われたりすることはほとんどありませんが、それでも非常に便利なものがいくつかあります。
まず、Unicodeの処理はやや重要です。問題は、ASCII入力の幅が8ビットしかないことです。つまり、DFAには256エントリしかないため、DFAのすべての状態の遷移テーブルを簡単に作成できます。ただし、16ビット幅のUnicode(UTF-16を使用する場合)では、DFAのすべてのエントリに64kテーブルが必要です。複雑な文法がある場合、これはかなりのスペースを占有する可能性があります。これらのテーブルを埋めるには、かなりの時間がかかります。
または、間隔ツリーを生成できます。範囲ツリーには、タプル( 'a'、 'z')、( 'A'、 'Z')が含まれる場合があります。これは、テーブル全体を保持するよりもはるかにメモリ効率が高くなります。オーバーラップしない間隔を維持する場合、この目的のためにバランスの取れたバイナリツリーを使用できます。実行時間はすべての文字に必要なビット数に比例するため、Unicodeの場合はO(16)です。ただし、最良の場合、通常はかなり少なくなります。
もう1つの問題は、一般的に生成されるレクサーの実際の最悪の2次パフォーマンスです。この最悪の場合の動作は一般的には見られませんが、あなたに噛みつくかもしれません。問題にぶつかり、それを解決したい場合は、線形時間を達成する方法を説明した論文がここにあります。
通常、正規表現が文字列形式で記述されるようにしたいでしょう。ただし、これらの正規表現の説明をNFA(または最初に再帰的な中間構造)に解析することは、鶏卵の問題です。正規表現の説明を解析するには、Shunting Yardアルゴリズムが非常に適しています。ウィキペディアには、アルゴリズムに関する広範なページがあるようです。