Scrivere un lexer in C ++


18

Quali sono le buone risorse su come scrivere un lexer in C ++ (libri, tutorial, documenti), quali sono alcune buone tecniche e pratiche?

Ho guardato su Internet e tutti dicono di usare un generatore di lexer come lex. Non voglio farlo, voglio scrivere un lexer a mano.


Ok, perché Lex non è buono per il tuo scopo?
CarneyCode

13
Voglio imparare come funzionano i lexer. Non posso farlo con un generatore di lexer.
rightfold

11
Lex genera disgustoso codice C. Chiunque voglia un discreto lexer non usa Lex.
DeadMG

5
@Giorgio: il codice generato è il codice con cui devi interfacciarti, con disgustose variabili globali non thread-safe, ad esempio, ed è il codice di cui stai introducendo i bug di terminazione NULL nella tua applicazione.
DeadMG

1
@Giorgio: hai mai dovuto eseguire il debug dell'output del codice da Lex?
Mattnz,

Risposte:


7

Tenere presente che ogni macchina a stati finiti corrisponde a un'espressione regolare, che corrisponde a un programma strutturato che utilizza if e whileistruzioni.

Quindi, per esempio, per riconoscere i numeri interi potresti avere la macchina a stati:

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

o l'espressione regolare:

digit digit*

o il codice strutturato:

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

Personalmente, scrivo sempre lexer usando quest'ultimo, perché IMHO non è meno chiaro e non c'è niente di più veloce.


Penso che se l'espressione regolare diventa molto complessa, lo è anche il codice corrispondente. Ecco perché i generatori di lexer sono buoni: normalmente codificherei un lexer solo se la lingua è molto semplice.
Giorgio

1
@Giorgio: forse è una questione di gusti, ma ho costruito molti parser in questo modo. Il lexer non deve gestire nulla oltre numeri, punteggiatura, parole chiave, identificatori, costanti di stringa, spazi bianchi e commenti.
Mike Dunlavey,

Non ho mai scritto un parser complesso e anche tutti i lexer e i parser che ho scritto sono stati codificati a mano. Mi chiedo solo come questo riduca a linguaggi regolari più complessi: non l'ho mai provato ma immagino che l'uso di un generatore (come lex) sarebbe più compatto. Ammetto di non avere esperienza con lex o altri generatori oltre ad alcuni esempi di giocattoli.
Giorgio

1
Ci sarebbe una stringa a cui aggiungere *pc, giusto? Come while(isdigit(*pc)) { value += pc; pc++; }. Quindi dopo }convertire il valore in un numero e assegnarlo a un token.
destra

@WTP: per i numeri, li ho calcolati al volo, in modo simile a n = n * 10 + (*pc++ - '0');. Diventa un po 'più complesso per la virgola mobile e la notazione' e ', ma non è male. Sono sicuro di poter salvare un piccolo codice impacchettando i caratteri in un buffer e chiamando atofo altro. Non funzionerebbe più velocemente.
Mike Dunlavey,

9

I Lexer sono macchine a stati finiti. Pertanto, possono essere costruiti da qualsiasi libreria FSM generica. Ai fini della mia educazione, tuttavia, ho scritto la mia, usando modelli di espressione. Ecco il mio 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;
}

È supportato da una libreria di macchine a stati finiti basata su iteratore, con back-tracking, della lunghezza di circa 400 righe. Tuttavia, è facile vedere che tutto quello che dovevo fare era costruire semplici operazioni booleane, come and, ore not, e un paio di operatori in stile regex come *zero o più, epsper significare "abbinare qualsiasi cosa" e optsignificare "abbinare tutt'altro che non consumarlo ". La libreria è completamente generica e basata su iteratori. Il materiale MakeEquality è un semplice test per l'uguaglianza tra *ite il valore passato, e MakeRange è un semplice<= >= test.

Alla fine, sto programmando di passare dal backtracking al predittivo.


2
Ho visto diversi lexer che hanno appena letto il token successivo quando richiesto dal parser per farlo. Il tuo sembra passare attraverso un intero file e creare un elenco di token. C'è qualche vantaggio particolare in questo metodo?
user673679,

2
@DeadMG: ti interessa condividere lo MakeEqualitysnippet? In particolare l'oggetto restituito da quella funzione. Sembra molto interessante
Deathicon,

3

Prima di tutto, ci sono diverse cose che succedono qui:

  • suddividere l'elenco di caratteri nudi in token
  • riconoscere quei token (identificazione di parole chiave, valori letterali, parentesi quadre, ...)
  • verifica di una struttura grammaticale generale

Generalmente, ci aspettiamo che un lexer esegua tutti e 3 i passaggi in una volta, tuttavia quest'ultimo è intrinsecamente più difficile e ci sono alcuni problemi con l'automazione (ne parleremo più avanti).

Il lexer più sorprendente che conosco è Boost.Spirit.Qi . Utilizza i modelli di espressione per generare le espressioni lexer e, una volta abituato alla sua sintassi, il codice sembra davvero pulito. Si compila molto lentamente (template pesanti), quindi è meglio isolare le varie porzioni in file dedicati per evitare di ricompilarle quando non sono state toccate.

Ci sono alcune insidie ​​nelle prestazioni e l'autore del compilatore di Epoch spiega come ha ottenuto una velocità di 1000 volte accelerando la profilazione e le indagini su come funziona Qi in un articolo .

Infine, ci sono anche codici generati da strumenti esterni (Yacc, Bison, ...).


Ma ho promesso un commento su cosa non andava nell'automazione della verifica grammaticale.

Se dai un'occhiata a Clang, per esempio, ti renderai conto che invece di usare un parser generato e qualcosa come Boost.Spirit, invece, si prefiggono di convalidare la grammatica manualmente usando una tecnica di analisi della discesa generica. Sicuramente questo sembra arretrato?

In realtà, c'è un motivo molto semplice: il recupero degli errori .

L'esempio tipico, in C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Notare l'errore? Un punto e virgola mancante subito dopo la dichiarazione di Foo.

È un errore comune e Clang recupera ordinatamente rendendosi conto che è semplicemente mancante e voidnon è un'istanza Fooma parte della prossima dichiarazione. Questo evita messaggi di errore criptici difficili da diagnosticare.

La maggior parte degli strumenti automatizzati non ha (almeno ovvio) modi per specificare quei probabili errori e come recuperarli. Spesso il recupero richiede una piccola analisi sintattica, quindi è tutt'altro che evidente.


Quindi, c'è un compromesso nell'uso di uno strumento automatizzato: ottieni rapidamente il tuo parser, ma è meno facile da usare.


3

Dato che vuoi imparare come funzionano i lexer, presumo che tu voglia davvero sapere come funzionano i generatori di lexer.

Un generatore di lexer accetta una specifica lessicale, che è un elenco di regole (coppie di token di espressione regolare) e genera un lexer. Questo lexer risultante può quindi trasformare una stringa di input (carattere) in una stringa token secondo questo elenco di regole.

Il metodo più comunemente usato consiste principalmente nel trasformare un'espressione regolare in automi finiti deterministici (DFA) tramite automi non deterministici (NFA), oltre a qualche dettaglio.

È possibile trovare una guida dettagliata per eseguire questa trasformazione qui . Nota che non l'ho letto da solo, ma sembra abbastanza buono. Inoltre, quasi ogni libro sulla costruzione di compilatori presenterà questa trasformazione nei primi capitoli.

Se sei interessato a diapositive di lezioni di corsi sull'argomento, non vi è dubbio che una quantità infinita di loro dai corsi sulla costruzione di compilatori. Dalla mia università, puoi trovare queste slide qui e qui .

Ci sono alcune altre cose che non sono comunemente impiegate nei lexer o trattate nei testi, ma sono comunque abbastanza utili:

Innanzitutto, la gestione di Unicode è in qualche modo non banale. Il problema è che l'ingresso ASCII è largo solo 8 bit, il che significa che puoi avere facilmente una tabella di transizione per ogni stato nel DFA, perché hanno solo 256 voci. Tuttavia, Unicode, essendo largo 16 bit (se usi UTF-16), richiede 64k tabelle per ogni voce nel DFA. Se hai grammatiche complesse, questo potrebbe iniziare a occupare abbastanza spazio. Anche riempire queste tabelle inizia a richiedere parecchio tempo.

In alternativa, è possibile generare alberi ad intervalli. Un albero di intervallo può contenere le tuple ('a', 'z'), ('A', 'Z'), ad esempio, che è molto più efficiente in termini di memoria rispetto alla tabella completa. Se si mantengono intervalli non sovrapposti, è possibile utilizzare qualsiasi albero binario bilanciato per questo scopo. Il tempo di esecuzione è lineare nel numero di bit necessari per ogni carattere, quindi O (16) nel caso Unicode. Tuttavia, nel migliore dei casi, sarà di solito un po 'meno.

Un altro problema è che i lexer generati comunemente hanno effettivamente prestazioni quadratiche nel caso peggiore. Sebbene questo comportamento nel peggiore dei casi non sia comune, potrebbe morderti. Se si riscontra un problema e si desidera risolverlo, è possibile trovare un documento che descrive come ottenere un tempo lineare qui .

Probabilmente vorrai essere in grado di descrivere espressioni regolari in forma di stringa, come appaiono normalmente. Tuttavia, analizzare queste descrizioni di espressioni regolari in NFA (o forse prima una struttura intermedia ricorsiva) è un po 'un problema con l'uovo di gallina. Per analizzare le descrizioni delle espressioni regolari, l'algoritmo Shunting Yard è molto adatto. Wikipedia sembra avere una pagina estesa sull'algoritmo .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.