Analyseur d'équation (expression) avec priorité?


105

J'ai développé un analyseur d'équation en utilisant un algorithme de pile simple qui gérera les opérateurs binaires (+, -, |, &, *, /, etc.), les opérateurs unaires (!) Et les parenthèses.

Cependant, l'utilisation de cette méthode me laisse avec tout ce qui a la même priorité - elle est évaluée de gauche à droite quel que soit l'opérateur, bien que la priorité puisse être appliquée en utilisant des parenthèses.

Donc pour l'instant "1 + 11 * 5" renvoie 60, pas 56 comme on pourrait s'y attendre.

Bien que cela convienne au projet actuel, je souhaite avoir une routine à usage général que je peux utiliser pour des projets ultérieurs.

Modifié pour plus de clarté:

Qu'est-ce qu'un bon algorithme pour analyser les équations avec priorité?

Je suis intéressé par quelque chose de simple à implémenter et je comprends que je peux coder moi-même pour éviter les problèmes de licence avec le code disponible.

Grammaire:

Je ne comprends pas la question de grammaire - je l'ai écrite à la main. C'est assez simple que je ne vois pas la nécessité de YACC ou de Bison. J'ai simplement besoin de calculer des chaînes avec des équations telles que "2 + 3 * (42/13)".

Langue:

Je fais cela en C, mais je suis intéressé par un algorithme, pas par une solution spécifique à un langage. C est suffisamment bas pour qu'il soit facile de se convertir dans une autre langue en cas de besoin.

Exemple de code

J'ai posté le code de test pour l'analyseur d'expression simple dont je parlais ci-dessus. Les exigences du projet ont changé et je n'ai donc jamais eu besoin d'optimiser le code pour les performances ou l'espace car il n'était pas incorporé dans le projet. C'est dans la forme verbeuse originale et devrait être facilement compréhensible. Si je fais quelque chose de plus avec cela en termes de priorité des opérateurs, je choisirai probablement le hack de macro car il correspond au reste du programme dans la simplicité. Si jamais je l'utilise dans un vrai projet, je vais opter pour un analyseur plus compact / rapide.

Question connexe

Conception intelligente d'un analyseur mathématique?

-Adam


J'ai écrit un analyseur d'expressions en C # sur mon blog. Il infixe pour postfixer sans la pile dans l'algorithme de triage de manœuvre. Il utilise uniquement un tableau.
Guge

Si je comprends bien, vous n'avez besoin d'analyser que l'expression arithmétique. Utilisez la notation polonaise inversée
mishadoff

Réponses:


69

À la dure

Vous voulez un analyseur de descente récursif .

Pour obtenir la priorité, vous devez penser de manière récursive, par exemple en utilisant votre exemple de chaîne,

1+11*5

pour ce faire manuellement, vous devrez lire le 1, puis voir le plus et démarrer une toute nouvelle "session" d'analyse récursive commençant par 11... et assurez-vous d'analyser le 11 * 5dans son propre facteur, ce qui donne un arbre d'analyse avec 1 + (11 * 5).

Tout cela semble si douloureux même d'essayer d'expliquer, en particulier avec l'impuissance supplémentaire de C.Voyez, après avoir analysé le 11, si le * était en fait un + à la place, vous devriez abandonner la tentative de création d'un terme et analyser à la place le 11lui-même en tant que facteur. Ma tête explose déjà. C'est possible avec la stratégie décente récursive, mais il y a une meilleure façon ...

La manière la plus simple (bonne)

Si vous utilisez un outil GPL comme Bison, vous n'avez probablement pas à vous soucier des problèmes de licence car le code C généré par bison n'est pas couvert par la GPL (IANAL mais je suis presque sûr que les outils GPL ne forcent pas la GPL sur code généré / binaires; par exemple, Apple compile du code comme, par exemple, Aperture avec GCC et ils le vendent sans avoir à GPL ledit code).

Téléchargez Bison (ou quelque chose d'équivalent, ANTLR, etc.).

Il existe généralement un exemple de code sur lequel vous pouvez simplement exécuter bison et obtenir le code C souhaité qui illustre cette calculatrice à quatre fonctions:

http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html

Regardez le code généré et voyez que ce n'est pas aussi simple qu'il y paraît. En outre, les avantages d'utiliser un outil comme Bison sont 1) vous apprenez quelque chose (surtout si vous lisez le livre Dragon et apprenez à connaître les grammaires), 2) vous évitez que les NIH tentent de réinventer la roue. Avec un véritable outil de générateur d'analyseurs, vous avez en fait l'espoir de passer à l'échelle plus tard, en montrant aux autres personnes que vous savez que les analyseurs sont le domaine des outils d'analyse.


Mettre à jour:

Les gens ici ont offert de nombreux conseils judicieux. Mon seul avertissement contre le fait de sauter les outils d'analyse ou simplement d'utiliser l'algorithme Shunting Yard ou un analyseur décent récursif roulé à la main est que les petits langages jouets 1 peuvent un jour se transformer en grands langages réels avec des fonctions (sin, cos, log) et des variables, des conditions et pour boucles.

Flex / Bison peut très bien être exagéré pour un petit interpréteur simple, mais un analyseur + évaluateur unique peut causer des problèmes en aval lorsque des modifications doivent être apportées ou des fonctionnalités doivent être ajoutées. Votre situation variera et vous devrez utiliser votre jugement; ne punissez pas les autres pour vos péchés [2] et construisez un outil moins qu'adéquat.

Mon outil d'analyse préféré

Le meilleur outil au monde pour ce travail est la bibliothèque Parsec (pour les parseurs récursifs décents) qui est fournie avec le langage de programmation Haskell. Cela ressemble beaucoup à BNF , ou à un outil spécialisé ou à un langage spécifique à un domaine pour l'analyse (exemple de code [3]), mais il s'agit en fait d'une bibliothèque ordinaire dans Haskell, ce qui signifie qu'il se compile dans la même étape de construction que le reste de votre code Haskell, et vous pouvez écrire du code Haskell arbitraire et l'appeler dans votre analyseur, et vous pouvez mélanger et faire correspondre d'autres bibliothèques dans le même code . (Incorporer un langage d'analyse comme celui-ci dans un langage autre que Haskell entraîne des tas de cruauté syntaxique, au fait. J'ai fait cela en C # et cela fonctionne assez bien mais ce n'est pas si joli et succinct.)

Remarques:

1 Richard Stallman dit, dans Pourquoi vous ne devriez pas utiliser Tcl

La principale leçon d'Emacs est qu'un langage pour extensions ne doit pas être un simple "langage d'extension". Ce devrait être un véritable langage de programmation, conçu pour écrire et maintenir des programmes importants. Parce que les gens voudront faire ça!

[2] Oui, je suis à jamais marqué par l'utilisation de ce «langage».

Notez également que lorsque j'ai soumis cette entrée, l'aperçu était correct, mais l'analyseur moins qu'adéquat de SO a mangé ma balise d'ancrage proche sur le premier paragraphe , prouvant que les analyseurs ne sont pas quelque chose à prendre à la légère, car si vous utilisez des expressions régulières et un piratage, vous obtiendra probablement quelque chose de subtil et petit mal .

[3] Extrait d'un analyseur Haskell utilisant Parsec: une calculatrice à quatre fonctions étendue avec des exposants, des parenthèses, des espaces pour la multiplication et des constantes (comme pi et e).

aexpr   =   expr `chainl1` toOp
expr    =   optChainl1 term addop (toScalar 0)
term    =   factor `chainl1` mulop
factor  =   sexpr  `chainr1` powop
sexpr   =   parens aexpr
        <|> scalar
        <|> ident

powop   =   sym "^" >>= return . (B Pow)
        <|> sym "^-" >>= return . (\x y -> B Pow x (B Sub (toScalar 0) y))

toOp    =   sym "->" >>= return . (B To)

mulop   =   sym "*" >>= return . (B Mul)
        <|> sym "/" >>= return . (B Div)
        <|> sym "%" >>= return . (B Mod)
        <|>             return . (B Mul)

addop   =   sym "+" >>= return . (B Add) 
        <|> sym "-" >>= return . (B Sub)

scalar = number >>= return . toScalar

ident  = literal >>= return . Lit

parens p = do
             lparen
             result <- p
             rparen
             return result

9
Pour souligner mon point, notez que le balisage dans mon article n'est pas analysé correctement (et cela varie entre le balisage rendu statiquement et celui rendu dans l'aperçu WMD). Il y a eu plusieurs tentatives pour résoudre ce problème, mais je pense que LE PARSER EST FAUX. Faites une faveur à tout le monde et faites une bonne analyse!
Jared Updike

155

L' algorithme de triage de manœuvre est le bon outil pour cela. Wikipedia est vraiment déroutant à ce sujet, mais fondamentalement, l'algorithme fonctionne comme ceci:

Disons que vous voulez évaluer 1 + 2 * 3 + 4. Intuitivement, vous «savez» que vous devez d'abord faire le 2 * 3, mais comment obtenez-vous ce résultat? La clé est de réaliser que lorsque vous balayez la chaîne de gauche à droite, vous évaluerez un opérateur lorsque l'opérateur qui le suit a une priorité inférieure (ou égale à). Dans le contexte de l'exemple, voici ce que vous souhaitez faire:

  1. Regardez: 1 + 2, ne faites rien.
  2. Maintenant, regardez 1 + 2 * 3, ne faites toujours rien.
  3. Maintenant, regardez 1 + 2 * 3 + 4, vous savez maintenant que 2 * 3 doit être évalué car l'opérateur suivant a une priorité inférieure.

Comment implémentez-vous cela?

Vous voulez avoir deux piles, une pour les nombres et une autre pour les opérateurs. Vous poussez des nombres sur la pile tout le temps. Vous comparez chaque nouvel opérateur avec celui en haut de la pile, si celui du haut de la pile a une priorité plus élevée, vous le faites sortir de la pile d'opérateurs, faites sortir les opérandes de la pile de nombres, appliquez l'opérateur et poussez le résultat sur la pile de nombres. Maintenant, vous répétez la comparaison avec l'opérateur du haut de la pile.

Pour en revenir à l'exemple, cela fonctionne comme ceci:

N = [] Ops = []

  • Lire 1. N = [1], Ops = []
  • Lire +. N = [1], Ops = [+]
  • Lire 2. N = [1 2], Ops = [+]
  • Lisez *. N = [1 2], Ops = [+ *]
  • Lire 3. N = [1 2 3], Ops = [+ *]
  • Lire +. N = [1 2 3], Ops = [+ *]
    • Pop 3, 2 et exécuter 2 *3, et pousser le résultat sur N. N = [1 6], Ops = [+]
    • +est laissé associatif, vous voulez donc également supprimer 1, 6 et exécuter le +. N = [7], Ops = [].
    • Enfin, poussez le [+] sur la pile d'opérateurs. N = [7], Ops = [+].
  • Lire 4. N = [7 4]. Ops = [+].
  • Vous êtes à court d'entrée, vous voulez donc vider les piles maintenant. Sur lequel vous obtiendrez le résultat 11.

Là, ce n'est pas si difficile, n'est-ce pas? Et il ne fait aucune invocation à des grammaires ou des générateurs d'analyseurs.


6
Vous n'avez pas réellement besoin de deux piles, tant que vous pouvez voir la deuxième chose sur la pile sans faire sauter le haut. Vous pouvez à la place utiliser une seule pile qui alterne les nombres et les opérateurs. En fait, cela correspond exactement à ce que fait un générateur d'analyseurs LR (comme bison).
Chris Dodd

2
Très belle explication de l'algorithme que je viens de mettre en œuvre en ce moment. De plus, vous ne le convertissez pas en postfix, ce qui est également bien. L'ajout de la prise en charge des parenthèses est également très simple.
Giorgi le

4
Une version simplifiée de l'algorithme de shunting -yard peut être trouvée ici: andreinc.net/2010/10/05/… (avec des implémentations en Java et python)
Andrei Ciobanu

1
Merci pour cela, exactement ce que je recherche!
Joe Green

Merci beaucoup pour la mention de gauche - associatif. Je suis resté avec l'opérateur ternaire: comment analyser les expressions complexes avec "?:" Imbriqué. J'ai réalisé que les deux '?' et «:» doivent avoir la même priorité. Et si nous interprétons '?' comme à droite - associatif et «:» comme à gauche - associatif, cet algorithme fonctionne très bien avec eux. De plus, nous ne pouvons réduire 2 opérateurs que lorsque les deux sont laissés - associatifs.
Vladislav

25

http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm

Très bonne explication des différentes approches:

  • Reconnaissance de descente récursive
  • L'algorithme de triage de manœuvre
  • La solution classique
  • Escalade de priorité

Rédigé en langage simple et pseudo-code.

J'aime celui de «montée en priorité».


Le lien semble rompu. Ce qui aurait fait une meilleure réponse aurait été de paraphraser chaque méthode de sorte que lorsque ce lien aurait disparu, certaines de ces informations utiles auraient été conservées ici.
Adam White

18

Il y a un bel article ici sur la combinaison d'un simple analyseur de descendance récursive avec l'analyse de la priorité des opérateurs. Si vous avez récemment écrit des analyseurs, cela devrait être très intéressant et instructif à lire.


16

Il y a longtemps, j'ai créé mon propre algorithme d'analyse, que je n'ai trouvé dans aucun livre sur l'analyse (comme le Dragon Book). En regardant les pointeurs vers l'algorithme Shunting Yard, je vois la ressemblance.

Il y a environ 2 ans, j'ai publié un article à ce sujet, avec le code source de Perl, sur http://www.perlmonks.org/?node_id=554516 . Il est facile de porter vers d'autres langages: la première implémentation que j'ai faite était dans l'assembleur Z80.

Il est idéal pour le calcul direct avec des nombres, mais vous pouvez l'utiliser pour produire un arbre d'analyse si nécessaire.

Mise à jour Parce que plus de personnes peuvent lire (ou exécuter) Javascript, j'ai réimplémenté mon analyseur en Javascript, après la réorganisation du code. L'analyseur entier est sous 5k de code Javascript (environ 100 lignes pour l'analyseur, 15 lignes pour une fonction wrapper), y compris les rapports d'erreurs et les commentaires.

Vous pouvez trouver une démo en direct sur http://users.telenet.be/bartl/expressionParser/expressionParser.html .

// operator table
var ops = {
   '+'  : {op: '+', precedence: 10, assoc: 'L', exec: function(l,r) { return l+r; } },
   '-'  : {op: '-', precedence: 10, assoc: 'L', exec: function(l,r) { return l-r; } },
   '*'  : {op: '*', precedence: 20, assoc: 'L', exec: function(l,r) { return l*r; } },
   '/'  : {op: '/', precedence: 20, assoc: 'L', exec: function(l,r) { return l/r; } },
   '**' : {op: '**', precedence: 30, assoc: 'R', exec: function(l,r) { return Math.pow(l,r); } }
};

// constants or variables
var vars = { e: Math.exp(1), pi: Math.atan2(1,1)*4 };

// input for parsing
// var r = { string: '123.45+33*8', offset: 0 };
// r is passed by reference: any change in r.offset is returned to the caller
// functions return the parsed/calculated value
function parseVal(r) {
    var startOffset = r.offset;
    var value;
    var m;
    // floating point number
    // example of parsing ("lexing") without aid of regular expressions
    value = 0;
    while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    if(r.string.substr(r.offset, 1) == ".") {
        r.offset++;
        while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    }
    if(r.offset > startOffset) {  // did that work?
        // OK, so I'm lazy...
        return parseFloat(r.string.substr(startOffset, r.offset-startOffset));
    } else if(r.string.substr(r.offset, 1) == "+") {  // unary plus
        r.offset++;
        return parseVal(r);
    } else if(r.string.substr(r.offset, 1) == "-") {  // unary minus
        r.offset++;
        return negate(parseVal(r));
    } else if(r.string.substr(r.offset, 1) == "(") {  // expression in parens
        r.offset++;   // eat "("
        value = parseExpr(r);
        if(r.string.substr(r.offset, 1) == ")") {
            r.offset++;
            return value;
        }
        r.error = "Parsing error: ')' expected";
        throw 'parseError';
    } else if(m = /^[a-z_][a-z0-9_]*/i.exec(r.string.substr(r.offset))) {  // variable/constant name        
        // sorry for the regular expression, but I'm too lazy to manually build a varname lexer
        var name = m[0];  // matched string
        r.offset += name.length;
        if(name in vars) return vars[name];  // I know that thing!
        r.error = "Semantic error: unknown variable '" + name + "'";
        throw 'unknownVar';        
    } else {
        if(r.string.length == r.offset) {
            r.error = 'Parsing error at end of string: value expected';
            throw 'valueMissing';
        } else  {
            r.error = "Parsing error: unrecognized value";
            throw 'valueNotParsed';
        }
    }
}

function negate (value) {
    return -value;
}

function parseOp(r) {
    if(r.string.substr(r.offset,2) == '**') {
        r.offset += 2;
        return ops['**'];
    }
    if("+-*/".indexOf(r.string.substr(r.offset,1)) >= 0)
        return ops[r.string.substr(r.offset++, 1)];
    return null;
}

function parseExpr(r) {
    var stack = [{precedence: 0, assoc: 'L'}];
    var op;
    var value = parseVal(r);  // first value on the left
    for(;;){
        op = parseOp(r) || {precedence: 0, assoc: 'L'}; 
        while(op.precedence < stack[stack.length-1].precedence ||
              (op.precedence == stack[stack.length-1].precedence && op.assoc == 'L')) {  
            // precedence op is too low, calculate with what we've got on the left, first
            var tos = stack.pop();
            if(!tos.exec) return value;  // end  reached
            // do the calculation ("reduce"), producing a new value
            value = tos.exec(tos.value, value);
        }
        // store on stack and continue parsing ("shift")
        stack.push({op: op.op, precedence: op.precedence, assoc: op.assoc, exec: op.exec, value: value});
        value = parseVal(r);  // value on the right
    }
}

function parse (string) {   // wrapper
    var r = {string: string, offset: 0};
    try {
        var value = parseExpr(r);
        if(r.offset < r.string.length){
          r.error = 'Syntax error: junk found at offset ' + r.offset;
            throw 'trailingJunk';
        }
        return value;
    } catch(e) {
        alert(r.error + ' (' + e + '):\n' + r.string.substr(0, r.offset) + '<*>' + r.string.substr(r.offset));
        return;
    }    
}

11

Cela vous aiderait si vous pouviez décrire la grammaire que vous utilisez actuellement pour analyser. On dirait que le problème réside peut-être là!

Éditer:

Le fait que vous ne compreniez pas la question de grammaire et que `` vous l'avez écrit à la main '' explique très probablement pourquoi vous rencontrez des problèmes avec les expressions de la forme `` 1 + 11 * 5 '' (c'est-à-dire avec la priorité des opérateurs) . Rechercher sur Google «grammaire pour les expressions arithmétiques», par exemple, devrait donner de bons pointeurs. Une telle grammaire n'a pas besoin d'être compliquée:

<Exp> ::= <Exp> + <Term> |
          <Exp> - <Term> |
          <Term>

<Term> ::= <Term> * <Factor> |
           <Term> / <Factor> |
           <Factor>

<Factor> ::= x | y | ... |
             ( <Exp> ) |
             - <Factor> |
             <Number>

ferait l'affaire par exemple, et peut être augmenté de manière triviale pour prendre en charge certaines expressions plus compliquées (y compris des fonctions par exemple, ou des pouvoirs, ...).

Je vous suggère de jeter un œil à ce fil, par exemple.

Presque toutes les introductions aux grammaires / analyses traitent les expressions arithmétiques comme un exemple.

Notez que l'utilisation d'une grammaire n'implique nullement l'utilisation d'un outil spécifique ( à la Yacc, Bison, ...). En effet, vous utilisez très certainement déjà la grammaire suivante:

<Exp>  :: <Leaf> | <Exp> <Op> <Leaf>

<Op>   :: + | - | * | /

<Leaf> :: <Number> | (<Exp>)

(ou quelque chose du genre) sans le savoir!


8

Avez-vous pensé à utiliser Boost Spirit ? Il vous permet d'écrire des grammaires de type EBNF en C ++ comme ceci:

group       = '(' >> expression >> ')';
factor      = integer | group;
term        = factor >> *(('*' >> factor) | ('/' >> factor));
expression  = term >> *(('+' >> term) | ('-' >> term));

1
+1 Et le résultat est que tout fait partie de Boost. La grammaire de la calculatrice est ici: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . L'implémentation de la calculatrice est ici: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Et la documentation est ici: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Je ne comprendrai jamais pourquoi les gens implémentent encore leurs propres mini-analyseurs.
stephan

5

Lorsque vous posez votre question, aucune récursivité n'est nécessaire. La réponse est trois choses: la notation Postfix plus l'algorithme Shunting Yard plus l'évaluation des expressions Postfix:

1). Notation Postfix = inventée pour éliminer le besoin de spécification de précédence explicite. En savoir plus sur le net mais en voici l'essentiel: expression infixe (1 + 2) * 3 tout en étant facile à lire pour les humains et traitement peu efficace pour le calcul via machine. Quel est? Règle simple qui dit "réécrire l'expression en la mettant en cache en priorité, puis toujours la traiter de gauche à droite". Ainsi infix (1 + 2) * 3 devient un suffixe 12 + 3 *. POST car l'opérateur est toujours placé APRÈS les opérandes.

2). Évaluation de l'expression postfix. Facile. Lire les nombres sur la chaîne de suffixe. Poussez-les sur une pile jusqu'à ce qu'un opérateur soit vu. Vérifier le type d'opérateur - unaire? binaire? tertiaire? Supprimez autant d'opérandes que nécessaire pour évaluer cet opérateur. Évaluer. Repoussez le résultat sur la pile! Et tu as presque fini. Continuez ainsi jusqu'à ce que la pile n'ait qu'une seule entrée = valeur que vous recherchez.

Faisons (1 + 2) * 3 qui est dans postfix est "12 + 3 *". Lire le premier nombre = 1. Poussez-le sur la pile. Lisez ensuite. Nombre = 2. Poussez-le sur la pile. Lisez ensuite. Opérateur. Laquelle? +. Quel genre? Binaire = nécessite deux opérandes. Pop stack deux fois = argright est 2 et argleft est 1. 1 + 2 est 3. Repoussez 3 sur la pile. Lire la suite de la chaîne de suffixe. C'est un nombre. 3. pousser. Lisez ensuite. Opérateur. Laquelle? *. Quel genre? Binaire = nécessite deux nombres -> pop pile deux fois. D'abord sautez dans l'argright, deuxième fois dans l'argleft. Évaluer l'opération - 3 fois 3 équivaut à 9. Lire le prochain caractère de suffixe. C'est nul. Fin de l'entrée. Pop stack onec = c'est votre réponse.

3). Shunting Yard est utilisé pour transformer une expression d'infixe (facilement) lisible par l'homme en une expression de suffixe (également facilement lisible par l'homme après une certaine pratique). Facile à coder manuellement. Voir les commentaires ci-dessus et net.


4

Y a-t-il une langue que vous souhaitez utiliser? ANTLR vous permettra de le faire dans une perspective Java. Adrian Kuhn a une excellente rédaction sur la façon d'écrire une grammaire exécutable en Ruby; en fait, son exemple est presque exactement votre exemple d'expression arithmétique.


Je dois admettre que mes exemples donnés dans le billet de blog se trompent sur la récursivité à gauche, c'est-à-dire que a - b - c s'évalue à (a - (b -c)) au lieu de ((a -b) - c). En fait, cela me rappelle d'ajouter une tâche que je devrais corriger les articles du blog.
akuhn

4

Cela dépend de la façon dont vous voulez que ce soit "général".

Si vous voulez que ce soit vraiment vraiment général, comme être capable d'analyser des fonctions mathématiques ainsi que sin (4 + 5) * cos (7 ^ 3), vous aurez probablement besoin d'un arbre d'analyse.

Dans lequel, je ne pense pas qu'une implémentation complète soit appropriée pour être collée ici. Je vous suggère de consulter l'un des tristement célèbres " Dragon book ".

Mais si vous voulez juste la prise en charge de la priorité , vous pouvez le faire en convertissant d'abord l'expression en forme de suffixe dans lequel un algorithme que vous pouvez copier-coller devrait être disponible sur Google ou je pense que vous pouvez le coder vous-même avec un binaire arbre.

Quand vous l'avez sous forme de postfix, alors c'est un jeu d'enfant puisque vous comprenez déjà comment la pile aide.


Le livre du dragon peut être un peu excessif pour un évaluateur d'expression - un simple analyseur de descente récursive est tout ce dont vous avez besoin, mais c'est une lecture incontournable si vous voulez faire quelque chose de plus étendu dans les compilateurs.
Eclipse

1
Wow - c'est agréable de savoir que le "livre du dragon" est toujours en discussion. Je me souviens de l'avoir étudié - et lu tout entier - à l'université, il y a 30 ans.
Schroedingers Cat

4

Je suggérerais de tricher et d'utiliser l' algorithme Shunting Yard . C'est un moyen simple d'écrire un analyseur simple de type calculatrice et qui prend en compte la priorité.

Si vous voulez tokeniser correctement les choses et avoir des variables, etc. impliquées, alors j'écrirais un analyseur de descente récursif comme suggéré par d'autres ici, mais si vous avez simplement besoin d'un analyseur de type calculatrice, cet algorithme devrait être suffisant :-)


4

J'ai trouvé ceci sur la PIClist à propos de l' algorithme Shunting Yard :

Harold écrit:

Je me souviens avoir lu, il y a longtemps, un algorithme qui convertissait des expressions algébriques en RPN pour une évaluation facile. Chaque valeur d'infixe ou opérateur ou parenthèse était représenté par un wagon sur une voie. Un type de voiture s'est séparé sur une autre piste et l'autre a continué tout droit. Je ne me souviens pas des détails (évidemment!), Mais j'ai toujours pensé que ce serait intéressant de coder. Cela remonte à l'époque où j'écrivais 6800 (et non 68000) code d'assemblage.

C'est "l'algorithme de triage de manœuvre" et c'est ce que la plupart des analyseurs de machines utilisent. Voir l'article sur l'analyse dans Wikipedia. Un moyen simple de coder l'algorithme de triage de manœuvre consiste à utiliser deux piles. L'un est la pile «push» et l'autre la pile «réduire» ou «résultat». Exemple:

pstack = () // vide rstack = () input: 1 + 2 * 3 precedence = 10 // le plus bas réduire = 0 // ne pas réduire

start: token '1': isnumber, mettre dans pstack (push) token '+': isoperator set precedence = 2 if precedence <previous_operator_precedence then reduction () // voir ci-dessous mettre '+' dans pstack (push) token '2' : isnumber, mettre dans pstack (push) token '*': isoperator, set precedence = 1, mettre dans pstack (push) // vérifier la priorité comme // au-dessus du token '3': isnumber, mettre dans pstack (push) fin de input, besoin de réduire (l'objectif est vide pstack) reduction () // fait

pour réduire, faites sortir les éléments de la pile de poussée et mettez-les dans la pile de résultats, échangez toujours les 2 premiers éléments sur pstack s'ils sont de la forme 'opérateur' 'nombre':

pstack: '1' '+' '2' ' ' '3' rstack: () ... pstack: () rstack: '3' '2' ' ' '1' '+'

si l'expression aurait été:

1 * 2 + 3

alors le déclencheur de réduction aurait été la lecture du jeton '+' qui a une précharge inférieure à celle du '*' déjà poussé, donc il l'aurait fait:

pstack: '1' ' ' '2' rstack: () ... pstack: () rstack: '1' '2' ' '

puis poussé '+' puis '3' et enfin réduit:

pstack: '+' '3' rstack: '1' '2' ' ' ... pstack: () rstack: '1' '2' ' ' '3' '+'

Donc, la version courte est: pousser les nombres, lorsque les opérateurs poussent, vérifiez la priorité de l'opérateur précédent. S'il était plus haut que celui de l'opérateur à pousser maintenant, réduisez d'abord, puis poussez l'opérateur actuel. Pour gérer les parens, il suffit de sauvegarder la priorité de l'opérateur «précédent» et de mettre une marque sur la pile p qui indique à l'algorithme de réduction d'arrêter de réduire lors de la résolution de l'intérieur d'une paire de paren. Le paren de fermeture déclenche une réduction comme le fait la fin de l'entrée, et supprime également la marque de paren ouverte de la pile p, et rétablit la priorité de «l'opération précédente» afin que l'analyse puisse continuer après le paren de fermeture là où il s'était arrêté. Cela peut être fait avec ou sans récursivité (indice: utilisez une pile pour stocker la priorité précédente lorsque vous rencontrez un '(' ...). La version généralisée de ceci est d'utiliser un générateur d'analyseur implémenté l'algorithme de shuntage de triage, par ex. en utilisant yacc ou bison ou taccle (analogue de tcl de yacc).

Peter

-Adam


4

Une autre ressource pour l'analyse de priorité est l' entrée de l' analyseur de priorité d'opérateur sur Wikipedia. Couvre l'algorithme de shunting yard de Dijkstra et un algorithme d'alternance d'arbre, mais couvre plus particulièrement un algorithme de remplacement de macro très simple qui peut être implémenté de manière triviale devant tout analyseur ignorant de priorité:

#include <stdio.h>
int main(int argc, char *argv[]){
  printf("((((");
  for(int i=1;i!=argc;i++){
    if(argv[i] && !argv[i][1]){
      switch(argv[i]){
      case '^': printf(")^("); continue;
      case '*': printf("))*(("); continue;
      case '/': printf("))/(("); continue;
      case '+': printf(")))+((("); continue;
      case '-': printf(")))-((("); continue;
      }
    }
    printf("%s", argv[i]);
  }
  printf("))))\n");
  return 0;
}

Appelez-le comme:

$ cc -o parenthesise parenthesise.c
$ ./parenthesise a \* b + c ^ d / e
((((a))*((b)))+(((c)^(d))/((e))))

Ce qui est génial dans sa simplicité et très compréhensible.


3
C'est une jolie petite perle. Mais l'étendre (par exemple, avec une application de fonction, une multiplication implicite, des opérateurs de préfixe et de suffixe, des annotations de type facultatives, n'importe quoi) casserait le tout. En d'autres termes, c'est un hack élégant.
Jared Updike

Je ne vois pas l'intérêt. Tout cela ne fait que changer un problème d'analyse de priorité d'opérateur en un problème d'analyse de priorité de parenthèses.
Marquis of Lorne

@EJP bien sûr, mais l'analyseur de la question gère très bien les parenthèses, c'est donc une solution raisonnable. Si vous avez un analyseur qui ne le fait pas, alors vous avez raison de dire que cela déplace simplement le problème vers un autre domaine.
Adam Davis

4

J'ai publié la source d'un évaluateur de mathématiques Java ultra compact (1 classe, <10 Kio) sur mon site Web. Il s'agit d'un analyseur de descente récursif du type qui a provoqué l'explosion crânienne pour l'affiche de la réponse acceptée.

Il prend en charge la priorité complète, les parenthèses, les variables nommées et les fonctions à argument unique.




2

Je travaille actuellement sur une série d'articles construisant un analyseur d'expressions régulières comme outil d'apprentissage pour les modèles de conception et la programmation lisible. Vous pouvez jeter un oeil à readablecode . L'article présente une utilisation claire de l'algorithme des chantiers de manœuvre.


2

J'ai écrit un analyseur d'expression en F # et j'en ai blogué ici . Il utilise l'algorithme de triage de manœuvre, mais au lieu de convertir d'infixe en RPN, j'ai ajouté une deuxième pile pour accumuler les résultats des calculs. Il gère correctement la priorité des opérateurs, mais ne prend pas en charge les opérateurs unaires. J'ai écrit ceci pour apprendre F #, pas pour apprendre l'analyse d'expression, cependant.


2

Une solution Python utilisant pyparsing peut être trouvée ici . L'analyse de la notation infixe avec divers opérateurs avec priorité est assez courante, et donc pyparsing inclut également le infixNotation(anciennement operatorPrecedence) générateur d'expression. Avec lui, vous pouvez facilement définir des expressions booléennes en utilisant "ET", "OU", "NON", par exemple. Ou vous pouvez développer votre arithmétique à quatre fonctions pour utiliser d'autres opérateurs, tels que! pour factoriel, ou '%' pour module, ou ajoutez les opérateurs P et C pour calculer les permutations et les combinaisons. Vous pouvez écrire un analyseur d'infixe pour la notation matricielle, qui inclut la gestion des opérateurs «-1» ou «T» (pour l'inversion et la transposition). L'exemple operatorPrecedence d'un analyseur à 4 fonctions (avec '!'.


1

Je sais que c'est une réponse tardive, mais je viens d'écrire un petit analyseur qui permet à tous les opérateurs (préfixe, suffixe et infixe-gauche, infixe-droite et non associatif) d'avoir une priorité arbitraire.

Je vais développer cela pour un langage avec un support DSL arbitraire, mais je voulais juste souligner que l'on n'a pas besoin d'analyseurs personnalisés pour la priorité des opérateurs, on peut utiliser un analyseur généralisé qui n'a pas du tout besoin de tables, et recherche simplement la priorité de chaque opérateur telle qu'elle apparaît. Les gens ont mentionné des analyseurs Pratt personnalisés ou des analyseurs de triage de manœuvre qui peuvent accepter des entrées illégales - celui-ci n'a pas besoin d'être personnalisé et (sauf s'il y a un bogue) n'acceptera pas de mauvaises entrées. Il n'est pas complet dans un sens, il a été écrit pour tester l'algorithme et son entrée est sous une forme qui nécessitera un prétraitement, mais il y a des commentaires qui le clarifient.

Notez que certains types d'opérateurs courants manquent, par exemple le type d'opérateur utilisé pour l'indexation, c'est-à-dire la table [index] ou l'appel d'une fonction de fonction (expression-paramètre, ...) Je vais les ajouter, mais pensez aux deux comme suffixe opérateurs dans lesquels ce qui se trouve entre les délimètres '[' et ']' ou '(' et ')' est analysé avec une instance différente de l'analyseur d'expression. Désolé d'avoir laissé cela de côté, mais la partie postfix est en cours - ajouter le reste doublera probablement presque la taille du code.

Puisque l'analyseur n'est que de 100 lignes de code de raquette, je devrais peut-être le coller ici, j'espère que ce n'est pas plus long que ce que permet le stackoverflow.

Quelques détails sur les décisions arbitraires:

Si un opérateur de suffixe de faible priorité est en concurrence pour les mêmes blocs d'infixe qu'un opérateur de préfixe de faible priorité, l'opérateur de préfixe l'emporte. Cela ne se produit pas dans la plupart des langues car la plupart n'ont pas d'opérateurs de suffixe de faible priorité. - par exemple: ((data a) (left 1 +) (pre 2 not) (data b) (post 3!) (left 1 +) (data c)) is a + not b! + c where not is a opérateur de préfixe et! est un opérateur postfix et les deux ont une priorité inférieure à + donc ils veulent grouper de manière incompatible soit comme (a + pas b!) + c ou comme a + (pas b! + c) dans ces cas, l'opérateur de préfixe l'emporte toujours, donc le la seconde est la façon dont il analyse

Les opérateurs d'infixe non associatifs sont vraiment là pour que vous n'ayez pas à prétendre que les opérateurs qui renvoient des types différents de ceux qu'ils prennent ont du sens ensemble, mais sans avoir différents types d'expression pour chacun, c'est un kludge. En tant que tel, dans cet algorithme, les opérateurs non associatifs refusent de s'associer non seulement à eux-mêmes, mais à tout opérateur ayant la même priorité. C'est un cas courant car <<= ==> = etc ne s'associent pas entre eux dans la plupart des langues.

La question de savoir comment différents types d'opérateurs (gauche, préfixe, etc.) rompent les liens de priorité est une question qui ne devrait pas se poser, car cela n'a pas vraiment de sens de donner aux opérateurs de types différents la même priorité. Cet algorithme fait quelque chose dans ces cas, mais je ne prends même pas la peine de comprendre exactement quoi parce qu'une telle grammaire est une mauvaise idée en premier lieu.

#lang racket
;cool the algorithm fits in 100 lines!
(define MIN-PREC -10000)
;format (pre prec name) (left prec name) (right prec name) (nonassoc prec name) (post prec name) (data name) (grouped exp)
;for example "not a*-7+5 < b*b or c >= 4"
;which groups as: not ((((a*(-7))+5) < (b*b)) or (c >= 4))"
;is represented as '((pre 0 not)(data a)(left 4 *)(pre 5 -)(data 7)(left 3 +)(data 5)(nonassoc 2 <)(data b)(left 4 *)(data b)(right 1 or)(data c)(nonassoc 2 >=)(data 4)) 
;higher numbers are higher precedence
;"(a+b)*c" is represented as ((grouped (data a)(left 3 +)(data b))(left 4 *)(data c))

(struct prec-parse ([data-stack #:mutable #:auto]
                    [op-stack #:mutable #:auto])
  #:auto-value '())

(define (pop-data stacks)
  (let [(data (car (prec-parse-data-stack stacks)))]
    (set-prec-parse-data-stack! stacks (cdr (prec-parse-data-stack stacks)))
    data))

(define (pop-op stacks)
  (let [(op (car (prec-parse-op-stack stacks)))]
    (set-prec-parse-op-stack! stacks (cdr (prec-parse-op-stack stacks)))
    op))

(define (push-data! stacks data)
    (set-prec-parse-data-stack! stacks (cons data (prec-parse-data-stack stacks))))

(define (push-op! stacks op)
    (set-prec-parse-op-stack! stacks (cons op (prec-parse-op-stack stacks))))

(define (process-prec min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((>= (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-prec min-prec stacks))))))))

(define (process-nonassoc min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((> (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-nonassoc min-prec stacks))
                   ((= (cadr op) min-prec) (error "multiply applied non-associative operator"))
                   ))))))

(define (apply-op op stacks)
  (let [(op-type (car op))]
    (cond ((eq? op-type 'post)
           (push-data! stacks `(,op ,(pop-data stacks) )))
          (else ;assume infix
           (let [(tos (pop-data stacks))]
             (push-data! stacks `(,op ,(pop-data stacks) ,tos))))))) 

(define (finish input min-prec stacks)
  (process-prec min-prec stacks)
  input
  )

(define (post input min-prec stacks)
  (if (null? input) (finish input min-prec stacks)
      (let* [(cur (car input))
             (input-type (car cur))]
        (cond ((eq? input-type 'post)
               (cond ((< (cadr cur) min-prec)
                      (finish input min-prec stacks))
                     (else 
                      (process-prec (cadr cur)stacks)
                      (push-data! stacks (cons cur (list (pop-data stacks))))
                      (post (cdr input) min-prec stacks))))
              (else (let [(handle-infix (lambda (proc-fn inc)
                                          (cond ((< (cadr cur) min-prec)
                                                 (finish input min-prec stacks))
                                                (else 
                                                 (proc-fn (+ inc (cadr cur)) stacks)
                                                 (push-op! stacks cur)
                                                 (start (cdr input) min-prec stacks)))))]
                      (cond ((eq? input-type 'left) (handle-infix process-prec 0))
                            ((eq? input-type 'right) (handle-infix process-prec 1))
                            ((eq? input-type 'nonassoc) (handle-infix process-nonassoc 0))
                            (else error "post op, infix op or end of expression expected here"))))))))

;alters the stacks and returns the input
(define (start input min-prec stacks)
  (if (null? input) (error "expression expected")
      (let* [(cur (car input))
             (input-type (car cur))]
        (set! input (cdr input))
        ;pre could clearly work with new stacks, but could it reuse the current one?
        (cond ((eq? input-type 'pre)
               (let [(new-stack (prec-parse))]
                 (set! input (start input (cadr cur) new-stack))
                 (push-data! stacks 
                             (cons cur (list (pop-data new-stack))))
                 ;we might want to assert here that the cdr of the new stack is null
                 (post input min-prec stacks)))
              ((eq? input-type 'data)
               (push-data! stacks cur)
               (post input min-prec stacks))
              ((eq? input-type 'grouped)
               (let [(new-stack (prec-parse))]
                 (start (cdr cur) MIN-PREC new-stack)
                 (push-data! stacks (pop-data new-stack)))
               ;we might want to assert here that the cdr of the new stack is null
               (post input min-prec stacks))
              (else (error "bad input"))))))

(define (op-parse input)
  (let [(stacks (prec-parse))]
    (start input MIN-PREC stacks)
    (pop-data stacks)))

(define (main)
  (op-parse (read)))

(main)

1

Voici une solution récursive de cas simple écrite en Java. Notez qu'il ne gère pas les nombres négatifs, mais vous pouvez l'ajouter si vous le souhaitez:

public class ExpressionParser {

public double eval(String exp){
    int bracketCounter = 0;
    int operatorIndex = -1;

    for(int i=0; i<exp.length(); i++){
        char c = exp.charAt(i);
        if(c == '(') bracketCounter++;
        else if(c == ')') bracketCounter--;
        else if((c == '+' || c == '-') && bracketCounter == 0){
            operatorIndex = i;
            break;
        }
        else if((c == '*' || c == '/') && bracketCounter == 0 && operatorIndex < 0){
            operatorIndex = i;
        }
    }
    if(operatorIndex < 0){
        exp = exp.trim();
        if(exp.charAt(0) == '(' && exp.charAt(exp.length()-1) == ')')
            return eval(exp.substring(1, exp.length()-1));
        else
            return Double.parseDouble(exp);
    }
    else{
        switch(exp.charAt(operatorIndex)){
            case '+':
                return eval(exp.substring(0, operatorIndex)) + eval(exp.substring(operatorIndex+1));
            case '-':
                return eval(exp.substring(0, operatorIndex)) - eval(exp.substring(operatorIndex+1));
            case '*':
                return eval(exp.substring(0, operatorIndex)) * eval(exp.substring(operatorIndex+1));
            case '/':
                return eval(exp.substring(0, operatorIndex)) / eval(exp.substring(operatorIndex+1));
        }
    }
    return 0;
}

}


1

L'algorithme pourrait être facilement encodé en C comme analyseur de descente récursif.

#include <stdio.h>
#include <ctype.h>

/*
 *  expression -> sum
 *  sum -> product | product "+" sum
 *  product -> term | term "*" product
 *  term -> number | expression
 *  number -> [0..9]+
 */

typedef struct {
    int value;
    const char* context;
} expression_t;

expression_t expression(int value, const char* context) {
    return (expression_t) { value, context };
}

/* begin: parsers */

expression_t eval_expression(const char* symbols);

expression_t eval_number(const char* symbols) {
    // number -> [0..9]+
    double number = 0;        
    while (isdigit(*symbols)) {
        number = 10 * number + (*symbols - '0');
        symbols++;
    }
    return expression(number, symbols);
}

expression_t eval_term(const char* symbols) {
    // term -> number | expression
    expression_t number = eval_number(symbols);
    return number.context != symbols ? number : eval_expression(symbols);
}

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

expression_t eval_sum(const char* symbols) {
    // sum -> product | product "+" sum
    expression_t product = eval_product(symbols);
    if (*product.context != '+')
        return product;

    expression_t sum = eval_sum(product.context + 1);
    return expression(product.value + sum.value, sum.context);
}

expression_t eval_expression(const char* symbols) {
    // expression -> sum
    return eval_sum(symbols);
}

/* end: parsers */

int main() {
    const char* expression = "1+11*5";
    printf("eval(\"%s\") == %d\n", expression, eval_expression(expression).value);

    return 0;
}

les librairies suivantes pourraient être utiles: yupana - opérations strictement arithmétiques; tinyexpr - opérations arithmétiques + fonctions mathématiques C + une fournie par l'utilisateur; mpc - combinateurs d'analyseurs

Explication

Capturons la séquence de symboles qui représentent l'expression algébrique. Le premier est un nombre, c'est-à-dire un chiffre décimal répété une ou plusieurs fois. Nous appellerons cette notation une règle de production.

number -> [0..9]+

L'opérateur d'addition avec ses opérandes est une autre règle. C'est l'un numberou l'autre des symboles qui représente la sum "*" sumséquence.

sum -> number | sum "+" sum

Substitut Essayez numberdans sum "+" sumce sera number "+" numberà son tour pourrait être élargi en [0..9]+ "+" [0..9]+que , finalement , pourrait être réduit à ce 1+8qui est l' expression d'addition correcte.

D'autres substitutions produiront également une expression correcte: sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

Petit à petit, nous pourrions ressembler à un ensemble de règles de production aka grammaire qui expriment toutes les expressions algébriques possibles.

expression -> sum
sum -> difference | difference "+" sum
difference -> product | difference "-" product
product -> fraction | fraction "*" product
fraction -> term | fraction "/" term
term -> "(" expression ")" | number
number -> digit+                                                                    

Pour contrôler la priorité de l'opérateur, modifiez la position de sa règle de production par rapport aux autres. Regardez la grammaire ci-dessus et notez que la règle de production pour *est placée en dessous, +cela forcera l' productévaluation avant sum. La mise en œuvre combine simplement la reconnaissance de formes avec l'évaluation et reflète ainsi étroitement les règles de production.

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

Ici, nous évaluons d' termabord et le retournons s'il n'y a pas de *caractère après celui- ci, c'est à gauche de choisir dans notre règle de production sinon - évaluer les symboles après et renvoyer term.value * product.value c'est le bon choix dans notre règle de production, c'est-à-direterm "*" product

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.