Escrevendo um lexer em C ++

18

Quais são os bons recursos para escrever um lexer em C ++ (livros, tutoriais, documentos), quais são algumas boas técnicas e práticas?

Eu olhei na internet e todo mundo diz para usar um gerador lexer como lex. Não quero fazer isso, quero escrever um lexer à mão.

destro
fonte
Ok, por que lex não é bom para o seu propósito?
CarneyCode
13
Quero aprender como os lexers funcionam. Não posso fazer isso com um gerador de lexer.
#
11
Lex gera código C nojento. Quem quer um lexer decente não usa Lex.
DeadMG
5
@ Giorgio: O código gerado é o código com o qual você precisa fazer interface, com variáveis ​​globais repugnantes e sem segurança de thread, por exemplo, e é o código cujos erros de terminação NULL você está introduzindo em seu aplicativo.
DeadMG
1
@Giorgio: Você já teve que depurar a saída de código por Lex?
mattnz

Respostas:

7

Lembre-se de que toda máquina de estados finitos corresponde a uma expressão regular, que corresponde a um programa estruturado usando ife whileinstruções.

Então, por exemplo, para reconhecer números inteiros, você poderia ter a máquina de estado:

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

ou a expressão regular:

digit digit*

ou o código estruturado:

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

Pessoalmente, eu sempre escrevo lexers usando o último, porque IMHO não é menos claro e não há nada mais rápido.

Mike Dunlavey
fonte
Eu acho que se a expressão regular se torna muito complexa, o mesmo ocorre com o código correspondente. É por isso que o lexer generator é bom: eu normalmente codificaria apenas um lexer se a linguagem fosse muito simples.
Giorgio
1
@Giorgio: Talvez seja uma questão de gosto, mas eu criei muitos analisadores dessa maneira. O lexer não precisa lidar com nada além de números, pontuação, palavras-chave, identificadores, constantes de string, espaço em branco e comentários.
Mike Dunlavey
Eu nunca escrevi um analisador complexo e todos os lexers e analisadores que escrevi também foram codificados à mão. Eu apenas me pergunto como isso pode ser dimensionado para linguagens regulares mais complexas: nunca tentei, mas imagino que usar um gerador (como o lex) seria mais compacto. Admito que não tenho experiência com lex ou outros geradores além de alguns exemplos de brinquedos.
Giorgio
1
Haveria uma string à qual você anexa *pc, certo? Like while(isdigit(*pc)) { value += pc; pc++; }. Depois que }você converter o valor em um número e atribuí-lo a um token.
rightfold
@WTP: Para números, apenas os calculo em tempo real, semelhante a n = n * 10 + (*pc++ - '0');. Torna-se um pouco mais complexo para ponto flutuante e notação 'e', ​​mas não é ruim. Tenho certeza de que poderia salvar um pouco de código colocando os caracteres em um buffer e ligando atofou o que for. Não seria mais rápido.
Mike Dunlavey
9

Lexers são máquinas de estado finito. Portanto, eles podem ser construídos por qualquer biblioteca FSM de uso geral. Para os propósitos de minha própria educação, no entanto, escrevi minha própria, usando modelos de expressão. Aqui está o meu 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;
}

É apoiado por uma biblioteca de máquinas de estado finito, com rastreamento de retorno e com base em iterador, com aproximadamente 400 linhas de comprimento. No entanto, é fácil ver que tudo o que eu tinha a fazer era construção simples operações booleanas, como and, or, enot , e um par de operadores de estilo regex como *para-ou-mais zero, epsque significa "match qualquer coisa" e optque significa "jogo qualquer coisa, mas não consuma ". A biblioteca é totalmente genérica e baseada em iteradores. O material MakeEquality é um teste simples de igualdade entre *ite o valor passado, e MakeRange é um <= >=teste simples .

Eventualmente, estou planejando mudar de retorno para previsão.

DeadMG
fonte
2
Eu já vi vários lexers que acabam de ler o próximo token quando solicitados pelo analisador para fazer isso. O seu parece vasculhar um arquivo inteiro e fazer uma lista de tokens. Existe alguma vantagem específica nesse método?
user673679
2
@DeadMG: Gostaria de compartilhar um MakeEqualitysnippet? Especificamente, o objeto retornado por essa função. Parece muito interessante.
Deathicon
3

Primeiro de tudo, há coisas diferentes acontecendo aqui:

  • dividindo a lista de caracteres simples em tokens
  • reconhecendo esses tokens (identificando palavras-chave, literais, colchetes, ...)
  • verificando uma estrutura gramatical geral

Geralmente, esperamos que um lexer execute as três etapas de uma só vez, no entanto, o último é inerentemente mais difícil e há alguns problemas com a automação (mais sobre isso mais tarde).

O lexer mais incrível que eu conheço é o Boost.Spirit.Qi . Ele usa modelos de expressão para gerar suas expressões lexer e, uma vez acostumado à sua sintaxe, o código parece realmente legal. Embora seja compilado muito lentamente (modelos pesados), é melhor isolar as várias partes em arquivos dedicados para evitar recompilá-las quando não foram tocadas.

Existem algumas armadilhas no desempenho, e o autor do compilador Epoch explica como ele conseguiu uma velocidade de 1000x por meio de criação de perfil e investigação intensivos sobre como o Qi funciona em um artigo .

Por fim, também são gerados códigos por ferramentas externas (Yacc, Bison, ...).


Mas prometi um artigo sobre o que havia de errado em automatizar a verificação gramatical.

Se você verificar o Clang, por exemplo, perceberá que, em vez de usar um analisador gerado e algo como Boost.Spirit, eles pretendem validar a gramática manualmente usando uma técnica genérica de Análise de Descida. Certamente isso parece atrasado?

De fato, há uma razão muito simples: recuperação de erros .

O exemplo típico, em C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Percebeu o erro? Um ponto e vírgula ausente logo após a declaração de Foo.

É um erro comum, e Clang se recupera perfeitamente ao perceber que está simplesmente ausente e voidnão é uma instância, Foomas parte da próxima declaração. Isso evita o diagnóstico de mensagens de erro enigmáticas.

A maioria das ferramentas automatizadas não possui maneiras (pelo menos óbvias) de especificar esses erros prováveis ​​e como se recuperar deles. Muitas vezes, a recuperação requer um pouco de análise sintática, por isso está longe de ser evidente.


Portanto, há uma troca envolvida no uso de uma ferramenta automatizada: você obtém seu analisador rapidamente, mas é menos amigável.

Matthieu M.
fonte
3

Como você quer aprender como os lexers funcionam, presumo que você realmente queira saber como os geradores de lexer funcionam.

Um gerador de lexer pega uma especificação lexical, que é uma lista de regras (pares de expressão regular-token) e gera um lexer. Esse lexer resultante pode transformar uma string de entrada (caractere) em uma string de token de acordo com esta lista de regras.

O método mais comumente usado consiste principalmente em transformar uma expressão regular em um autômato finito determinístico (DFA) por meio de um autômato não determinístico (NFA), além de alguns detalhes.

Um guia detalhado de como fazer essa transformação pode ser encontrado aqui . Observe que eu não li, mas parece muito bom. Além disso, praticamente qualquer livro sobre construção de compilador apresentará essa transformação nos primeiros capítulos.

Se você estiver interessado em slides de palestras de cursos sobre o tema, não há dúvida de que uma quantidade infinita deles é de cursos sobre construção de compiladores. Da minha universidade, você pode encontrar esses slides aqui e aqui .

Existem poucas outras coisas que não são comumente empregadas em lexers ou tratadas em textos, mas são bastante úteis, no entanto:

Em primeiro lugar, o manuseio do Unicode não é trivial. O problema é que a entrada ASCII tem apenas 8 bits de largura, o que significa que você pode facilmente ter uma tabela de transição para todos os estados do DFA, porque eles possuem apenas 256 entradas. No entanto, o Unicode, com 16 bits de largura (se você usar UTF-16), requer tabelas de 64k para cada entrada no DFA. Se você possui gramáticas complexas, isso pode começar a ocupar bastante espaço. O preenchimento dessas tabelas também começa a demorar um pouco.

Como alternativa, você pode gerar árvores de intervalo. Uma árvore de intervalo pode conter as tuplas ('a', 'z'), ('A', 'Z'), por exemplo, que são muito mais eficientes em termos de memória do que a tabela completa. Se você mantiver intervalos sem sobreposição, poderá usar qualquer árvore binária balanceada para esse fim. O tempo de execução é linear no número de bits que você precisa para cada caractere; portanto, O (16) no caso Unicode. No entanto, na melhor das hipóteses, geralmente será um pouco menos.

Mais uma questão é que os lexers, como geralmente gerados, têm realmente um desempenho quadrático no pior dos casos. Embora esse comportamento de pior caso não seja comumente visto, ele pode te morder. Se você se deparar com o problema e quiser resolvê-lo, um documento descrevendo como obter tempo linear pode ser encontrado aqui .

Provavelmente, você poderá descrever expressões regulares na forma de string, como normalmente aparecem. No entanto, analisar essas descrições de expressões regulares nos NFAs (ou possivelmente uma estrutura intermediária recursiva primeiro) é um pouco problemático. Para analisar descrições de expressões regulares, o algoritmo Shunting Yard é muito adequado. A Wikipedia parece ter uma página extensa sobre o algoritmo .

Alex ten Brink
fonte