Écrire un lexer en C ++

18

Quelles sont les bonnes ressources pour écrire un lexer en C ++ (livres, tutoriels, documents), quelles sont les bonnes techniques et pratiques?

J'ai regardé sur Internet et tout le monde dit d'utiliser un générateur de lexer comme lex. Je ne veux pas faire ça, je veux écrire un lexer à la main.

à droite
la source
Ok, pourquoi lex n'est pas bon pour ton but?
CarneyCode
13
Je veux savoir comment fonctionnent les lexeurs. Je ne peux pas faire ça avec un générateur de lexer.
pli droit
11
Lex génère un code C dégoûtant. Quiconque veut un lexer décent n'utilise pas Lex.
DeadMG
5
@Giorgio: Le code généré est le code avec lequel vous devez vous interfacer, avec des variables globales dégoûtantes non thread-safe, par exemple, et c'est le code dont vous introduisez des bogues de terminaison NULL dans votre application.
DeadMG
1
@Giorgio: Avez-vous déjà dû déboguer la sortie de code de Lex?
mattnz

Réponses:

7

Gardez à l'esprit que chaque machine à états finis correspond à une expression régulière, ce qui correspond à un programme structuré utilisant des instructions ifet while.

Ainsi, par exemple, pour reconnaître des entiers, vous pouvez avoir la machine d'état:

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

ou l'expression régulière:

digit digit*

ou le code structuré:

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

Personnellement, j'écris toujours des lexers en utilisant ce dernier, car à mon humble avis ce n'est pas moins clair, et il n'y a rien de plus rapide.

Mike Dunlavey
la source
Je pense que si l'expression régulière devient très complexe, le code correspondant l'est aussi. C'est pourquoi le générateur de lexer est bon: normalement, je ne coderais moi-même un lexer que si le langage est très simple.
Giorgio
1
@Giorgio: C'est peut-être une question de goût, mais j'ai construit de nombreux analyseurs de cette façon. Le lexer n'a pas à gérer quoi que ce soit au-delà des nombres, de la ponctuation, des mots-clés, des identificateurs, des constantes de chaîne, des espaces et des commentaires.
Mike Dunlavey
Je n'ai jamais écrit un analyseur complexe et tous les lexers et analyseurs que j'ai écrits étaient également codés à la main. Je me demande simplement comment cela évolue pour des langages réguliers plus complexes: je ne l'ai jamais essayé mais j'imagine que l'utilisation d'un générateur (comme lex) serait plus compact. J'avoue que je n'ai aucune expérience avec lex ou d'autres générateurs au-delà de quelques exemples de jouets.
Giorgio
1
Il y aurait une chaîne à laquelle vous ajoutez *pc, non? Comme while(isdigit(*pc)) { value += pc; pc++; }. Ensuite, }vous convertissez la valeur en nombre et l'assignez à un jeton.
droite
@WTP: Pour les nombres, je les calcule simplement à la volée, comme pour n = n * 10 + (*pc++ - '0');. Il devient un peu plus complexe pour la notation à virgule flottante et «e», mais pas mal. Je suis sûr que je pourrais enregistrer un peu de code en emballant les caractères dans un tampon et en appelant atofou autre chose. Cela ne fonctionnerait pas plus vite.
Mike Dunlavey
9

Les Lexers sont des machines à états finis. Par conséquent, ils peuvent être construits par n'importe quelle bibliothèque FSM à usage général. Pour les besoins de ma propre éducation, cependant, j'ai écrit la mienne, en utilisant des modèles d'expression. Voici mon 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;
}

Il est soutenu par une bibliothèque de machines à états finis, à suivi arrière, basée sur un itérateur, d'une longueur d'environ 400 lignes. Cependant, il est facile de voir que tout ce que j'avais à faire était de construire de simples opérations booléennes, comme and, oret not, et quelques opérateurs de style regex comme *pour zéro ou plus, epspour signifier "correspondre à n'importe quoi" et optsignifier "correspondre rien mais ne le consomme pas ". La bibliothèque est entièrement générique et basée sur des itérateurs. Le truc MakeEquality est un simple test d'égalité entre *itet la valeur transmise, et MakeRange est un <= >=test simple .

Finalement, je prévois de passer du retour en arrière à la prévision.

DeadMG
la source
2
J'ai vu plusieurs lexers qui viennent de lire le jeton suivant lorsque l'analyseur le demande. Le vôtre semble parcourir un fichier entier et faire une liste de jetons. Y a-t-il un avantage particulier à cette méthode?
user673679
2
@DeadMG: Voulez-vous partager l' MakeEqualityextrait? Plus précisément, l'objet renvoyé par cette fonction. Semble très intéressant.
Deathicon
3

Tout d'abord, il se passe différentes choses ici:

  • diviser la liste des personnages nus en jetons
  • reconnaître ces jetons (identifier des mots clés, des littéraux, des crochets, ...)
  • vérification d'une structure grammaticale générale

En général, nous nous attendons à ce qu'un lexer effectue les 3 étapes en une seule fois, mais cette dernière est intrinsèquement plus difficile et il y a quelques problèmes avec l'automatisation (plus à ce sujet plus tard).

Le lexer le plus étonnant que je connaisse est Boost.Spirit.Qi . Il utilise des modèles d'expression pour générer vos expressions lexères, et une fois habitué à sa syntaxe, le code se sent vraiment bien. Cependant, il se compile très lentement (modèles lourds), il est donc préférable d'isoler les différentes parties dans des fichiers dédiés pour éviter de les recompiler quand ils n'ont pas été touchés.

Il y a quelques pièges dans les performances, et l'auteur du compilateur Epoch explique comment il a obtenu une accélération de 1000x par un profilage et une enquête intensifs sur le fonctionnement de Qi dans un article .

Enfin, il existe également du code généré par des outils externes (Yacc, Bison, ...).


Mais j'ai promis un article sur ce qui n'allait pas avec l'automatisation de la vérification de la grammaire.

Si vous consultez Clang, par exemple, vous vous rendrez compte qu'au lieu d'utiliser un analyseur généré et quelque chose comme Boost.Spirit, ils ont plutôt décidé de valider la grammaire manuellement en utilisant une technique générique d'analyse de descente. Cela semble sûrement en arrière?

En fait, il y a une raison très simple: la récupération d'erreur .

L'exemple typique, en C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Remarquez l'erreur? Un point-virgule manquant juste après la déclaration de Foo.

C'est une erreur courante, et Clang récupère parfaitement en réalisant qu'elle est simplement manquante et voidn'est pas une instance Foomais une partie de la prochaine déclaration. Cela évite de diagnostiquer les messages d'erreur cryptiques.

La plupart des outils automatisés n'ont aucun moyen (au moins évident) de spécifier ces erreurs probables et comment s'en remettre. Souvent, la récupération nécessite une petite analyse syntaxique, donc c'est loin d'être évident.


Il y a donc un compromis à faire avec l'utilisation d'un outil automatisé: vous obtenez votre analyseur rapidement, mais il est moins convivial.

Matthieu M.
la source
3

Puisque vous voulez savoir comment fonctionnent les lexers, je suppose que vous voulez réellement savoir comment fonctionnent les générateurs de lexers.

Un générateur de lexer prend une spécification lexicale, qui est une liste de règles (paires d'expression régulière-jeton), et génère un lexer. Ce lexer résultant peut ensuite transformer une chaîne d'entrée (caractère) en une chaîne de jeton selon cette liste de règles.

La méthode la plus couramment utilisée consiste principalement à transformer une expression régulière en automates finis déterministes (DFA) via un automate non déterministe (NFA), plus quelques détails.

Un guide détaillé de cette transformation peut être trouvé ici . Notez que je ne l'ai pas lu moi-même, mais il semble assez bon. De plus, à peu près n'importe quel livre sur la construction d'un compilateur présentera cette transformation dans les premiers chapitres.

Si vous êtes intéressé par des diapositives de cours sur le sujet, il y en a sans aucun doute une quantité infinie parmi les cours sur la construction de compilateurs. De mon université, vous pouvez trouver de telles diapositives ici et ici .

Il y a peu d'autres choses qui ne sont pas couramment utilisées dans les lexers ou traitées dans les textes, mais qui sont néanmoins très utiles:

Premièrement, la gestion d'Unicode est quelque peu banale. Le problème est que l'entrée ASCII ne fait que 8 bits de large, ce qui signifie que vous pouvez facilement avoir une table de transition pour chaque état dans le DFA, car ils n'ont que 256 entrées. Cependant, Unicode, ayant une largeur de 16 bits (si vous utilisez UTF-16), nécessite 64k tables pour chaque entrée dans le DFA. Si vous avez des grammaires complexes, cela peut commencer à prendre beaucoup de place. Remplir ces tableaux commence également à prendre un peu de temps.

Alternativement, vous pouvez générer des arbres d'intervalles. Un arbre de plage peut contenir les tuples ('a', 'z'), ('A', 'Z') par exemple, ce qui est beaucoup plus efficace en mémoire que d'avoir la table complète. Si vous conservez des intervalles sans chevauchement, vous pouvez utiliser n'importe quel arbre binaire équilibré à cet effet. Le temps d'exécution est linéaire dans le nombre de bits dont vous avez besoin pour chaque caractère, donc O (16) dans le cas Unicode. Cependant, dans le meilleur des cas, ce sera généralement un peu moins.

Un autre problème est que les lexers générés couramment ont en fait une performance quadratique dans le pire des cas. Bien que ce comportement du pire des cas ne soit pas communément observé, il pourrait vous mordre. Si vous rencontrez le problème et souhaitez le résoudre, un article décrivant comment obtenir un temps linéaire peut être trouvé ici .

Vous voudrez probablement pouvoir décrire les expressions régulières sous forme de chaînes, telles qu'elles apparaissent normalement. Cependant, l'analyse de ces descriptions d'expressions régulières dans des NFA (ou éventuellement une structure intermédiaire récursive en premier) est un peu un problème d'oeuf de poule. Pour analyser les descriptions d'expressions régulières, l'algorithme Shunting Yard est très approprié. Wikipédia semble avoir une longue page sur l'algorithme .

Alex ten Brink
la source