web-dev-qa-db-fra.com

Écrire un lexer en C ++

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.

19
rightfold

Gardez à l'esprit que chaque machine à états finis correspond à une expression régulière, ce qui correspond à un programme structuré utilisant des instructions if et 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.

7
Mike Dunlavey

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. Aux fins 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 basée sur un itérateur, avec un suivi arrière, d'une longueur d'environ 400 lignes. Cependant, il est facile de voir que tout ce que j'avais à faire était de construire des opérations booléennes simples, comme and, or et not, et quelques opérateurs de style regex comme * pour zéro ou plus, eps pour "correspondre à tout" et opt pour "correspondre à tout mais ne pas le consommer". 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 *it et la valeur transmise, et MakeRange est un simple <= >= test.

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

9
DeadMG

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'expressions 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 à l'aide d'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: 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.

Il s'agit d'une erreur courante, et Clang se rétablit parfaitement en réalisant qu'elle est simplement manquante et que void n'est pas une instance de Foo mais une partie de la déclaration suivante. 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.

3
Matthieu M.

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 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 quelques 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 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, étant 16 bits de large (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 à occuper un peu d'espace. Le remplissage de ces tables 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, il sera généralement un peu moins .

Un autre problème est que les lexers généralement générés 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 que vous 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 de poulet-œuf. 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 .

3
Alex ten Brink