Analisador de equação (expressão) com precedência?


104

Desenvolvi um analisador de equação usando um algoritmo de pilha simples que manipulará operadores binários (+, -, |, &, *, /, etc), operadores unários (!) E parênteses.

Usar este método, no entanto, me deixa com tudo tendo a mesma precedência - é avaliado da esquerda para a direita, independentemente do operador, embora a precedência possa ser aplicada usando parênteses.

Portanto, agora "1 + 11 * 5" retorna 60, não 56, como seria de se esperar.

Embora seja adequado para o projeto atual, quero ter uma rotina de uso geral que possa usar para projetos posteriores.

Editado para maior clareza:

Qual é um bom algoritmo para analisar equações com precedência?

Estou interessado em algo simples de implementar e entendo que posso me programar para evitar problemas de licenciamento com o código disponível.

Gramática:

Não entendo a pergunta de gramática - escrevi isso à mão. É tão simples que não vejo necessidade de YACC ou Bison. Eu simplesmente preciso calcular strings com equações como "2 + 3 * (42/13)".

Língua:

Estou fazendo isso em C, mas estou interessado em um algoritmo, não em uma solução específica de linguagem. C é de nível baixo o suficiente para que seja fácil converter para outro idioma, caso haja necessidade.

Exemplo de Código

Publiquei o código de teste para o analisador de expressão simples de que falei acima. Os requisitos do projeto foram alterados e, portanto, nunca precisei otimizar o código para desempenho ou espaço, pois não foi incorporado ao projeto. Está na forma verbosa original e deve ser facilmente compreensível. Se eu fizer mais alguma coisa com ele em termos de precedência de operador, provavelmente irei escolher o hack macro porque ele corresponde ao resto do programa em simplicidade. Se eu alguma vez usar isso em um projeto real, porém, vou optar por um analisador mais compacto / rápido.

Questão relacionada

O design inteligente de um analisador matemático?

-Adão


Eu escrevi um analisador de expressão em C # no meu blog. Ele infixa para postfixar sem a pilha no algoritmo do pátio de manobras. Ele usa apenas uma matriz.
Guge

Pelo que entendi, você precisa analisar apenas a expressão aritmética. Usar notação polonesa reversa
mishadoff de

Respostas:


69

O jeito difícil

Você quer um analisador descendente recursivo .

Para obter precedência, você precisa pensar recursivamente, por exemplo, usando sua string de amostra,

1+11*5

para fazer isso manualmente, você teria que ler o 1, ver o sinal de mais e iniciar uma nova "sessão" de análise recursiva começando com 11... e certifique-se de analisar 11 * 5em seu próprio fator, gerando uma árvore de análise com1 + (11 * 5) .

Tudo isso parece tão doloroso até mesmo para tentar explicar, especialmente com a impotência adicional de C. Veja, depois de analisar o 11, se o * fosse realmente um + em vez disso, você teria que abandonar a tentativa de criar um termo e, em vez disso, analisar o 11 se como um fator. Minha cabeça já está explodindo. É possível com a estratégia decente recursiva, mas existe uma maneira melhor ...

A maneira fácil (certa)

Se você usa uma ferramenta GPL como o Bison, provavelmente não precisa se preocupar com problemas de licenciamento, já que o código C gerado pelo bison não é coberto pela GPL (IANAL, mas tenho certeza de que as ferramentas GPL não forçam a GPL código / binários gerados; por exemplo, a Apple compila códigos como, digamos, Aperture com GCC e os vende sem ter que colocar o código em GPL).

Baixar Bison (ou algo equivalente, ANTLR, etc.).

Normalmente, há algum código de amostra em que você pode simplesmente executar o bison e obter o código C desejado que demonstra esta calculadora de quatro funções:

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

Observe o código gerado e veja que isso não é tão fácil quanto parece. Além disso, as vantagens de usar uma ferramenta como o Bison são: 1) você aprende algo (especialmente se você ler o livro do Dragão e aprender sobre gramáticas), 2) você evita o NIH tentando reinventar a roda. Com uma ferramenta real de gerador de analisador, você realmente tem esperança de aumentar a escala mais tarde, mostrando a outras pessoas que você conhece que analisadores são o domínio das ferramentas de análise.


Atualizar:

As pessoas aqui ofereceram muitos bons conselhos. Meu único aviso contra pular as ferramentas de análise ou apenas usar o algoritmo Shunting Yard ou um analisador decente recursivo rolado à mão é que pequenas linguagens de brinquedo 1 podem algum dia se transformar em grandes linguagens reais com funções (sin, cos, log) e variáveis, condições e para rotações.

Flex / Bison pode muito bem ser um exagero para um intérprete pequeno e simples, mas um analisador + avaliador único pode causar problemas no futuro quando mudanças precisam ser feitas ou recursos precisam ser adicionados. Sua situação irá variar e você precisará usar seu bom senso; apenas não puna outras pessoas por seus pecados [2] e construa uma ferramenta menos do que adequada.

Minha ferramenta favorita para análise

A melhor ferramenta do mundo para esse trabalho é a biblioteca Parsec (para analisadores decentes recursivos) que vem com a linguagem de programação Haskell. Parece muito com o BNF , ou como alguma ferramenta especializada ou linguagem específica de domínio para análise (código de amostra [3]), mas é na verdade apenas uma biblioteca regular em Haskell, o que significa que compila na mesma etapa de construção que o resto de seu código Haskell, e você pode escrever código Haskell arbitrário e chamá-lo dentro de seu analisador, e você pode misturar e combinar outras bibliotecas no mesmo código . (Incorporar uma linguagem de análise como esta em uma linguagem diferente de Haskell resulta em muitos problemas sintáticos, a propósito. Eu fiz isso em C # e funciona muito bem, mas não é tão bonito e sucinto.)

Notas:

1 Richard Stallman diz, em Por que você não deve usar o Tcl

A principal lição do Emacs é que uma linguagem para extensões não deve ser uma mera "linguagem de extensão". Deve ser uma linguagem de programação real, projetada para escrever e manter programas substanciais. Porque as pessoas vão querer fazer isso!

[2] Sim, estou para sempre marcado por usar essa "linguagem".

Observe também que quando eu enviei esta entrada, a visualização estava correta, mas o analisador menos do que adequado comeu minha tag de fechar âncora no primeiro parágrafo , provando que os analisadores não são algo para se brincar porque se você usar regexes e um deles te hackear provavelmente errará algo sutil e pequeno .

[3] Snippet de um parser Haskell usando Parsec: uma calculadora de quatro funções ampliada com expoentes, parênteses, espaços em branco para multiplicação e constantes (como pi e 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
Para enfatizar meu ponto, observe que a marcação em minha postagem não está sendo analisada corretamente (e isso varia entre a marcação renderizada estaticamente e aquela renderizada na visualização do WMD). Houve várias tentativas de consertar, mas acho que THE PARSER ESTÁ ERRADO. Faça um favor a todos e analise corretamente!
Jared Updike

155

O algoritmo do pátio de manobras é a ferramenta certa para isso. A Wikipedia é muito confusa sobre isso, mas basicamente o algoritmo funciona assim:

Digamos que você queira avaliar 1 + 2 * 3 + 4. Intuitivamente, você "sabe" que precisa fazer 2 * 3 primeiro, mas como obter esse resultado? A chave é perceber que, ao escanear a string da esquerda para a direita, você avaliará um operador quando o operador que o segue tiver uma precedência inferior (ou igual a). No contexto do exemplo, aqui está o que você deseja fazer:

  1. Observe: 1 + 2, não faça nada.
  2. Agora olhe para 1 + 2 * 3, ainda não faça nada.
  3. Agora olhe para 1 + 2 * 3 + 4, agora você sabe que 2 * 3 deve ser avaliado porque o próximo operador tem precedência inferior.

Como você implementa isso?

Você deseja ter duas pilhas, uma para números e outra para operadores. Você coloca números na pilha o tempo todo. Você compara cada novo operador com o que está no topo da pilha, se o que está no topo da pilha tiver maior prioridade, você o retira da pilha de operadores, retira os operandos da pilha de números, aplica o operador e empurra o resultado na pilha de números. Agora você repete a comparação com o operador do topo da pilha.

Voltando ao exemplo, funciona assim:

N = [] Ops = []

  • Leia 1. N = [1], Ops = []
  • Leia +. N = [1], Ops = [+]
  • Leia 2. N = [1 2], Ops = [+]
  • Leia *. N = [1 2], Ops = [+ *]
  • Leia 3. N = [1 2 3], Ops = [+ *]
  • Leia +. N = [1 2 3], Ops = [+ *]
    • Estale 3, 2 e execute 2 *3, e empurre o resultado para N. N = [1 6], Ops = [+]
    • +é associativo à esquerda, então você deseja retirar 1, 6 também e executar o +. N = [7], Ops = [].
    • Por fim, coloque o [+] na pilha de operadores. N = [7], Ops = [+].
  • Leia 4. N = [7 4]. Ops = [+].
  • Você está sem entrada, então deseja esvaziar as pilhas agora. Após o qual você obterá o resultado 11.

Pronto, isso não é tão difícil, não é? E ele não invoca nenhuma gramática ou gerador de analisador.


6
Na verdade, você não precisa de duas pilhas, desde que possa ver a segunda coisa na pilha sem abrir o topo. Em vez disso, você pode usar uma única pilha que alterna números e operadores. Na verdade, isso corresponde exatamente ao que um gerador de analisador LR (como o bison) faz.
Chris Dodd

2
Explicação muito boa do algoritmo que acabei de implementar agora. Além disso, você não está convertendo para postfix, o que também é bom. Adicionar suporte para parênteses também é muito fácil.
Giorgi

4
Uma versão simplificada do algoritmo do pátio de manobras pode ser encontrada aqui: andreinc.net/2010/10/05/… (com implementações em Java e python)
Andrei Ciobanu

1
Obrigado por isso, exatamente o que estou procurando!
Joe Green

Muito obrigado pela menção sobre associativo à esquerda. Eu fiquei com o operador ternário: como analisar expressões complexas com "?:" Aninhado. Eu percebi que ambos '?' e ':' têm que ter a mesma prioridade. E se interpretarmos '?' como associativo à direita e ':' como associativo à esquerda, esse algoritmo funciona muito bem com eles. Além disso, podemos recolher 2 operadores apenas quando ambos são associativos à esquerda.
Vladislav

25

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

Explicação muito boa das diferentes abordagens:

  • Reconhecimento descendente recursivo
  • O algoritmo do pátio de manobras
  • A solução clássica
  • Escalada de precedência

Escrito em linguagem simples e pseudo-código.

Eu gosto de 'escalada de precedência'.


O link parece estar quebrado. O que teria sido uma resposta melhor teria sido parafrasear cada método para que, quando aquele link desaparecesse, algumas das informações úteis tivessem sido preservadas aqui.
Adam White

18

Há um bom artigo aqui sobre como combinar um analisador descendente recursivo simples com análise de precedência de operador. Se você escreveu analisadores recentemente, deve ser muito interessante e instrutivo de ler.


16

Há muito tempo atrás, criei meu próprio algoritmo de análise, que não consegui encontrar em nenhum livro sobre análise (como o Livro do Dragão). Olhando para os ponteiros para o algoritmo Shunting Yard, eu vejo a semelhança.

Cerca de 2 anos atrás, eu fiz uma postagem sobre isso, completo com código-fonte Perl, em http://www.perlmonks.org/?node_id=554516 . É fácil portar para outras linguagens: a primeira implementação que fiz foi no Z80 assembler.

É ideal para cálculos diretos com números, mas você pode usá-lo para produzir uma árvore de análise, se necessário.

Atualizar Como mais pessoas podem ler (ou executar) Javascript, reimplementei meu analisador em Javascript, depois que o código foi reorganizado. Todo o analisador tem menos de 5k de código Javascript (cerca de 100 linhas para o analisador, 15 linhas para uma função de wrapper), incluindo relatórios de erros e comentários.

Você pode encontrar uma demonstração ao vivo em 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

Seria útil se você pudesse descrever a gramática que está usando atualmente para analisar. Parece que o problema pode estar aí!

Editar:

O fato de você não entender a questão gramatical e 'você escreveu isso à mão' muito provavelmente explica por que você está tendo problemas com expressões da forma '1 + 11 * 5' (ou seja, com precedência de operador) . Buscar no Google por 'gramática para expressões aritméticas', por exemplo, deve render algumas boas dicas. Essa gramática não precisa ser complicada:

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

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

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

faria o truque, por exemplo, e pode ser aumentado trivialmente para cuidar de algumas expressões mais complicadas (incluindo funções, por exemplo, ou poderes, ...).

Eu sugiro que você dê uma olhada neste tópico, por exemplo.

Quase todas as introduções à gramática / análise tratam as expressões aritméticas como um exemplo.

Observe que usar uma gramática não implica em usar uma ferramenta específica ( a la Yacc, Bison, ...). Na verdade, você certamente já está usando a seguinte gramática:

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

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

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

(ou algo do tipo) sem saber!


8

Você já pensou em usar o Boost Spirit ? Ele permite que você escreva gramáticas semelhantes a EBNF em C ++ como esta:

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

1
+1 E o resultado é que tudo faz parte do Boost. A gramática da calculadora está aqui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . A implementação da calculadora está aqui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . E a documentação está aqui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Eu nunca vou entender porque as pessoas ainda implementam seus próprios mini-parsers.
stephan de

5

Ao fazer sua pergunta, não há necessidade de recursão alguma. A resposta é três coisas: Notação Postfix mais algoritmo Shunting Yard mais avaliação de expressão Postfix:

1). Notação Postfix = inventada para eliminar a necessidade de especificação de precedência explícita. Leia mais na rede, mas aqui está o ponto principal: expressão infixo (1 + 2) * 3 embora seja fácil para humanos ler e processar, não muito eficiente para computação via máquina. O que é? Regra simples que diz "reescreva a expressão armazenando em cache na precedência e sempre processe da esquerda para a direita". Portanto, infixo (1 + 2) * 3 torna-se um pós-fixado 12 + 3 *. POST porque o operador é colocado sempre APÓS os operandos.

2). Avaliando a expressão pós-fixada. Fácil. Leia os números fora da string pós-fixada. Empurre-os em uma pilha até que um operador seja visto. Verifique o tipo de operador - unário? binário? terciário? Retire tantos operandos da pilha quantos forem necessários para avaliar esse operador. Avalie. Coloque o resultado de volta na pilha! E você está quase pronto. Continue fazendo isso até que a pilha tenha apenas uma entrada = valor que você está procurando.

Vamos fazer (1 + 2) * 3 que está em postfix é "12 + 3 *". Leia o primeiro número = 1. Empurre-o na pilha. Leia a seguir. Número = 2. Empurre-o na pilha. Leia a seguir. Operador. Qual? +. Que tipo? Binário = precisa de dois operandos. Pop stack duas vezes = argright é 2 e argleft é 1. 1 + 2 é 3. Empurre 3 de volta na pilha. Leia a seguir a partir da string Postfix. É um número. 3. Empurre. Leia a seguir. Operador. Qual? *. Que tipo? Binário = precisa de dois números -> pop stack duas vezes. Primeiro entre em argright, segunda vez em argleft. Avalie a operação - 3 vezes 3 é 9. Empurre 9 na pilha. Leia o próximo caractere postfix. É nulo. Fim da entrada. Pop stack onec = essa é a sua resposta.

3). O Shunting Yard é usado para transformar a expressão infixada (facilmente) legível para humanos em expressão pós-fixada (também facilmente legível para humanos após alguma prática). Fácil de codificar manualmente. Veja os comentários acima e net.


4

Existe um idioma que você deseja usar? ANTLR permitirá que você faça isso a partir de uma perspectiva Java. Adrian Kuhn tem um excelente artigo sobre como escrever uma gramática executável em Ruby; na verdade, seu exemplo é quase exatamente o seu exemplo de expressão aritmética.


Devo admitir que meus exemplos dados na postagem do blog estão obtendo recursão à esquerda errada, ou seja, a - b - c avalia para (a - (b -c)) em vez de ((a -b) - c). Na verdade, isso me lembra de adicionar uma tarefa que devo corrigir as postagens do blog.
Akuhn

4

Depende de quão "geral" você deseja que seja.

Se você quiser que seja realmente geral, como ser capaz de analisar funções matemáticas, bem como sin (4 + 5) * cos (7 ^ 3), você provavelmente precisará de uma árvore de análise.

No qual, eu não acho que uma implementação completa seja apropriada para ser colada aqui. Eu sugiro que você dê uma olhada em um dos infames " livros do dragão ".

Mas se você quiser apenas suporte de precedência , poderá fazer isso primeiro convertendo a expressão para a forma pós-fixada, na qual um algoritmo que você pode copiar e colar deve estar disponível no google ou eu acho que você mesmo pode codificá-lo com um binário árvore.

Quando você tem a forma pós-fixada, é moleza a partir de então, pois você já sabe como a pilha ajuda.


O livro do dragão pode ser um pouco excessivo para um avaliador de expressão - um analisador descendente recursivo simples é tudo o que é necessário, mas é uma leitura obrigatória se você quiser fazer algo mais extenso em compiladores.
Eclipse

1
Uau - é bom saber que o "livro do dragão" ainda é discutido. Lembro-me de estudá-lo - e lê-lo inteiro - na universidade, 30 anos atrás.
Schroedingers Cat

4

Eu sugeriria trapacear e usar o algoritmo Shunting Yard . É um meio fácil de escrever um analisador simples do tipo calculadora e leva a precedência em consideração.

Se você deseja tokenizar as coisas apropriadamente e ter variáveis, etc. envolvidas, então eu iria em frente e escreveria um analisador descendente recursivo como sugerido por outros aqui, no entanto, se você simplesmente precisar de um analisador do tipo calculadora, este algoritmo deve ser suficiente :-)


4

Eu encontrei isso na PIClist sobre o algoritmo de Shunting Yard :

Harold escreve:

Lembro-me de ter lido, muito tempo atrás, um algoritmo que convertia expressões algébricas em RPN para fácil avaliação. Cada valor infixo ou operador ou parêntese foi representado por um vagão em um trilho. Um tipo de carro se separou para outra pista e o outro continuou em frente. Não me lembro dos detalhes (obviamente!), Mas sempre achei que seria interessante codificar. Isso foi quando eu estava escrevendo o código assembly 6800 (não 68000).

Este é o "algoritmo do pátio de manobras" e é o que a maioria dos analisadores de máquina usa. Veja o artigo sobre análise na Wikipedia. Uma maneira fácil de codificar o algoritmo do pátio de manobras é usar duas pilhas. Uma é a pilha "push" e a outra a pilha "reduzir" ou "resultado". Exemplo:

pstack = () // vazio rstack = () input: 1 + 2 * 3 precedência = 10 // menor reduzir = 0 // não reduzir

start: token '1': isnumber, colocado em pstack (push) token '+': isoperator definir precedência = 2 se precedência <previous_operator_precedence then reduce () // veja abaixo colocar '+' em pstack (push) token '2' : isnumber, colocar em pstack (push) token '*': isoperator, definir precedência = 1, colocar em pstack (push) // verificar a precedência como // acima do token '3': isnumber, colocar em pstack (push) fim de entrada, necessidade de reduzir (o objetivo é pstack vazio) reduzir () // concluído

para reduzir, destaque os elementos da pilha push e coloque-os na pilha de resultados, sempre troque os 2 itens principais em pstack se eles estiverem no formato 'operador' 'número':

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

se a expressão fosse:

1 * 2 + 3

então, o gatilho de redução teria sido a leitura do token '+', que tem precendece menor do que o '*' já pressionado, então teria feito:

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

e então apertou '+' e depois '3' e finalmente reduziu:

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

Portanto, a versão resumida é: empurrar números, ao empurrar os operadores, verifique a precedência do operador anterior. Se fosse maior do que o operador que deve ser empurrado agora, primeiro reduza e, em seguida, empurre o operador atual. Para lidar com os parênteses, basta salvar a precedência do operador 'anterior' e colocar uma marca na pstack que informa ao algoritmo de redução para parar de reduzir ao resolver o interior de um par de parênteses. O parêntese de fechamento dispara uma redução, assim como o fim da entrada, e também remove a marca de parênteses de abertura do pstack e restaura a precedência da 'operação anterior' para que a análise possa continuar após o parêntese de fechamento de onde parou. Isso pode ser feito com recursão ou sem (dica: use uma pilha para armazenar a precedência anterior ao encontrar um '(' ...). A versão generalizada disso é usar um gerador de analisador implementado algoritmo de pátio de manobras, f.ex. usando yacc ou bison ou taccle (análogo tcl de yacc).

Peter

-Adão


4

Outro recurso para análise de precedência é a entrada do analisador de precedência do operador na Wikipedia. Abrange o algoritmo de pátio de manobra de Dijkstra e um algoritmo alternativo de árvore, mas mais notavelmente cobre um algoritmo de substituição de macro realmente simples que pode ser implementado trivialmente na frente de qualquer analisador ignorante de precedência:

#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;
}

Invoque-o como:

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

O que é incrível em sua simplicidade e muito compreensível.


3
É uma pequena pérola bonita. Mas estendê-lo (digamos, com a aplicação de função, multiplicação implícita, operadores de prefixo e pós-fixados, anotações de tipo opcionais, qualquer coisa) quebraria tudo. Em outras palavras, é um hack elegante.
Jared Updike

Eu não vejo o ponto. Tudo o que isso faz é mudar um problema de análise de precedência de operador em um problema de análise de precedência de parênteses.
Marquês de Lorne

@EJP com certeza, mas o analisador na questão lida perfeitamente com parênteses, então esta é uma solução razoável. No entanto, se você tiver um analisador que não funciona, está correto ao dizer que isso apenas move o problema para outra área.
Adam Davis

4

Publiquei o código-fonte de um Java Math Evaluator ultracompacto (1 classe, <10 KiB) em meu site. Este é um analisador descendente recursivo do tipo que causou a explosão craniana para o pôster da resposta aceita.

Ele suporta precedência total, parênteses, variáveis ​​nomeadas e funções de argumento único.




2

Atualmente, estou trabalhando em uma série de artigos construindo um analisador de expressão regular como uma ferramenta de aprendizado para padrões de projeto e programação legível. Você pode dar uma olhada em readablecode . O artigo apresenta um uso claro do algoritmo de jardas de manobra.


2

Eu escrevi um analisador de expressão em F # e escrevi sobre ele aqui . Ele usa o algoritmo do pátio de manobras, mas em vez de converter de infixo em RPN, adicionei uma segunda pilha para acumular os resultados dos cálculos. Ele lida corretamente com a precedência do operador, mas não oferece suporte a operadores unários. Escrevi isso para aprender F #, não para aprender a análise de expressão, no entanto.


2

Uma solução Python usando pyparsing pode ser encontrada aqui . Analisar notação infixa com vários operadores com precedência é bastante comum e, portanto, pyparsing também inclui o infixNotation(anteriormente operatorPrecedence) construtor de expressão. Com ele você pode definir facilmente expressões booleanas usando "AND", "OR", "NOT", por exemplo. Ou você pode expandir sua aritmética de quatro funções para usar outros operadores, como! para fatorial ou '%' para módulo, ou adicione os operadores P e C para calcular permutações e combinações. Você poderia escrever um analisador de infixo para notação de matriz, que inclui o manuseio de operadores '-1' ou 'T' (para inversão e transposição). O exemplo operatorPrecedence de um analisador de 4 funções (com '!'.


1

Eu sei que esta é uma resposta tardia, mas acabei de escrever um analisador minúsculo que permite que todos os operadores (prefixo, pós-fixo e infixo-esquerdo, infixo-direito e não associativo) tenham precedência arbitrária.

Vou expandir isso para uma linguagem com suporte a DSL arbitrário, mas só queria salientar que não é necessário analisar analisadores personalizados para a precedência do operador, é possível usar um analisador generalizado que não precisa de tabelas e apenas verifica a precedência de cada operador conforme ele aparece. As pessoas têm mencionado analisadores Pratt personalizados ou analisadores de pátio de manobras que podem aceitar entradas ilegais - este não precisa ser personalizado e (a menos que haja um bug) não aceitará entradas incorretas. Em certo sentido, não está completo, foi escrito para testar o algoritmo e sua entrada está em uma forma que precisará de algum pré-processamento, mas há comentários que deixam isso claro.

Observe que alguns tipos comuns de operadores estão faltando, por exemplo, o tipo de operador usado para indexar, ou seja, tabela [índice] ou chamar uma função de função (expressão-parâmetro, ...). Vou adicioná-los, mas pense em ambos como pós-fixados operadores onde o que vem entre os delimitadores '[' e ']' ou '(' e ')' é analisado com uma instância diferente do analisador de expressão. Desculpe ter deixado isso de fora, mas a parte pós-fixada está - adicionar o resto provavelmente quase dobrará o tamanho do código.

Como o analisador tem apenas 100 linhas de código de raquete, talvez eu deva apenas colá-lo aqui, espero que não seja mais longo do que o stackoverflow permite.

Alguns detalhes sobre decisões arbitrárias:

Se um operador de pós-fixo de baixa precedência estiver competindo pelos mesmos blocos de infixo que um operador de prefixo de baixa precedência, o operador de prefixo vence. Isso não aparece na maioria das linguagens, pois a maioria não tem operadores Postfix de baixa precedência. - por exemplo: ((dados a) (esquerda 1 +) (pré 2 não) (dados b) (pós 3!) (esquerda 1 +) (dados c)) é a + não b! + c onde não é a operador de prefixo e! é um operador pós-fixado e ambos têm precedência menor do que +, então eles querem agrupar de maneiras incompatíveis como (a + não b!) + c ou como a + (não b! + c) nestes casos, o operador prefixo sempre vence, então o segundo é a maneira como ele analisa

Operadores de infixo não associativos estão realmente lá para que você não tenha que fingir que os operadores que retornam tipos diferentes dos que assumem fazem sentido juntos, mas sem ter diferentes tipos de expressão para cada um, é uma confusão. Como tal, neste algoritmo, os operadores não associativos se recusam a se associar não apenas a eles próprios, mas a qualquer operador com a mesma precedência. Esse é um caso comum, pois <<= ==> = etc não se associam na maioria dos idiomas.

A questão de como diferentes tipos de operadores (esquerda, prefixo etc.) quebram laços na precedência é algo que não deveria surgir, porque realmente não faz sentido dar a operadores de tipos diferentes a mesma precedência. Esse algoritmo faz algo nesses casos, mas nem estou me preocupando em descobrir exatamente o que, porque tal gramática é uma má ideia em primeiro lugar.

#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

Aqui está uma solução recursiva de caso simples escrita em Java. Observe que não lida com números negativos, mas você pode adicionar se quiser:

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

O algoritmo pode ser facilmente codificado em C como analisador descendente recursivo.

#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;
}

próximas libs podem ser úteis: yupana - operações estritamente aritméticas; tinyexpr - operações aritméticas + funções matemáticas C + uma fornecida pelo usuário; mpc - combinadores de analisador

Explicação

Vamos capturar a sequência de símbolos que representam a expressão algébrica. O primeiro é um número, ou seja, um dígito decimal repetido uma ou mais vezes. Iremos nos referir a essa notação como regra de produção.

number -> [0..9]+

O operador de adição com seus operandos é outra regra. É um numberou quaisquer símbolos que representam a sum "*" sumsequência.

sum -> number | sum "+" sum

Tente substituir numberem sum "+" sumque será number "+" numberque por sua vez pode ser expandido para [0..9]+ "+" [0..9]+que finalmente possa ser reduzido a 1+8que é a expressão de adição correta.

Outras substituições também produzirão a expressão correta: sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

Pouco a pouco, podemos nos assemelhar a um conjunto de regras de produção, também conhecidas como gramática, que expressam todas as expressões algébricas possíveis.

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

Para controlar a precedência do operador, altere a posição de sua regra de produção em relação a outras. Observe a gramática acima e observe que a regra de produção para *está colocada abaixo, +isso forçará a productavaliação antes sum. A implementação apenas combina o reconhecimento de padrões com a avaliação e, portanto, reflete de perto as regras de produção.

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);
}

Aqui nós avaliamos termprimeiro e retornamos se não houver nenhum *caractere depois dele, isso é escolha à esquerda em nossa regra de produção, caso contrário - avalie os símbolos depois e retorne term.value * product.value esta é a escolha certa em nossa regra de produção, ou sejaterm "*" product

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.