แหล่งข้อมูลที่ดีในการเขียน lexer ใน C ++ คืออะไร (หนังสือบทเรียนแบบฝึกหัดเอกสาร) เทคนิคและวิธีปฏิบัติที่ดีมีอะไรบ้าง
ฉันดูบนอินเทอร์เน็ตและทุกคนบอกว่าจะใช้ตัวสร้าง lexer เช่น lex ฉันไม่ต้องการทำอย่างนั้นฉันต้องการเขียน lexer ด้วยมือ
แหล่งข้อมูลที่ดีในการเขียน lexer ใน C ++ คืออะไร (หนังสือบทเรียนแบบฝึกหัดเอกสาร) เทคนิคและวิธีปฏิบัติที่ดีมีอะไรบ้าง
ฉันดูบนอินเทอร์เน็ตและทุกคนบอกว่าจะใช้ตัวสร้าง lexer เช่น lex ฉันไม่ต้องการทำอย่างนั้นฉันต้องการเขียน lexer ด้วยมือ
คำตอบ:
โปรดจำไว้ว่าเครื่องสถานะ จำกัด ทุกตัวนั้นสอดคล้องกับนิพจน์ทั่วไปซึ่งสอดคล้องกับโปรแกรมที่มีโครงสร้างโดยใช้if
และwhile
คำสั่ง
ตัวอย่างเช่นในการรับรู้จำนวนเต็มคุณอาจมีเครื่องสถานะ:
0: digit -> 1
1: digit -> 1
หรือการแสดงออกปกติ:
digit digit*
หรือรหัสที่มีโครงสร้าง:
if (isdigit(*pc)){
while(isdigit(*pc)){
pc++;
}
}
โดยส่วนตัวฉันมักจะใช้ lexers เขียนหลังเพราะ IMHO มันไม่ชัดเจนและไม่มีอะไรเร็วขึ้น
*pc
ใช่ไหม กดwhile(isdigit(*pc)) { value += pc; pc++; }
ไลค์ จากนั้นหลังจากที่}
คุณแปลงค่าเป็นตัวเลขและกำหนดให้กับโทเค็น
n = n * 10 + (*pc++ - '0');
สำหรับหมายเลขฉันเพียงแค่พวกเขาคำนวณได้ทันทีเพื่อที่คล้ายกัน มันซับซ้อนกว่าเล็กน้อยสำหรับจุดลอยตัวและสัญกรณ์ 'e' แต่ไม่เลว ฉันแน่ใจว่าฉันสามารถบันทึกรหัสเล็กน้อยโดยการบรรจุอักขระลงในบัฟเฟอร์และการโทรatof
หรืออะไรก็ตาม มันจะไม่ทำงานเร็วขึ้น
Lexers เป็นเครื่องจักรสถานะ จำกัด ดังนั้นจึงสามารถสร้างได้โดยไลบรารี FSM ที่มีวัตถุประสงค์ทั่วไป อย่างไรก็ตามสำหรับวัตถุประสงค์ของการศึกษาของฉันฉันเขียนด้วยตัวเองโดยใช้เทมเพลตการแสดงออก นี่คือ lexer ของฉัน:
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
, และคู่ของผู้ประกอบการ regex สไตล์เช่น*
สำหรับศูนย์หรือมากขึ้นeps
หมายถึง "การแข่งขันอะไร" และopt
หมายถึง "การจับคู่ อะไรก็ได้ แต่อย่ากิน " ไลบรารีเป็นแบบทั่วไปอย่างสมบูรณ์และขึ้นอยู่กับตัววนซ้ำ สิ่งที่ MakeEquality เป็นการทดสอบอย่างง่ายสำหรับความเท่าเทียมกันระหว่าง*it
และค่าที่ส่งผ่านและ MakeRange เป็นการ<= >=
ทดสอบอย่างง่าย
ในที่สุดฉันวางแผนที่จะย้ายจากการย้อนรอยไปสู่การทำนาย
MakeEquality
ข้อมูลหรือไม่ วัตถุที่ส่งคืนโดยฟังก์ชันนั้นโดยเฉพาะ ดูน่าสนใจมาก ๆ
ก่อนอื่นมีสิ่งต่าง ๆ เกิดขึ้นที่นี่:
โดยทั่วไปเราคาดว่า lexer จะทำทั้ง 3 ขั้นตอนในคราวเดียวอย่างไรก็ตามหลังนี้ยากกว่ามากและมีปัญหาบางอย่างเกี่ยวกับระบบอัตโนมัติ (เพิ่มเติมในภายหลัง)
lexer ที่น่าตื่นตาตื่นใจที่สุดที่ฉันรู้คือBoost.Spirit.Qi มันใช้เทมเพลตนิพจน์เพื่อสร้างการแสดงออกของ lexer ของคุณและเมื่อคุ้นเคยกับไวยากรณ์แล้วโค้ดจะรู้สึกเรียบร้อยดี มันรวบรวมช้ามาก (เทมเพลตหนัก) ดังนั้นจึงเป็นการดีที่สุดที่จะแยกส่วนต่าง ๆ ในไฟล์เฉพาะเพื่อหลีกเลี่ยงการคอมไพล์ใหม่เมื่อไม่ได้สัมผัส
มีข้อผิดพลาดบางอย่างในการทำงานของผู้เขียนของคอมไพเลอร์ยุคอธิบายว่าเขามี 1000x ความเร็วขึ้นโปรไฟล์เข้มข้นและการตรวจสอบในวิธีการทำงานของฉีมีและในบทความ
ในที่สุดก็มีรหัสที่สร้างขึ้นโดยเครื่องมือภายนอก (Yacc, Bison, ... )
แต่ฉันสัญญาว่าจะเขียนสิ่งที่ผิดกับการตรวจสอบไวยากรณ์อัตโนมัติ
ตัวอย่างเช่นหากคุณตรวจสอบ Clang คุณจะรู้ว่าแทนที่จะใช้ parser ที่สร้างขึ้นและสิ่งที่เรียกว่า Boost.Spirit พวกเขาจะออกไปตรวจสอบไวยากรณ์ด้วยตนเองโดยใช้เทคนิค Descent Parsing ทั่วไป แน่นอนว่าสิ่งนี้ดูเหมือนว่าย้อนหลัง?
ในความเป็นจริงมีเหตุผลที่ง่ายมาก: การกู้คืนข้อผิดพลาด
ตัวอย่างทั่วไปใน C ++:
struct Immediate { } instanceOfImmediate;
struct Foo {}
void bar() {
}
สังเกตเห็นข้อผิดพลาด? Foo
หายไปทางขวากึ่งลำไส้ใหญ่หลังจากการประกาศของ
มันเป็นข้อผิดพลาดทั่วไปและ Clang กู้คืนอย่างเรียบร้อยโดยตระหนักว่ามันหายไปและvoid
ไม่ใช่ตัวอย่างFoo
แต่เป็นส่วนหนึ่งของการประกาศครั้งต่อไป หลีกเลี่ยงปัญหานี้อย่างหนักในการวิเคราะห์ข้อความผิดพลาดที่เป็นความลับ
เครื่องมืออัตโนมัติส่วนใหญ่ไม่มีวิธีที่ชัดเจน (อย่างน้อยที่สุด) ในการระบุข้อผิดพลาดที่น่าจะเป็นและวิธีการกู้คืนจากพวกเขา บ่อยครั้งที่การกู้คืนจำเป็นต้องมีการวิเคราะห์วากยสัมพันธ์เล็กน้อยดังนั้นจึงยังไม่ชัดเจน
ดังนั้นจึงมีการแลกเปลี่ยนที่เกี่ยวข้องในการใช้เครื่องมืออัตโนมัติ: คุณได้รับ parser ของคุณอย่างรวดเร็ว แต่มันเป็นมิตรกับผู้ใช้น้อย
เนื่องจากคุณต้องการเรียนรู้วิธีการทำงานของ lexers ฉันคิดว่าคุณต้องการทราบว่าเครื่องกำเนิด lexer ทำงานอย่างไร
ตัวสร้าง lexer ใช้ข้อมูลจำเพาะของ lexical ซึ่งเป็นรายการของกฎ (คู่นิพจน์ทั่วไป - โทเค็น) และสร้าง lexer lexer ที่เป็นผลลัพธ์นี้สามารถแปลงสตริงอินพุต (อักขระ) เป็นสตริงโทเค็นตามรายการกฎนี้
วิธีการที่ใช้กันมากที่สุดส่วนใหญ่ประกอบด้วยการเปลี่ยนนิพจน์ปกติเป็นออโต จำกัด (DFA) ที่กำหนดขึ้นผ่านทางออโตเมติก nondeterministic (NFA) บวกกับรายละเอียดเล็กน้อย
คู่มือรายละเอียดของการทำแปลงนี้สามารถพบได้ที่นี่ โปรดทราบว่าฉันยังไม่ได้อ่านด้วยตัวเอง แต่มันก็ค่อนข้างดี นอกจากนี้หนังสือเกี่ยวกับการสร้างคอมไพเลอร์จะมีการเปลี่ยนแปลงนี้ในบทแรก ๆ
หากคุณสนใจสไลด์บรรยายของหลักสูตรในหัวข้อไม่ต้องสงสัยเลยว่าหลักสูตรเหล่านี้มีจำนวน จำกัด ตั้งแต่หลักสูตรการสร้างคอมไพเลอร์ จากมหาวิทยาลัยของฉันคุณสามารถหาภาพนิ่งดังกล่าวที่นี่และที่นี่
มีบางสิ่งที่ไม่ได้ใช้กันทั่วไปใน lexers หรือปฏิบัติในตำรา แต่มีประโยชน์ค่อนข้างมาก:
ประการแรกการจัดการ Unicode นั้นค่อนข้างไม่สำคัญ ปัญหาคืออินพุต ASCII กว้างเพียง 8 บิตซึ่งหมายความว่าคุณสามารถมีตารางการเปลี่ยนแปลงสำหรับทุกสถานะใน DFA ได้อย่างง่ายดายเนื่องจากมีเพียง 256 รายการเท่านั้น อย่างไรก็ตาม Unicode ที่มีความกว้าง 16 บิต (ถ้าคุณใช้ UTF-16) ต้องใช้ตาราง 64k สำหรับทุกรายการใน DFA หากคุณมีไวยากรณ์ที่ซับซ้อนการทำเช่นนี้อาจเริ่มใช้พื้นที่ว่างพอสมควร การกรอกตารางเหล่านี้ก็เริ่มใช้เวลาสักครู่
หรือคุณสามารถสร้างต้นไม้ช่วงเวลา ทรีช่วงอาจมีสิ่งอันดับ ('a', 'z'), ('A', 'Z') ซึ่งเป็นหน่วยความจำที่มีประสิทธิภาพมากกว่าการมีตารางเต็ม หากคุณรักษาช่วงเวลาที่ไม่ทับซ้อนกันคุณสามารถใช้แผนภูมิไบนารีแบบสมดุลเพื่อวัตถุประสงค์นี้ เวลาทำงานเป็นเส้นตรงในจำนวนบิตที่คุณต้องการสำหรับตัวละครทุกตัวดังนั้น O (16) ในกรณี Unicode อย่างไรก็ตามในกรณีที่ดีที่สุดมันมักจะน้อยลงเล็กน้อย
อีกประเด็นหนึ่งคือ lexers ที่สร้างขึ้นตามความเป็นจริงมีประสิทธิภาพเป็นกำลังสองกรณีที่เลวร้ายที่สุด แม้ว่าพฤติกรรมกรณีที่เลวร้ายที่สุดนี้จะไม่ได้เห็นกันโดยทั่วไป แต่ก็อาจกัดคุณ ถ้าคุณทำงานเป็นปัญหาและต้องการที่จะแก้มัน, กระดาษอธิบายถึงวิธีการเพื่อให้บรรลุเส้นเวลาสามารถพบได้ที่นี่
คุณอาจต้องการอธิบายนิพจน์ทั่วไปในรูปแบบสตริงตามที่ปรากฏ อย่างไรก็ตามการแยกคำอธิบายการแสดงออกปกติเหล่านี้ลงใน NFAs (หรืออาจเป็นโครงสร้างกลางซ้ำก่อน) เป็นบิตของปัญหาไก่ไข่ เพื่อแยกคำอธิบายการแสดงออกปกติอัลกอริทึม Shunting Yard เหมาะมาก วิกิพีเดียดูเหมือนว่าจะมีหน้าอย่างกว้างขวางในขั้นตอนวิธี