[algorithm] 우선 순위가있는 방정식 (표현식) 파서?

이진 (+,-, |, &, *, / 등) 연산자, 단항 (!) 연산자 및 괄호를 처리하는 간단한 스택 알고리즘을 사용하여 방정식 파서를 개발했습니다.

그러나이 방법을 사용하면 모든 것이 동일한 우선 순위를 갖게됩니다. 괄호를 사용하여 우선 순위를 적용 할 수 있지만 연산자에 관계없이 왼쪽에서 오른쪽으로 평가됩니다.

지금 당장 “1 + 11 * 5″는 예상대로 56이 아니라 60을 반환합니다.

이것은 현재 프로젝트에 적합하지만 이후 프로젝트에 사용할 수있는 범용 루틴을 갖고 싶습니다.

명확성을 위해 편집 :

우선 순위로 방정식을 구문 분석하는 데 좋은 알고리즘은 무엇입니까?

구현하기 쉬운 것에 관심이 있으며 사용 가능한 코드에 대한 라이선스 문제를 피하기 위해 직접 코딩 할 수 있다는 것을 이해하고 있습니다.

문법:

문법 문제를 이해하지 못합니다. 손으로 썼습니다. YACC 또는 Bison이 필요하지 않을 정도로 간단합니다. “2 + 3 * (42/13)”과 같은 방정식으로 문자열을 계산하기 만하면됩니다.

언어:

C로이 작업을 수행하고 있지만 언어 별 솔루션이 아닌 알고리즘에 관심이 있습니다. C는 필요한 경우 다른 언어로 쉽게 변환 할 수있을 정도로 낮은 수준입니다.

코드 예

위에서 언급 한 간단한 표현식 파서테스트 코드를 게시했습니다 . 프로젝트 요구 사항이 변경 되었기 때문에 프로젝트에 통합되지 않았기 때문에 성능이나 공간을 위해 코드를 최적화 할 필요가 없었습니다. 그것은 원래의 장황한 형태이며 쉽게 이해할 수 있어야합니다. 운영자 우선 순위 측면에서 추가 작업을 수행 하면 나머지 프로그램과 간단하게 일치하기 때문에 매크로 해킹을 선택할 것입니다 . 그래도 실제 프로젝트에서 이것을 사용한다면 좀 더 간결하고 빠른 파서를 사용할 것입니다.

관련 질문

수학 파서의 스마트 디자인?

-아담



답변

어려운 방법

재귀 하강 파서를 원합니다. .

우선 순위를 얻으려면 예를 들어 샘플 문자열을 사용하여 재귀 적으로 생각해야합니다.

1+11*5

이 작업을 수동으로 수행하려면을 읽고 1플러스를 확인하고 …로 시작하는 완전히 새로운 재귀 구문 분석 “세션”을 시작해야합니다 11. 그리고를 11 * 5자체 요소로 구문 분석하여 다음과 같은 구문 분석 트리를 생성해야합니다.1 + (11 * 5) .

특히 C의 무력 함이 추가되어 설명을 시도하는 것조차도이 모든 것이 너무 고통 스럽습니다. 11을 파싱 한 후 *가 실제로 +이면 용어를 만드는 시도를 포기하고 대신 파싱해야합니다. 11 그 자체가 요인입니다. 내 머리가 벌써 터져 요. 재귀 적 괜찮은 전략으로 가능하지만 더 좋은 방법이 있습니다 …

쉬운 (올바른) 방법

Bison과 같은 GPL 도구를 사용하는 경우 bison에 의해 생성 된 C 코드가 GPL에 포함되지 않기 때문에 라이선스 문제에 대해 걱정할 필요가 없습니다 (IANAL하지만 GPL 도구가 GPL을 강제로 실행하지 않는다고 확신합니다. 생성 된 코드 / 바이너리; 예를 들어 Apple은 GCC로 Aperture와 같은 코드를 컴파일하고 GPL에서 언급 한 코드없이 판매합니다.)

들소 다운로드 (또는 이와 동등한 것, ANTLR 등)을 다운로드하십시오.

일반적으로 bison을 실행하고이 네 가지 함수 계산기를 보여주는 원하는 C 코드를 얻을 수있는 몇 가지 샘플 코드가 있습니다.

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

생성 된 코드를보고 이것이 들리는 것처럼 쉽지 않은지 확인하십시오. 또한 Bison과 같은 도구를 사용할 때의 장점은 1) 무언가를 배우고 (특히 Dragon 책을 읽고 문법에 대해 배우는 경우) 2) NIH 가 바퀴를 재발 명 하지 않도록 하는 것입니다. 실제 파서 생성기 도구를 사용하면 실제로 나중에 확장하여 파서가 파싱 도구의 도메인임을 아는 다른 사람들에게 보여줄 수 있습니다.


최신 정보:

여기 사람들은 많은 건전한 조언을 제공했습니다. 구문 분석 도구를 건너 뛰거나 Shunting Yard 알고리즘 또는 손으로 굴린 재귀 적 괜찮은 파서를 사용하는 것에 대한 유일한 경고는 작은 장난감 언어 1 이 언젠가 함수 (sin, cos, log) 및 변수, 조건 및 루프.

Flex / Bison은 작고 단순한 인터프리터에게는 너무 과도 할 수 있지만, 파서 + 평가자는 변경이 필요하거나 기능을 추가해야 할 때 문제를 일으킬 수 있습니다. 귀하의 상황은 다양하며 귀하의 판단을 사용해야합니다. 당신의 죄로 다른 사람을 벌 하지 말고 [2] 적절하지 않은 도구를 만드십시오.

내가 가장 좋아하는 구문 분석 도구

이 작업을위한 세계 최고의 도구 는 프로그래밍 언어 Haskell과 함께 제공되는 Parsec 라이브러리 (재귀 적 괜찮은 파서 용)입니다. 이처럼 많이 보이는 BNF , 또는 구문 분석에 대한 몇 가지 특별한 도구 나 도메인 특정 언어와 같은 (샘플 코드 [3]), 그러나 그것은 나머지와 같은 빌드 단계에서 컴파일 것을 의미 사실 하스켈 단지 일반 라이브러리 Haskell 코드의 일부를 사용하고 임의의 Haskell 코드를 작성하고 파서 내에서 호출 할 수 있으며 동일한 코드에서 다른 라이브러리를 모두 혼합하고 일치시킬 수 있습니다 . (하스켈이 아닌 다른 언어에 이와 같은 구문 분석 언어를 포함 시키면 많은 구문이 복잡해집니다. 저는 C #에서이 작업을 수행했으며 꽤 잘 작동하지만 그렇게 예쁘고 간결하지는 않습니다.)

노트:

1 Richard Stallman은 Tcl을 사용해서는 안되는 이유 에서 말합니다.

Emacs의 주요 교훈은 확장을위한 언어가 단순한 “확장 언어”가 아니어야한다는 것입니다. 실질적인 프로그램을 작성하고 유지하기 위해 설계된 실제 프로그래밍 언어 여야합니다. 사람들이 그렇게하고 싶기 때문입니다!

[2] 예, 저는 그 “언어”를 사용함으로써 영원히 상처를 입습니다.

또한이 항목을 제출했을 때 미리보기가 정확했지만 SO는 첫 번째 단락에서 가까운 앵커 태그를 파서가 적절 하지 않아서 파서가 사소한 것이 아니라는 것을 증명 합니다. 아마도 미묘하고 작은 오류가 발생할 것 입니다.

[3] Parsec을 사용하는 하스켈 파서 조각 : 지수, 괄호, 곱셈을위한 공백, 상수 (예 : pi 및 e)로 확장 된 4 개의 함수 계산기.

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


답변

입환 야드 알고리즘 이 적합한 도구입니다. Wikipedia는 이것에 대해 정말 혼란 스럽지만 기본적으로 알고리즘은 다음과 같이 작동합니다.

1 + 2 * 3 + 4를 평가하고 싶다고 가정 해 보겠습니다. 직관적으로 2 * 3을 먼저해야한다는 것을 “알고”있지만이 결과는 어떻게 얻습니까? 핵심은 문자열을 왼쪽에서 오른쪽으로 스캔 할 때 뒤에 오는 연산자가 더 낮은 (또는 같음) 우선 순위를 가질 때 연산자를 평가한다는 것을 인식하는 것 입니다. 예제의 컨텍스트에서 수행 할 작업은 다음과 같습니다.

  1. 보세요 : 1 + 2, 아무것도하지 마세요.
  2. 이제 1 + 2 * 3을보십시오. 여전히 아무것도하지 마십시오.
  3. 이제 1 + 2 * 3 + 4를 살펴보면 다음 연산자의 우선 순위가 더 낮기 때문에 2 * 3을 평가해야한다는 것을 알 수 있습니다.

이것을 어떻게 구현합니까?

하나는 숫자 용이고 다른 하나는 연산자 용입니다. 항상 스택에 숫자를 넣습니다. 각각의 새 연산자를 스택의 맨 위에있는 연산자와 비교합니다. 스택 맨 위에있는 연산자가 더 높은 우선 순위를 갖는 경우 연산자 스택에서 해당 연산자를 꺼내고, 숫자 스택에서 피연산자를 꺼내고, 연산자를 적용하고, 결과를 푸시합니다. 숫자 스택에. 이제 스택 상단 연산자로 비교를 반복합니다.

예제로 돌아 가면 다음과 같이 작동합니다.

N = [] 작업 = []

  • 1을 읽습니다. N = [1], Ops = []
  • +를 읽으십시오. N = [1], 작업 = [+]
  • 읽기 2. N = [1 2], Ops = [+]
  • 읽다 * . N = [1 2], 작업 = [+ *]
  • 3. N = [1 2 3], Ops = [+ *]
  • +를 읽으십시오. N = [12 3], 작업 = [+ *]
    • 3, 2를 팝하고 2 *3을 실행 하고 결과를 N에 푸시합니다. N = [1 6], Ops = [+]
    • +연관성이 있으므로 1, 6도 빼고 +를 실행하려고합니다. N = [7], Ops = [].
    • 마지막으로 [+]를 연산자 스택으로 밀어 넣습니다. N = [7], Ops = [+].
  • 4. N = [7 4]를 읽습니다. Ops = [+].
  • 입력이 부족하므로 지금 스택을 비우고 싶습니다. 결과 11을 얻을 수 있습니다.

그렇게 어렵지 않죠? 그리고 문법이나 파서 생성기를 호출하지 않습니다.


답변

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

다양한 접근 방식에 대한 아주 좋은 설명 :

  • 재귀 하강 인식
  • 션팅 야드 알고리즘
  • 고전적인 솔루션
  • 우선 등반

간단한 언어와 의사 코드로 작성되었습니다.

나는 ‘우선 등반’을 좋아합니다.


답변

여기 에 간단한 재귀 하강 파서를 연산자 우선 파싱과 결합하는 방법에 대한 멋진 기사가 있습니다 . 최근에 파서를 작성했다면 읽기가 매우 흥미롭고 유익 할 것입니다.


답변

오래 전에 나는 파싱에 관한 책 (Dragon Book과 같은)에서 찾을 수 없었던 나만의 파싱 알고리즘을 만들었습니다. Shunting Yard 알고리즘에 대한 포인터를 살펴보면 유사점을 알 수 있습니다.

약 2 년 전, http://www.perlmonks.org/?node_id=554516 에 Perl 소스 코드로 완성 된 글을 올렸습니다 . . 다른 언어로 이식하는 것은 쉽습니다. 제가 한 첫 번째 구현은 Z80 어셈블러였습니다.

숫자를 사용한 직접 계산에 이상적이지만 필요한 경우이를 사용하여 구문 분석 트리를 생성 할 수 있습니다.

최신 정보 더 많은 사람들이 Javascript를 읽거나 실행할 수 있기 때문에 코드가 재구성 된 후 Javascript에서 파서를 다시 구현했습니다. 전체 파서는 오류보고 및 주석을 포함하여 5k 미만의 Javascript 코드 (파서의 경우 약 100 줄, 래퍼 기능의 경우 15 줄) 미만입니다.

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


답변

현재 구문 분석에 사용중인 문법을 설명 할 수 있다면 도움이 될 것입니다. 문제가 거기에있을 것 같은데!

편집하다:

문법 문제를 이해하지 못하고 ‘당신이 직접 작성했다’는 사실은 ‘1 + 11 * 5’형식의 표현에 문제가있는 이유를 설명 할 가능성이 매우 높습니다 (즉, 연산자 우선 순위). . 예를 들어 ‘산술 식 문법’을 검색하면 좋은 포인터를 얻을 수 있습니다. 이러한 문법은 복잡 할 필요가 없습니다.

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

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

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

예를 들어 트릭을 수행하고 좀 더 복잡한 표현식 (예 : 함수 또는 힘 포함)을 처리하기 위해 사소하게 확장 될 수 있습니다.

난 당신이 한 번 봐 가지고 제안 예를 들어, 스레드를.

문법 / 파싱에 대한 거의 모든 소개는 산술 표현을 예로 취급합니다.

문법을 사용한다고해서 특정 도구 ( a la Yacc, Bison, …) 를 사용하는 것은 전혀 의미가 없습니다 . 실제로 이미 다음 문법을 사용하고 있습니다.

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

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

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

(또는 어떤 종류의) 그것을 모르고 있습니다!


답변

Boost Spirit 사용에 대해 생각해 보셨습니까 ? 다음과 같이 C ++로 EBNF와 유사한 문법을 ​​작성할 수 있습니다.

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