web-dev-qa-db-fra.com

Pourquoi implémenter un lexer en tant que tableau 2D et un commutateur géant?

Je travaille lentement pour terminer mon diplôme et ce semestre est compilateur 101. Nous utilisons le livre de dragon . Peu de temps dans le cours et nous parlons de l'analyse lexicale et de la manière dont il peut être mis en œuvre via des automates finis déterministes (ci-après, DFA). Configurez vos différents états Lexer, définissez les transitions entre eux, etc.

Mais le professeur et le livre proposent de les mettre en œuvre via des tables de transition qui représentent un tableau 2D géant 2D (les différents états non terminaux en une dimension et les symboles d'entrée possibles comme l'autre) et une instruction de commutation pour gérer toutes les terminaux ainsi que l'envoi des tables de transition si dans un état non terminal.

La théorie est bien bonne et bonne, mais comme une personne qui a écrit du code depuis des décennies, la mise en œuvre est vile. Ce n'est pas testable, ce n'est pas responsable, ce n'est pas lisible, et c'est une douleur et une moitié de déboguer. Pire encore, je ne vois pas comment il serait pratique à distance si la langue était capable d'UTF. Avoir un million d'entrées de table de transition par état non terminal devient maltrée à la hâte.

Alors, quelle est l'accord? Pourquoi le livre définitif sur le sujet dit-il de cela de cette façon?

La surcharge de la fonction appelle-t-elle vraiment beaucoup? Est-ce quelque chose qui fonctionne bien ou est nécessaire lorsque la grammaire n'est pas connue à l'avance (expressions régulières?)? Ou peut-être quelque chose qui gère tous les cas, même si des solutions plus spécifiques fonctionneront mieux pour des grammaires plus spécifiques?

( Remarque: Dupliquer possible " pourquoi utiliser un OO approche au lieu d'une instruction de commutateur géant? "est proche, mais je me fiche de OO. Une approche fonctionnelle ou même une approche impérative SANER avec des fonctions autonomes irait bien.)

Et pour l'exemple de l'exemple, envisagez une langue que des identifiants que des identifiants et ces identificateurs sont [a-zA-Z]+. Dans la mise en œuvre de la DFA, vous obtiendriez quelque chose comme:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(bien que quelque chose qui gérerait correctement la fin du fichier)

Par rapport à ce à quoi je m'attendrais:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

Avec le code dans NextToken a refoulé dans sa propre fonction une fois que vous avez plusieurs destinations à partir du début de la DFA.

25
Telastyn

En pratique, ces tables sont générées à partir d'expressions régulières qui définissent les jetons de la langue:

number := [digit][digit|underscore]+
reserved_Word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

Nous avons eu des utilitaires pour générer des analyseurs lexicaux depuis 1975 lorsque Lex a été écrit.

Vous suggérez essentiellement de remplacer des expressions régulières avec du code de procédure. Cela élargit quelques caractères dans une expression régulière en plusieurs lignes de code. Code de procédure manuscrit pour l'analyse lexicale de toute langue modérément intéressante a tendance à être à la fois inefficace et difficile à maintenir.

17
kevin cline

La motivation de l'algorithme particulier est en grande partie que c'est un exercice d'apprentissage, il essaie donc de rester à proximité de l'idée d'une DFA et de garder les états et les transitions très explicites dans le code. En règle générale, personne ne écrivait manuellement aucun de ce code de toute façon - vous utiliseriez un outil pour générer du code d'une grammaire. Et cet outil ne se soucierait pas de la lisibilité du code car ce n'est pas un code source, c'est une sortie basée sur la définition d'une grammaire.

Votre code est plus propre pour quelqu'un de conserver une DFA écrite à la main, mais un peu plus éloigné des concepts enseignés.

7
psr

De la mémoire, - C'est une longue période depuis que j'ai lu le livre, et je suis sûr que je n'ai pas lu la dernière édition, je ne me souviens certain que de rien ressemblant à Java - - Cette partie a été écrite avec le code étant destinée à être un modèle, la table étant remplie d'un Lex comme Generator Lexer. Néanmoins de la mémoire, il y avait une section sur la compression de table (à nouveau de la mémoire, elle a été écrite de telle manière qu'il était également applicable aux analyseurs pilotés de table, donc peut-être plus loin dans le livre que ce que vous avez vu encore). De même, le livre que je me souviens avoir assumé un ensemble de caractères de 8 bits, je m'attendais à une section sur la manipulation de personnages plus importants dans des éditions ultérieures, probablement dans le cadre de la compression de la table. J'ai donné une autre façon de gérer cela comme une réponse à une question SO.

Il y a un avantage sûr de la performance dans la présentation d'une boucle serrée entraînée dans l'architecture moderne: elle est bien aménagée (si vous avez comprimé les tables) et que la prédiction de saut est aussi parfaite que possible (une manquer à la fin de la Lexem, peut-être une Mlle pour l'interrupteur expédiant au code qui dépend du symbole; qui suppose que votre décompression de table peut être effectuée avec des sauts prévisibles). Déplacement de cette machine d'état au code pur diminuerait la performance de prévision du saut et peut-être augmenter la pression de cache.

5
AProgrammer

Tard à la fête :-) Les jetons sont assortis contre des expressions régulières. Comme il y en a beaucoup d'entre eux, vous avez le Multi Regex Moteur, qui est à son tour DFA géant.

"Pire encore, je ne peux pas voir comment il serait pratique à distance si la langue était capable de UTF."

Il n'est pas pertinent (ou transparent). Outre UTF a une belle propriété, ses entités ne se chevauchent même pas partiellement. Par exemple. L'octet représentant le caractère "A" (à partir de la table ASCII-7) n'est plus utilisé pour tout autre caractère UTF.

Donc, vous avez une DFA unique (qui est multi-regex) pour l'ensemble du LXER. Comment mieux l'écrire que le tableau 2D?

0
greenoldman