C ++로 어휘 분석기 작성


18

C ++에서 렉서를 작성하는 방법 (책, 튜토리얼, 문서)에 대한 유용한 자료는 무엇이며 좋은 기술과 실습은 무엇입니까?

나는 인터넷을 보았고 모두가 lex와 같은 lexer 생성기를 사용한다고 말합니다. 나는 그것을하고 싶지 않습니다. 나는 손으로 렉서를 작성하고 싶습니다.


렉스가 왜 당신의 목적에 좋지 않은가요?
CarneyCode

13
렉서 작동 방식을 배우고 싶습니다. 렉서 생성기로는 그렇게 할 수 없습니다.
rightfold

11
Lex는 역겨운 C 코드를 생성합니다. 괜찮은 어휘 분석기를 원하는 사람은 Lex를 사용하지 않습니다.
DeadMG

5
@Giorgio : 생성 된 코드는 예를 들어 역 스레딩이 아닌 전역 변수를 사용하여 인터페이스해야하는 코드이며, 응용 프로그램에 NULL 종료 버그가있는 코드입니다.
DeadMG

1
@Giorgio : Lex의 코드 출력을 디버깅해야했던 적이 있습니까?
mattnz

답변:


7

모든 유한 상태 머신은 정규식에 해당하며, 이는 using ifwhilestatement를 사용하는 구조화 된 프로그램에 해당 합니다.

예를 들어 정수를 인식하려면 상태 머신을 가질 수 있습니다.

0: digit -> 1
1: digit -> 1

또는 정규식 :

digit digit*

또는 구조화 된 코드 :

if (isdigit(*pc)){
  while(isdigit(*pc)){
    pc++;
  }
}

개인적으로, 나는 IMHO가 덜 명확하지 않고 더 빠른 것이 없기 때문에 항상 후자를 사용하여 렉서를 작성합니다.


정규 표현식이 매우 복잡해지면 해당 코드도 그렇게 생각합니다. 렉서 생성기가 좋은 이유입니다. 언어가 매우 간단한 경우 일반적으로 직접 렉서를 코딩합니다.
Giorgio

1
@ Giorgio : 아마도 맛의 문제 일지 모르지만이 방법으로 많은 파서를 만들었습니다. 어휘 분석기는 숫자, 문장 부호, 키워드, 식별자, 문자열 상수, 공백 및 주석 이외의 것을 처리 할 필요가 없습니다.
Mike Dunlavey가

나는 복잡한 파서를 작성한 적이 없으며 내가 작성한 모든 어휘 분석기와 파서도 직접 코딩했다. 나는 이것이 더 복잡한 일반 언어에 대해 어떻게 확장되는지 궁금합니다. 시도한 적이 없지만 lex와 같은 생성기를 사용하는 것이 더 작을 것이라고 생각합니다. 나는 장난감 예제 이외의 lex 또는 다른 발전기에 대한 경험이 없다는 것을 인정한다
Giorgio

1
추가 *pc할 문자열이 있을까요? 처럼 while(isdigit(*pc)) { value += pc; pc++; }. 그런 다음 }값을 숫자로 변환하고 토큰에 할당합니다.
rightfold

@ WTP : 숫자의 경우와 비슷한 숫자를 즉시 ​​계산합니다 n = n * 10 + (*pc++ - '0');. 부동 소수점 및 'e'표기법에 대해 조금 더 복잡해 지지만 나쁘지는 않습니다. 문자를 버퍼에 패킹하고 호출 atof하거나 기타 로 작은 코드를 저장할 수 있다고 확신 합니다. 더 빨리 실행되지 않습니다.
Mike Dunlavey가

9

Lexers는 유한 상태 머신입니다. 따라서 모든 범용 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는 간단한 <= >=테스트입니다.

결국 저는 역 추적에서 예측으로 전환 할 계획입니다.


2
파서가 요청할 때 다음 토큰을 읽는 몇 가지 어휘 분석기를 보았습니다. 당신은 전체 파일을 검토하고 토큰 목록을 만드는 것 같습니다. 이 방법에 특별한 이점이 있습니까?
user673679

2
@DeadMG : MakeEquality스 니펫 을 공유 하시겠습니까? 특히 해당 함수에서 반환 한 객체입니다. 매우 흥미로운 것 같습니다.
Deathicon

3

우선, 여기에는 다른 일이 있습니다.

  • 베어 문자 목록을 토큰으로 분할
  • 해당 토큰 인식 (키워드, 리터럴, 괄호 등 식별)
  • 일반적인 문법 구조 확인

일반적으로 우리는 렉서가 한 번에 3 단계를 모두 수행 할 것으로 예상하지만, 후자는 본질적으로 더 어렵고 자동화와 관련된 몇 가지 문제가 있습니다 (나중에 자세히 설명).

내가 아는 가장 놀라운 어휘 분석기는 Boost.Spirit.Qi 입니다. 표현식 템플릿을 사용하여 렉서 표현식을 생성하고 구문에 익숙해지면 코드가 정말 깔끔합니다. 컴파일 속도는 매우 느리기 때문에 (무거운 템플릿), 만지지 않았을 때 다시 컴파일하지 않도록 전용 파일의 다양한 부분을 분리하는 것이 가장 좋습니다.

성능에 약간의 함정이 있으며 Epoch 컴파일러의 저자 는 기사에서 Qi의 작동 방식 에 대한 집중적 인 프로파일 링 및 조사를 통해 1000x 속도 향상 방법을 설명합니다 .

마지막으로 외부 도구 (Yacc, Bison 등)에 의해 생성 된 코드도 있습니다.


그러나 나는 문법 검증 자동화에 무엇이 잘못되었는지에 대한 글을 썼다.

예를 들어 Clang을 체크 아웃하면 생성 된 파서와 Boost.Spirit와 같은 것을 사용하는 대신 일반적인 Descent Parsing 기술을 사용하여 수동으로 문법을 검증하기 시작합니다. 확실히 이것은 뒤로 보인다?

실제로 오류 복구 라는 매우 간단한 이유 있습니다.

C ++의 일반적인 예 :

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

오류가 있습니까? 의 선언 직후에 세미콜론이 누락되었습니다 Foo.

일반적인 오류이며 Clang은 단순히 누락되어 있다는 사실을 인식하여 깔끔하게 복구합니다. voidFoo 되어 다음 선언의 일부 가 아니라는 . 이것은 암호 오류 메시지를 진단하기 어렵게합니다.

대부분의 자동화 된 도구에는 이러한 실수를 지정하는 방법과 오류를 복구하는 방법이 없습니다. 복구에는 약간의 구문 분석이 필요하기 때문에 분명하지 않습니다.


따라서 자동화 된 도구 사용과 관련된 절충안이 있습니다. 파서를 빨리 ​​가져 오지만 사용자에게 친숙하지 않습니다.


3

렉서 작동 방식을 배우고 싶기 때문에 실제로 렉서 생성기 작동 방식을 알고 싶다고 가정합니다.

렉서 생성기는 규칙 목록 (정규 표현 토큰 쌍) 인 렉시 컬 사양을 사용하여 렉서를 생성합니다. 이 결과 어휘 분석기는이 규칙 목록에 따라 입력 (문자) 문자열을 토큰 문자열로 변환 할 수 있습니다.

가장 일반적으로 사용되는 방법은 주로 정규식을 NFA (Non-Deterministic Automata)와 몇 가지 세부 정보를 통해 DFA (Deterministic Finite Automata)로 변환하는 것입니다.

이 변환을 수행하는 자세한 지침은 여기를 참조하십시오 . 나는 그것을 스스로 읽지 않았지만 꽤 좋아 보인다. 또한 컴파일러 구성에 관한 책은 처음 몇 장에서 이러한 변형을 특징으로합니다.

주제에 대한 강의 슬라이드에 관심이 있다면, 컴파일러 구성에 대한 강의에서 끝없는 양이있을 것입니다. 우리 대학에서 여기여기에 그런 슬라이드가 있습니다 .

어휘 분석기에서 일반적으로 사용되지 않거나 텍스트로 처리되지는 않지만 몇 가지 더 유용합니다.

첫째, 유니 코드를 다루는 것은 사소한 일이 아닙니다. 문제는 ASCII 입력의 너비가 8 비트에 불과하므로 256 개의 항목 만 있기 때문에 DFA의 모든 상태에 대한 전이 테이블을 쉽게 가질 수 있다는 것입니다. 그러나 16 비트 너비 (UTF-16을 사용하는 경우) 인 유니 코드에는 DFA의 모든 항목에 대해 64k 테이블이 필요합니다. 복잡한 문법이 있으면 공간이 상당히 많이 차지 될 수 있습니다. 이 테이블을 채우는데도 약간의 시간이 걸립니다.

또는 간격 트리를 생성 할 수 있습니다. 예를 들어, 범위 트리에는 튜플 ( 'a', 'z'), ( 'A', 'Z')이 포함될 수 있으며, 이는 전체 테이블보다 메모리 효율성이 훨씬 높습니다. 겹치지 않는 간격을 유지하면이 목적으로 균형 이진 트리를 사용할 수 있습니다. 실행 시간은 모든 문자에 필요한 비트 수에 따라 선형이므로 유니 코드의 경우 O (16)입니다. 그러나 가장 좋은 경우에는 대개 약간 줄어 듭니다.

또 다른 문제는 일반적으로 생성 된 어휘 분석기가 실제로 최악의 2 차 성능을 갖는다는 것입니다. 이 최악의 행동은 일반적으로 보이지 않지만, 당신을 물 수도 있습니다. 문제가 발생하여 해결하려면 선형 시간을 달성하는 방법을 설명하는 문서를 여기 에서 찾을 수 있습니다 .

정규식은 일반적으로 나타나는 것처럼 문자열 형식으로 설명 할 수 있습니다. 그러나 이러한 정규식 설명을 NFA (또는 재귀 중간 구조로 먼저 해석)로 파싱하는 것은 약간의 닭고기 달걀 문제입니다. 정규식 설명을 구문 분석하려면 Shunting Yard 알고리즘이 매우 적합합니다. Wikipedia 는 알고리즘 에 대한 광범위한 페이지를 가지고있는 것 같습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.