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


104

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

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

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

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

명확성을 위해 편집 :

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

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

문법:

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

언어:

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

코드 예

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

관련 질문

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

-아담


나는 writen 한 C #에서 표현 파서를 내 블로그에. shunting yard 알고리즘에서 스택없이 postfix에 infix를합니다. 배열 만 사용합니다.
Guge

내가 이해했듯이 산술 표현식 만 구문 분석해야합니다. 역 폴란드어 표기법
mishadoff

답변:


69

어려운 방법

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

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

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

9
내 요점을 강조하기 위해 내 게시물의 마크 업이 올바르게 구문 분석되지 않는다는 점에 유의하십시오 (정적으로 렌더링 된 마크 업과 WMD 미리보기에서 렌더링 된 마크 업간에 다릅니다). 그것을 고치려는 시도가 여러 번 있었지만 파서가 잘못되었다고 생각합니다. 모두에게 호의를 베풀고 올바르게 구문 분석하십시오!
Jared Updike

155

입환 야드 알고리즘 이 적합한 도구입니다. 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을 얻을 수 있습니다.

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


6
맨 위를 터뜨리지 않고 스택에서 두 번째 것을 볼 수 있다면 실제로 두 개의 스택이 필요하지 않습니다. 대신 숫자와 연산자를 번갈아 사용하는 단일 스택을 사용할 수 있습니다. 사실 이것은 LR 파서 생성기 (예 : 들소)가하는 일과 정확히 일치합니다.
Chris Dodd

2
방금 구현 한 알고리즘에 대한 정말 멋진 설명입니다. 또한 당신은 또한 좋은 접미사로 변환하지 않습니다. 괄호에 대한 지원을 추가하는 것도 매우 쉽습니다.
Giorgi

4
shunting-yard 알고리즘의 단순화 된 버전은 여기에서 찾을 수 있습니다. andreinc.net/2010/10/05/…(Java 및 Python으로 구현 됨)
Andrei Ciobanu

1
이것에 대해 감사합니다. 정확히 내가 추구하는 것입니다!
Joe Green

왼쪽-연관성에 대해 언급 해 주셔서 감사합니다. 삼항 연산자 : 중첩 된 "? :"로 복잡한 표현식을 구문 분석하는 방법을 고수했습니다. 나는 둘 다 '?' 및 ':'는 동일한 우선 순위를 가져야합니다. 그리고 '?'를 해석하면 오른쪽-연관성 및 ':'왼쪽-연관성이 알고리즘은 그들과 매우 잘 작동합니다. 또한 두 연산자가 모두 남아있는 경우에만 두 연산자를 축소 할 수 있습니다.
Vladislav

25

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

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

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

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

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


링크가 끊어진 것 같습니다. 더 나은 대답은 각 방법을 의역하여 해당 링크가 사라 졌을 때 유용한 정보 중 일부가 여기에 보존되도록하는 것이었을 것입니다.
아담 화이트

18

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


16

오래 전에 나는 파싱에 관한 책 (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;
    }    
}

11

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

편집하다:

문법 문제를 이해하지 못하고 '당신이 직접 작성했다'는 사실은 '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>)

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


8

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

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

1
+1 결론은 모든 것이 Boost의 일부라는 것입니다. 계산기의 문법은 다음과 같습니다 : spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . 계산기의 구현은 다음과 같습니다 : spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . 문서는 여기에 있습니다 : spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . 사람들이 왜 여전히 미니 파서를 구현하는지 이해하지 못할 것입니다.
stephan 2009-06-20

5

질문을 할 때 재귀가 필요하지 않습니다. 답은 세 가지입니다. Postfix 표기법, Shunting Yard 알고리즘 및 Postfix 표현식 평가 :

1). 후위 표기법 = 명시적인 우선 순위 지정이 필요하지 않도록 고안되었습니다. 인터넷에서 더 많은 것을 읽으십시오. 그러나 여기에 그것의 요점이 있습니다 : infix expression (1 + 2) * 3 인간이 읽고 처리하기 쉽지만 기계를 통한 컴퓨팅에는 그리 효율적이지 않습니다. 뭐가? "우선적으로 캐싱하여 표현식을 다시 작성하고 항상 왼쪽에서 오른쪽으로 처리"라는 간단한 규칙입니다. 따라서 infix (1 + 2) * 3은 postfix 12 + 3 *이됩니다. 연산자는 항상 피연산자 뒤에 위치하므로 POST입니다.

2). 접미사 표현 평가. 쉬운. 접미사 문자열에서 숫자를 읽습니다. 작업자가 보일 때까지 스택에 밀어 넣으십시오. 연산자 유형 확인-단항? 바이너리? 제삼기? 이 연산자를 평가하는 데 필요한만큼 스택에서 피연산자를 팝합니다. 평가하십시오. 결과를 다시 스택에 푸시하십시오! 그리고 거의 끝났습니다. 스택에 항목이 하나만있을 때까지 계속 그렇게합니다.

후위에있는 (1 + 2) * 3은 "12 + 3 *"입니다. 첫 번째 숫자 읽기 = 1. 스택에 밀어 넣습니다. 다음을 읽으십시오. 번호 = 2. 스택에 밀어 넣습니다. 다음을 읽으십시오. 운영자. 어느 것? +. 어떤 종류? Binary = 두 개의 피연산자가 필요합니다. 스택 두 번 팝 = argright는 2이고 argleft는 1입니다. 1 + 2는 3입니다. 3을 스택에 다시 밀어 넣습니다. 접미사 문자열에서 다음을 읽습니다. 그 숫자. 3. 푸시. 다음을 읽으십시오. 운영자. 어느 것? *. 어떤 종류? 바이너리 = 두 개의 숫자가 필요-> 두 번 스택. 먼저 argright로, 두 번째로 argleft로 팝합니다. 작업 평가-3 x 3은 9입니다. 스택에 9를 누릅니다. 다음 접미사 문자를 읽으십시오. null입니다. 입력 끝. 팝 스택 onec = 그것이 당신의 대답입니다.

삼). Shunting Yard는 인간 (쉽게) 읽을 수있는 중위 표현을 접미 표현으로 변환하는 데 사용됩니다 (일부 연습 후에 인간도 쉽게 읽을 수 있음). 수동으로 코딩하기 쉽습니다. 위의 주석과 net을 참조하십시오.


4

사용하고 싶은 언어가 있습니까? ANTLR 을 사용하면 Java 관점에서이를 수행 할 수 있습니다. Adrian Kuhn은 Ruby로 실행 가능한 문법을 ​​작성하는 방법에 대한 훌륭한 을 가지고 있습니다 . 사실, 그의 예는 거의 정확하게 당신의 산술 표현 예입니다.


블로그 게시물에 제공된 내 예제가 왼쪽 재귀가 잘못되었음을 인정해야합니다. 즉, a-b-c는 ((a -b)-c) 대신 (a-(b -c))로 평가됩니다. 사실, 그것은 내가 블로그 게시물을 수정해야한다는 할 일을 추가하는 것을 생각 나게합니다.
akuhn

4

그것은 당신이 원하는 "일반적인"정도에 달려 있습니다.

sin (4 + 5) * cos (7 ^ 3)처럼 수학 함수를 구문 분석 할 수있는 것과 같이 정말 일반적으로 사용하려면 구문 분석 트리 가 필요할 것입니다 .

여기서 완전한 구현이 여기에 붙여 넣는 것이 적절하다고 생각하지 않습니다. 악명 높은 " 드래곤 북 " 중 하나를 확인하는 것이 좋습니다 .

그러나 우선 순위 지원을 원하면 먼저 표현식을 후위 형식으로 변환하여 복사하여 붙여 넣을 수있는 알고리즘을 Google 에서 사용할 수 있어야 하거나 바이너리로 직접 코딩 할 수 있다고 생각합니다. 나무.

접미사 형태로 있으면 스택이 어떻게 도움이되는지 이미 이해하고 있기 때문에 그때부터 케이크 조각입니다.


드래곤 북은 표현식 평가자에게는 약간 과도 할 수 있습니다. 간단한 재귀 하강 파서 만 있으면되지만 컴파일러에서 더 광범위한 작업을 수행하려면 반드시 읽어야합니다.
Eclipse

1
와우- "드래곤 북"이 아직 논의되고 있다는 사실이 반갑습니다. 30 년 전 대학에서 공부하고 끝까지 읽었던 기억이납니다.
Schroedingers 고양이

4

나는 Shunting Yard 알고리즘을 속이고 사용하는 것이 좋습니다. . 간단한 계산기 유형 파서를 작성하는 쉬운 방법이며 우선 순위를 고려합니다.

사물을 적절하게 토큰 화하고 변수 등을 포함하려면 여기에 다른 사람들이 제안한대로 재귀 하강 파서를 작성합니다. 그러나 단순히 계산기 스타일 파서를 필요로한다면이 알고리즘이면 충분합니다 :-)


4

Shunting Yard 알고리즘 에 대한 PIClist에서 이것을 발견했습니다 .

Harold는 다음과 같이 썼습니다.

오래 전에 쉽게 평가할 수 있도록 대수식을 RPN으로 변환 한 알고리즘을 읽었던 기억이 있습니다. 각 중위 값 또는 연산자 또는 괄호는 트랙의 철도 차량으로 표시되었습니다. 한 유형의 자동차는 다른 트랙으로 분리되고 다른 유형은 계속 직진했습니다. 세부 사항은 기억 나지 않지만 (분명히!), 항상 코딩하는 것이 흥미로울 것이라고 생각했습니다. 이것은 6800 (68000이 아님) 어셈블리 코드를 작성했을 때 돌아 왔습니다.

이것은 "shunting yard algorythm"이며 대부분의 기계 파서가 사용하는 것입니다. Wikipedia의 구문 분석에 대한 기사를 참조하십시오. 션팅 야드 알고리즘을 코딩하는 쉬운 방법은 두 개의 스택을 사용하는 것입니다. 하나는 "push"스택이고 다른 하나는 "reduce"또는 "result"스택입니다. 예:

pstack = () // 비어 있음 rstack = () 입력 : 1 + 2 * 3 priority = 10 // 최저 감소 = 0 // 감소하지 않음

시작 : 토큰 '1': isnumber, pstack (푸시) 토큰 '+': isoperator set priority = 2 if priority <previous_operator_precedence then reduce () // 아래 참조 pstack (push) 토큰 '2'에 '+'삽입 : isnumber, pstack (push) 토큰에 넣어 '*': isoperator, 우선 순위 = 1 설정, pstack에 넣어 (push) // 우선 순위를 // 위 토큰 '3'으로 확인 : isnumber, pstack (push) 끝 입력, 축소해야 함 (목표는 비어 있음 pstack) reduce () // 완료

줄이기 위해 푸시 스택에서 요소를 팝하고 결과 스택에 넣습니다. 'operator' 'number'형식 인 경우 항상 pstack의 상위 2 개 항목을 교체합니다.

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

식이 다음과 같으면

1 * 2 + 3

그러면 축소 트리거는 이미 푸시 된 '*'보다 우선 순위가 낮은 토큰 '+'를 읽었을 것이므로 완료되었을 것입니다.

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

그런 다음 '+'를 누른 다음 '3'을 누른 다음 마지막으로 줄입니다.

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

따라서 짧은 버전은 다음과 같습니다. 푸시 번호, 푸시 연산자는 이전 연산자의 우선 순위를 확인합니다. 지금 밀어 넣을 작업자보다 높으면 먼저 줄인 다음 현재 작업자를 누릅니다. 괄호를 처리하려면 단순히 '이전'연산자의 우선 순위를 저장하고 pstack에 표시를하여 괄호 쌍의 내부를 해결할 때 감소 알고리즘이 감소를 중지하도록 지시합니다. 닫는 괄호는 입력이 끝날 때처럼 축소를 트리거하고 pstack에서 열린 괄호 표시를 제거하고 '이전 작업'우선 순위를 복원하여 종료 된 괄호를 닫은 후 구문 분석을 계속할 수 있습니다. 이것은 재귀를 사용하거나 사용하지 않고 수행 할 수 있습니다 (힌트 : '('...)를 만날 때 이전 우선 순위를 저장하려면 스택을 사용합니다. 이것의 일반화 된 버전은 shunting yard algorythm, f.ex를 구현 한 파서 생성기를 사용하는 것입니다. yacc 또는 bison 또는 taccle (yacc의 tcl 유사체) 사용.

베드로

-아담


4

우선 순위 구문 분석을위한 또 다른 리소스는 Wikipedia 의 연산자 우선 순위 구문 분석기 항목입니다. Dijkstra의 shunting yard 알고리즘과 트리 대체 알고리즘을 다룹니다. 그러나 특히 우선 순위를 모르는 파서 앞에서 사소하게 구현할 수있는 정말 간단한 매크로 대체 알고리즘을 다룹니다.

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

다음과 같이 호출하십시오.

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

단순함이 굉장하고 이해하기 쉽습니다.


3
그것은 아주 멋진 작은 진주입니다. 그러나 그것을 확장하면 (예를 들어, 함수 적용, 암시 적 곱셈, 접두사 및 접미사 연산자, 선택적 유형 주석 등) 모든 것이 깨질 것입니다. 즉, 우아한 해킹입니다.
Jared Updike

나는 요점을 보지 못한다. 이 모든 작업은 연산자 우선 순위 구문 분석 문제를 괄호 우선 순위 구문 분석 문제로 변경하는 것입니다.
론의 후작

@EJP는 확실하지만 질문의 파서는 괄호를 잘 처리하므로 합리적인 해결책입니다. 하지만 그렇지 않은 파서가있는 경우 문제가 다른 영역으로 이동하는 것이 맞습니다.
아담 데이비스

4

내 웹 사이트에 초소형 (1 클래스, <10 KiB) Java Math Evaluator의 소스를 게시했습니다 . 이것은 수락 된 답변의 포스터에 대한 두개골 폭발을 일으킨 유형의 재귀 하강 파서입니다.

전체 우선 순위, 괄호, 명명 된 변수 및 단일 인수 함수를 지원합니다.




2

저는 현재 디자인 패턴과 읽기 쉬운 프로그래밍을위한 학습 도구로 정규식 파서를 작성하는 일련의 기사를 작업 중입니다. 읽을 수있는 코드를 살펴볼 수 있습니다 . 이 기사는 shunting yards 알고리즘의 명확한 사용을 제시합니다.


2

F #으로 식 파서를 작성하고 여기에 블로그를 게시했습니다 . 션팅 야드 알고리즘을 사용하지만 중위에서 RPN으로 변환하는 대신 두 번째 스택을 추가하여 계산 결과를 축적했습니다. 연산자 우선 순위를 올바르게 처리하지만 단항 연산자는 지원하지 않습니다. 나는 식 파싱을 배우기 위해서가 아니라 F #을 배우기 위해 이것을 썼다.


2

pyparsing을 사용하는 Python 솔루션은 여기 에서 찾을 수 있습니다 . 우선 순위가있는 다양한 연산자로 중위 표기법을 구문 분석하는 것은 매우 일반적이므로 pyparsing에는 infixNotation(이전의 operatorPrecedence) 표현식 작성 기도 포함됩니다 . 예를 들어 "AND", "OR", "NOT"등을 사용하여 부울 표현식을 쉽게 정의 할 수 있습니다. 또는!와 같은 다른 연산자를 사용하도록 네 가지 함수 산술을 확장 할 수 있습니다. 계승의 경우 또는 모듈러스의 경우 '%', 순열 및 조합을 계산하려면 P 및 C 연산자를 추가합니다. '-1'또는 'T'연산자 (반전 및 전치) 처리를 포함하는 행렬 표기법을위한 중위 파서를 작성할 수 있습니다. 4 함수 파서의 operatorPrecedence 예제 ( '!'포함).


1

나는 이것이 늦은 대답이라는 것을 알고 있지만 모든 연산자 (접두사, 접미사 및 중위 왼쪽, 중위 오른쪽 및 비 연관)가 임의의 우선 순위를 갖도록 허용하는 작은 파서를 작성했습니다.

임의의 DSL을 지원하는 언어에 대해 이것을 확장 할 예정이지만 연산자 우선 순위를 위해 사용자 지정 파서가 필요하지 않고 테이블이 전혀 필요하지 않은 일반화 된 파서를 사용할 수 있다는 점을 지적하고 싶었습니다. 표시되는대로 각 연산자의 우선 순위를 찾습니다. 사람들은 불법 입력을 허용 할 수있는 맞춤형 Pratt 파서 또는 shunting yard 파서를 언급했습니다. 이것은 사용자 정의 할 필요가 없으며 (버그가없는 한) 잘못된 입력을 받아들이지 않습니다. 어떤 의미에서는 완전하지 않으며 알고리즘을 테스트하기 위해 작성되었으며 입력은 일부 전처리가 필요한 형식이지만이를 명확히하는 주석이 있습니다.

예를 들어 인덱싱에 사용되는 연산자 (예 : table [index] 또는 함수 함수 호출 (parameter-expression, ...))에 사용되는 연산자의 일부 일반적인 종류가 누락되었습니다. 두 가지를 모두 추가 할 것입니다. 구분 기호 '['와 ']'또는 '('와 ')'사이에있는 연산자는 식 파서의 다른 인스턴스로 구문 분석됩니다. 생략해서 미안하지만 접미사 부분이 있습니다. 나머지를 추가하면 코드 크기가 거의 두 배가 될 것입니다.

파서는 단지 100 줄의 라켓 코드이므로 여기에 붙여 넣어야합니다. 이것이 stackoverflow가 허용하는 것보다 길지 않기를 바랍니다.

임의 결정에 대한 몇 가지 세부 사항 :

낮은 우선 순위 접두사 연산자가 낮은 우선 순위 접두사 연산자와 동일한 중위 블록에 대해 경쟁하는 경우 접두사 연산자가 이깁니다. 대부분의 언어에는 우선 순위가 낮은 접미사 연산자가 없기 때문에 이것은 대부분의 언어에서 나오지 않습니다. -예 : ((data a) (left 1 +) (pre 2 not) (data b) (post 3!) (left 1 +) (data c)) is a + not b! + c where not is a 접두사 연산자 및! 후위 연산자이고 둘 다 +보다 우선 순위가 낮으므로 (a + not b!) + c 또는 a + (not b! + c)로 호환되지 않는 방식으로 그룹화하려고합니다.이 경우 접두사 연산자가 항상이기므로 두 번째는 파싱 방식입니다.

비 연관 중위 연산자는 실제로 존재하므로 서로 다른 유형을 반환하는 연산자가 합리적이라고 생각할 필요가 없지만 각각에 대해 다른 표현식 유형을 갖지 않는 것은 엉터리입니다. 따라서이 알고리즘에서 비 연관 연산자는 자신뿐만 아니라 동일한 우선 순위를 가진 연산자와의 연관을 거부합니다. <<= ==> = 등은 대부분의 언어에서 서로 연관되지 않는 일반적인 경우입니다.

서로 다른 유형의 연산자 (왼쪽, 접두사 등)가 우선 순위에서 연결을 끊는 방법에 대한 질문은 다른 유형의 연산자에 동일한 우선 순위를 부여하는 것이 실제로 의미가 없기 때문에 발생해서는 안되는 문제입니다. 이 알고리즘은 이러한 경우에 뭔가를 수행하지만, 그러한 문법은 애초에 나쁜 생각이기 때문에 정확히 무엇을 알아내는 것도 신경 쓰지 않습니다.

#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

다음은 Java로 작성된 간단한 케이스 재귀 솔루션입니다. 음수를 처리하지 않지만 원하는 경우 추가 할 수 있습니다.

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

알고리즘은 C에서 재귀 하강 파서로 쉽게 인코딩 할 수 있습니다.

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

다음 libs가 유용 할 수 있습니다 : yupana- 엄격하게 산술 연산; tinyexpr- 산술 연산 + C 수학 함수 + 사용자가 제공 한 함수; mpc- 파서 결합 자

설명

대수적 표현을 나타내는 일련의 기호를 캡처 해 보겠습니다. 첫 번째는 숫자, 즉 한 번 이상 반복되는 십진수입니다. 이러한 표기법을 생산 규칙이라고합니다.

number -> [0..9]+

피연산자가있는 더하기 연산자는 또 다른 규칙입니다. 그것은 하나 인 number또는 임의 나타내는 심볼 sum "*" sum시퀀스.

sum -> number | sum "+" sum

시도 대용품 number으로 sum "+" sum그 것 number "+" number차례로 확장 될 수있는 [0..9]+ "+" [0..9]+감소 될 수 마침내 1+8올바른 또한 표현이다.

다른 대체도 올바른 표현을 생성합니다 : sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

조금씩 우리는 가능한 모든 대수적 표현을 표현하는 일련의 생산 규칙, 즉 문법 과 비슷할 수 있습니다.

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

운영자 우선 순위를 제어하려면 생산 규칙의 위치를 ​​다른 사람에 비해 변경하십시오. 위의 문법을보고에 대한 생산 규칙 이이 *아래에 배치 +되면 product이전에 강제로 평가 sum됩니다. 구현은 패턴 인식과 평가를 결합하기 때문에 생산 규칙을 ​​거의 반영합니다.

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

여기서 우리는 term먼저 평가 하고 그 *뒤에 문자 가 없으면 반환합니다. 그렇지 않으면 생산 규칙에서 왼쪽 선택입니다. 기호를 평가하고 반환합니다. term.value * product.value 이것은 우리 생산 규칙에서 오른쪽 선택입니다.term "*" product

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.