Bộ phân tích cú pháp phương trình (biểu thức) với mức độ ưu tiên?


104

Tôi đã phát triển một trình phân tích cú pháp phương trình bằng cách sử dụng một thuật toán ngăn xếp đơn giản sẽ xử lý các toán tử nhị phân (+, -, |, &, *, /, v.v.), toán tử một ngôi (!) Và dấu ngoặc đơn.

Tuy nhiên, sử dụng phương pháp này để lại cho tôi mọi thứ đều có cùng một mức độ ưu tiên - nó được đánh giá từ trái sang phải bất kể toán tử nào, mặc dù có thể thực thi quyền ưu tiên bằng cách sử dụng dấu ngoặc đơn.

Vì vậy, ngay bây giờ "1 + 11 * 5" trả về 60, không phải 56 như người ta có thể mong đợi.

Mặc dù điều này phù hợp với dự án hiện tại, tôi muốn có một thói quen cho mục đích chung mà tôi có thể sử dụng cho các dự án sau này.

Đã chỉnh sửa cho rõ ràng:

Thuật toán tốt để phân tích cú pháp các phương trình với mức độ ưu tiên là gì?

Tôi quan tâm đến điều gì đó đơn giản để triển khai và hiểu rằng tôi có thể tự viết mã để tránh các vấn đề cấp phép với mã có sẵn.

Ngữ pháp:

Tôi không hiểu câu hỏi ngữ pháp - Tôi đã viết nó bằng tay. Nó đủ đơn giản để tôi không thấy cần thiết phải có YACC hoặc Bison. Tôi chỉ cần tính toán các chuỗi với các phương trình như "2 + 3 * (42/13)".

Ngôn ngữ:

Tôi đang làm điều này bằng C, nhưng tôi quan tâm đến một thuật toán, không phải một giải pháp ngôn ngữ cụ thể. C là mức đủ thấp để có thể dễ dàng chuyển đổi sang ngôn ngữ khác nếu nhu cầu phát sinh.

Ví dụ về mã

Tôi đã đăng mã thử nghiệm cho trình phân tích cú pháp biểu thức đơn giản mà tôi đã nói ở trên. Các yêu cầu của dự án đã thay đổi và vì vậy tôi không bao giờ cần tối ưu hóa mã cho hiệu suất hoặc không gian vì nó không được tích hợp vào dự án. Nó ở dạng dài dòng nguyên bản và phải dễ hiểu. Nếu tôi làm bất cứ điều gì thêm với nó về mức độ ưu tiên của toán tử, có lẽ tôi sẽ chọn macro hack vì nó phù hợp với phần còn lại của chương trình một cách đơn giản. Tuy nhiên, nếu tôi từng sử dụng nó trong một dự án thực tế, tôi sẽ sử dụng một trình phân tích cú pháp nhỏ gọn / nhanh hơn.

Câu hỏi liên quan

Thiết kế thông minh của trình phân tích cú pháp toán học?

-Adam


Tôi đã viết một trình phân tích cú pháp biểu thức bằng C # trên blog của mình. Nó thực hiện infix để postfix mà không cần ngăn xếp trong thuật toán shunting yard. Nó chỉ sử dụng một mảng.
Guge

Như tôi hiểu, bạn chỉ cần phân tích cú pháp biểu thức số học. Sử dụng Xếp Polish Notation
mishadoff

Câu trả lời:


69

Con đường gian nan

Bạn muốn một trình phân tích cú pháp gốc đệ quy .

Để được ưu tiên, bạn cần phải suy nghĩ đệ quy, ví dụ: sử dụng chuỗi mẫu của bạn,

1+11*5

để thực hiện việc này theo cách thủ công, bạn sẽ phải đọc 1, sau đó xem dấu cộng và bắt đầu một "phiên" phân tích cú pháp đệ quy hoàn toàn mới bắt đầu bằng 11... và đảm bảo phân tích cú pháp 11 * 5thành nhân tố của chính nó, tạo ra một cây phân tích cú pháp với 1 + (11 * 5).

Tất cả điều này đều cảm thấy rất đau đớn ngay cả khi cố gắng giải thích, đặc biệt là với sự bất lực cộng thêm của C. Hãy xem, sau khi phân tích cú pháp 11, nếu * thực sự là dấu +, bạn sẽ phải bỏ nỗ lực tạo một thuật ngữ và thay vào đó phân tích cú pháp 11chính nó như một yếu tố. Đầu tôi đang nổ tung. Nó có thể với chiến lược đệ quy tốt, nhưng có một cách tốt hơn ...

Cách dễ dàng (đúng)

Nếu bạn sử dụng công cụ GPL như Bison, bạn có thể không cần lo lắng về các vấn đề cấp phép vì mã C do bison tạo ra không được GPL đề cập (IANAL nhưng tôi khá chắc chắn rằng các công cụ GPL không buộc GPL bật mã / mã nhị phân được tạo; ví dụ: Apple biên dịch mã như giả sử, Aperture với GCC và họ bán nó mà không cần phải mã GPL).

Tải xuống Bison (hoặc thứ gì đó tương đương, ANTLR, v.v.).

Thường có một số mã mẫu mà bạn có thể chỉ cần chạy bison và nhận mã C mong muốn của bạn để minh họa cho máy tính bốn chức năng này:

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

Nhìn vào đoạn mã đã tạo và thấy rằng điều này không dễ dàng như nó vẫn tưởng. Ngoài ra, lợi thế của việc sử dụng một công cụ như Bison là 1) bạn học được điều gì đó (đặc biệt nếu bạn đọc cuốn sách Dragon và tìm hiểu về ngữ pháp), 2) bạn tránh được NIH cố gắng phát minh lại bánh xe. Với một công cụ tạo trình phân tích cú pháp thực sự, bạn thực sự có hy vọng mở rộng quy mô sau này, cho những người khác biết rằng trình phân tích cú pháp là lĩnh vực của các công cụ phân tích cú pháp.


Cập nhật:

Mọi người ở đây đã đưa ra nhiều lời khuyên hữu ích. Cảnh báo duy nhất của tôi về việc bỏ qua các công cụ phân tích cú pháp hoặc chỉ sử dụng thuật toán Shunting Yard hoặc trình phân tích cú pháp đệ quy đệ quy cuộn bằng tay là một ngày nào đó các ngôn ngữ đồ chơi nhỏ 1 có thể biến thành các ngôn ngữ thực tế lớn với các hàm (sin, cos, log) và các biến, điều kiện và cho các vòng lặp.

Flex / Bison rất có thể là quá mức cần thiết đối với một trình thông dịch nhỏ, đơn giản, nhưng trình phân tích cú pháp + đánh giá một lần có thể gây ra sự cố khi cần thực hiện thay đổi hoặc cần thêm các tính năng. Tình huống của bạn sẽ khác nhau và bạn sẽ cần sử dụng phán đoán của mình; chỉ cần không trừng phạt người khác vì tội lỗi của bạn [2] và xây dựng một công cụ không đầy đủ.

Công cụ yêu thích của tôi để phân tích cú pháp

Công cụ tốt nhất trên thế giới cho công việc này là thư viện Parsec (dành cho trình phân tích cú pháp đệ quy tốt) đi kèm với ngôn ngữ lập trình Haskell. Nó trông rất giống BNF , hoặc giống một số công cụ chuyên dụng hoặc ngôn ngữ miền cụ thể để phân tích cú pháp (mã mẫu [3]), nhưng thực tế nó chỉ là một thư viện thông thường trong Haskell, có nghĩa là nó biên dịch trong cùng một bước xây dựng như phần còn lại. mã Haskell của bạn và bạn có thể viết mã Haskell tùy ý và gọi mã đó trong trình phân tích cú pháp của bạn, đồng thời bạn có thể trộn và kết hợp các thư viện khác trong cùng một mã . (Nhân tiện, việc nhúng một ngôn ngữ phân tích cú pháp như thế này bằng một ngôn ngữ không phải Haskell dẫn đến vô số lỗi cú pháp. Tôi đã làm điều này trong C # và nó hoạt động khá tốt nhưng nó không đẹp và ngắn gọn.)

Ghi chú:

1 Richard Stallman nói, trong Tại sao bạn không nên sử dụng Tcl

Bài học chính của Emacs là một ngôn ngữ cho phần mở rộng không nên là một "ngôn ngữ mở rộng". Nó phải là một ngôn ngữ lập trình thực, được thiết kế để viết và duy trì các chương trình quan trọng. Bởi vì mọi người sẽ muốn làm điều đó!

[2] Vâng, tôi vĩnh viễn bị sẹo khi sử dụng "ngôn ngữ" đó.

Cũng xin lưu ý rằng khi tôi gửi mục nhập này, bản xem trước là chính xác, nhưng trình phân tích cú pháp của SO chưa đầy đủ đã ăn thẻ neo đóng của tôi trên đoạn đầu tiên , chứng tỏ rằng trình phân tích cú pháp không phải là thứ gì đó đáng lo ngại vì nếu bạn sử dụng regexes và một lần nữa sẽ tấn công bạn có thể sẽ nhận được một cái gì đó nhỏ và sai nhỏ .

[3] Đoạn mã của trình phân tích cú pháp Haskell sử dụng Parsec: một máy tính bốn hàm được mở rộng với số mũ, dấu ngoặc đơn, khoảng trắng cho phép nhân và hằng số (như pi và 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
Để nhấn mạnh quan điểm của tôi, hãy lưu ý rằng đánh dấu trong bài đăng của tôi không được phân tích cú pháp chính xác (và điều này khác nhau giữa đánh dấu được hiển thị tĩnh và đánh dấu được hiển thị trong bản xem trước WMD). Đã có vài nỗ lực để sửa nó nhưng tôi nghĩ THÔNG SỐ LÀ SAI. Làm ơn cho mọi người và phân tích cú pháp đúng!
Jared Updike

155

Các thuật toán shunting sân là công cụ thích hợp cho việc này. Wikipedia thực sự khó hiểu về điều này, nhưng về cơ bản thuật toán hoạt động như thế này:

Giả sử, bạn muốn đánh giá 1 + 2 * 3 + 4. Theo trực giác, bạn "biết" bạn phải thực hiện 2 * 3 trước, nhưng làm thế nào để bạn có được kết quả này? Điều quan trọng là nhận ra rằng khi bạn đang quét chuỗi từ trái sang phải, bạn sẽ đánh giá một toán tử khi toán tử theo sau nó có mức độ ưu tiên thấp hơn (hoặc bằng). Trong ngữ cảnh của ví dụ, đây là những gì bạn muốn làm:

  1. Nhìn vào: 1 + 2, đừng làm gì cả.
  2. Bây giờ nhìn vào 1 + 2 * 3, vẫn không làm gì cả.
  3. Bây giờ nhìn vào 1 + 2 * 3 + 4, bây giờ bạn biết rằng 2 * 3 phải được đánh giá vì toán tử tiếp theo có mức độ ưu tiên thấp hơn.

Làm thế nào để bạn thực hiện điều này?

Bạn muốn có hai ngăn xếp, một ngăn xếp cho các số và một ngăn xếp cho các toán tử. Bạn đẩy các số lên ngăn xếp mọi lúc. Bạn so sánh từng toán tử mới với toán tử ở trên cùng của ngăn xếp, nếu toán tử ở trên cùng của ngăn xếp có mức độ ưu tiên cao hơn, bạn bật nó ra khỏi ngăn xếp toán tử, bật các toán hạng ra khỏi ngăn xếp số, áp dụng toán tử và đẩy kết quả vào ngăn xếp số. Bây giờ bạn lặp lại phép so sánh với toán tử đầu ngăn xếp.

Quay lại với ví dụ, nó hoạt động như sau:

N = [] Hoạt động = []

  • Đọc 1. N = [1], Ops = []
  • Đọc +. N = [1], Ops = [+]
  • Đọc 2. N = [1 2], Ops = [+]
  • Đọc *. N = [1 2], Ops = [+ *]
  • Đọc 3. N = [1 2 3], Ops = [+ *]
  • Đọc +. N = [1 2 3], Ops = [+ *]
    • Bật 3, 2 và thực hiện 2 *3, và đẩy kết quả lên N. N = [1 6], Ops = [+]
    • +là liên kết trái, vì vậy bạn cũng muốn bật 1, 6 và thực thi dấu +. N = [7], Ops = [].
    • Cuối cùng đẩy [+] vào ngăn xếp toán tử. N = [7], Ops = [+].
  • Đọc 4. N = [7 4]. Hoạt động = [+].
  • Bạn sắp hết đầu vào, vì vậy bạn muốn làm trống các ngăn xếp ngay bây giờ. Khi đó bạn sẽ nhận được kết quả 11.

Đó, điều đó không quá khó phải không? Và nó không thực hiện lời gọi nào đến bất kỳ trình tạo ngữ pháp hoặc trình phân tích cú pháp nào.


6
Bạn không thực sự cần hai ngăn xếp, miễn là bạn có thể nhìn thấy thứ thứ hai trên ngăn xếp mà không bị bật lên trên cùng. Thay vào đó, bạn có thể sử dụng một ngăn xếp duy nhất xen kẽ các số và toán tử. Thực tế, điều này tương ứng với chính xác những gì một trình tạo phân tích cú pháp LR (chẳng hạn như bò rừng) làm.
Chris Dodd

2
Giải thích thực sự tốt về thuật toán mà tôi vừa triển khai ngay bây giờ. Ngoài ra, bạn không chuyển đổi nó thành postfix cũng tốt. Thêm hỗ trợ cho dấu ngoặc đơn cũng rất dễ dàng.
Giorgi

4
Một phiên bản đơn giản hóa cho các thuật toán shunting sân có thể được tìm thấy ở đây: andreinc.net/2010/10/05/... (với hiện thực trong Java và python)
Andrei Ciobanu

1
Cảm ơn vì điều này, chính xác những gì tôi đang theo đuổi!
Joe Green

Cảm ơn rất nhiều vì đã đề cập đến trái - liên kết. Tôi bị mắc kẹt với toán tử bậc ba: làm thế nào để phân tích cú pháp các biểu thức phức tạp với "?:" Lồng nhau. Tôi nhận ra rằng cả hai '?' và ':' phải có cùng mức độ ưu tiên. Và nếu chúng ta diễn giải '?' như phải - liên kết và ':' là liên kết trái, thuật toán này hoạt động rất tốt với chúng. Ngoài ra, chúng ta chỉ có thể thu gọn 2 toán tử khi cả hai đều được kết hợp.
Vladislav

25

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

Giải thích rất tốt về các cách tiếp cận khác nhau:

  • Nhận dạng đệ quy gốc
  • Thuật toán sân shunting
  • Giải pháp cổ điển
  • Ưu tiên leo núi

Được viết bằng ngôn ngữ đơn giản và mã giả.

Tôi thích 'ưu tiên leo lên' một.


Liên kết dường như bị phá vỡ. Điều gì sẽ tạo ra một câu trả lời tốt hơn sẽ là diễn giải từng phương pháp để khi liên kết đó biến mất, một số thông tin hữu ích sẽ được lưu giữ ở đây.
Adam White

18

Có một bài viết hay ở đây về việc kết hợp trình phân tích cú pháp gốc đệ quy đơn giản với phân tích cú pháp ưu tiên toán tử. Nếu gần đây bạn đang viết trình phân tích cú pháp, thì nó sẽ rất thú vị và có tính hướng dẫn để đọc.


16

Cách đây rất lâu, tôi đã tạo ra thuật toán phân tích cú pháp của riêng mình, mà tôi không thể tìm thấy trong bất kỳ cuốn sách nào về phân tích cú pháp (như Sách Rồng). Nhìn vào các con trỏ đến thuật toán Shunting Yard, tôi thấy có sự tương đồng.

Khoảng 2 năm trước, tôi đã đăng một bài về nó, hoàn chỉnh bằng mã nguồn Perl, trên http://www.perlmonks.org/?node_id=554516 . Thật dễ dàng để chuyển sang các ngôn ngữ khác: lần triển khai đầu tiên tôi đã làm là trong trình hợp dịch Z80.

Nó lý tưởng để tính toán trực tiếp với các con số, nhưng bạn có thể sử dụng nó để tạo ra một cây phân tích cú pháp nếu cần.

Cập nhật Vì nhiều người hơn có thể đọc (hoặc chạy) Javascript, tôi đã hoàn thiện lại trình phân tích cú pháp của mình trong Javascript, sau khi mã đã được tổ chức lại. Toàn bộ trình phân tích cú pháp có dưới 5k mã Javascript (khoảng 100 dòng cho trình phân tích cú pháp, 15 dòng cho một hàm trình bao bọc) bao gồm báo cáo lỗi và nhận xét.

Bạn có thể tìm thấy bản demo trực tiếp tại 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

Sẽ hữu ích nếu bạn có thể mô tả ngữ pháp bạn hiện đang sử dụng để phân tích cú pháp. Có vẻ như vấn đề có thể nằm ở đó!

Biên tập:

Thực tế là bạn không hiểu câu hỏi ngữ pháp và rằng 'bạn đã viết cái này bằng tay' rất có thể giải thích tại sao bạn đang gặp vấn đề với các biểu thức có dạng '1 + 11 * 5' (nghĩa là với ưu tiên toán tử) . Ví dụ: Google cho 'ngữ pháp cho biểu thức số học' sẽ mang lại một số gợi ý tốt. Một ngữ pháp như vậy không cần phải phức tạp:

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

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

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

chẳng hạn sẽ thực hiện thủ thuật và có thể được tăng cường một cách đáng kể để xử lý một số biểu thức phức tạp hơn (bao gồm các hàm chẳng hạn, hoặc lũy thừa, ...).

Tôi khuyên bạn nên xem chủ đề này chẳng hạn.

Hầu hết tất cả các bài giới thiệu về ngữ pháp / phân tích cú pháp đều coi các biểu thức số học như một ví dụ.

Lưu ý rằng việc sử dụng một ngữ pháp hoàn toàn không ngụ ý sử dụng một công cụ cụ thể ( la Yacc, Bison, ...). Thật vậy, chắc chắn bạn đã sử dụng ngữ pháp sau:

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

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

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

(hoặc một cái gì đó của loại) mà không biết nó!


8

Bạn đã nghĩ đến việc sử dụng Boost Spirit chưa? Nó cho phép bạn viết các ngữ pháp giống EBNF trong C ++ như thế này:

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

1
+1 Và kết quả là, mọi thứ đều là một phần của Boost. Ngữ pháp cho máy tính có ở đây: Spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Việc triển khai máy tính là ở đây: Spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Và tài liệu ở đây: Spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Tôi sẽ không bao giờ hiểu tại sao mọi người vẫn triển khai ở đó các trình phân tích cú pháp mini riêng.
stephan

5

Như bạn đặt câu hỏi, không cần đệ quy gì cả. Câu trả lời là ba điều: Ký hiệu Postfix cộng với thuật toán Shunting Yard cộng với đánh giá biểu thức Postfix:

1). Ký hiệu Postfix = được phát minh để loại bỏ nhu cầu về đặc tả ưu tiên rõ ràng. Đọc thêm trên mạng nhưng đây là ý chính của nó: biểu thức infix (1 + 2) * 3 trong khi con người dễ đọc và xử lý không hiệu quả lắm đối với tính toán qua máy. Những gì là? Quy tắc đơn giản có nội dung "ưu tiên viết lại biểu thức bằng cách lưu vào bộ nhớ đệm, sau đó luôn xử lý từ trái sang phải". Vì vậy, infix (1 + 2) * 3 trở thành một hậu tố 12 + 3 *. POST vì toán tử luôn được đặt SAU các toán hạng.

2). Đánh giá biểu thức hậu tố. Dễ dàng. Đọc số khỏi chuỗi hậu tố. Đẩy chúng vào một ngăn xếp cho đến khi một toán tử được nhìn thấy. Kiểm tra loại toán tử - một ngôi? nhị phân? đại học? Bật nhiều toán hạng ra khỏi ngăn xếp nếu cần để đánh giá toán tử này. Đánh giá. Đẩy kết quả trở lại ngăn xếp! Và bạn gần như hoàn thành. Tiếp tục làm như vậy cho đến khi ngăn xếp chỉ có một mục nhập = giá trị bạn đang tìm.

Hãy thực hiện (1 + 2) * 3 trong hậu tố là "12 + 3 *". Đọc số đầu tiên = 1. Đẩy nó vào ngăn xếp. Đọc tiếp theo. Số = 2. Đẩy nó vào ngăn xếp. Đọc tiếp theo. Nhà điều hành. Cái nào? +. Loại nào? Binary = cần hai toán hạng. Pop stack hai lần = argright là 2 và argleft là 1. 1 + 2 là 3. Đẩy 3 trở lại stack. Đọc tiếp theo từ chuỗi postfix. Một con số của nó. 3.Push. Đọc tiếp theo. Nhà điều hành. Cái nào? *. Loại nào? Binary = cần hai số -> pop stack hai lần. Lần đầu vào argright, lần thứ hai vào argleft. Đánh giá hoạt động - 3 lần 3 là 9 lần đẩy 9 trên ngăn xếp. Đọc tiếp char postfix. Nó vô hiệu. Kết thúc đầu vào. Pop stack onec = đó là câu trả lời của bạn.

3). Shunting Yard được sử dụng để biến đổi biểu thức infix có thể đọc được (dễ dàng) của con người thành biểu thức hậu tố (cũng có thể dễ dàng đọc được của con người sau một số thực hành). Dễ dàng viết mã bằng tay. Xem bình luận trên và net.


4

Có ngôn ngữ nào bạn muốn sử dụng không? ANTLR sẽ cho phép bạn làm điều này từ góc độ Java. Adrian Kuhn có một bài viết xuất sắc về cách viết một ngữ pháp thực thi trong Ruby; trên thực tế, ví dụ của anh ấy gần như chính xác là ví dụ về biểu thức số học của bạn.


Tôi phải thừa nhận rằng các ví dụ của tôi được đưa ra trong bài đăng trên blog đang nhận sai đệ quy trái, tức là a - b - c đánh giá thành (a - (b -c)) thay vì ((a -b) - c). Trên thực tế, điều đó nhắc nhở tôi về việc thêm một việc cần làm mà tôi nên sửa các bài đăng trên blog.
akuhn

4

Nó phụ thuộc vào cách bạn muốn nó "chung chung".

Nếu bạn muốn nó thực sự tổng quát chẳng hạn như có thể phân tích cú pháp các hàm toán học cũng như sin (4 + 5) * cos (7 ^ 3), bạn có thể sẽ cần một cây phân tích cú pháp.

Trong đó, tôi không nghĩ rằng việc thực hiện đầy đủ là thích hợp để được dán ở đây. Tôi khuyên bạn nên xem một trong những cuốn sách " Dragon book " khét tiếng .

Nhưng nếu bạn chỉ muốn hỗ trợ ưu tiên , thì bạn có thể làm điều đó bằng cách trước tiên chuyển đổi biểu thức thành dạng hậu tố trong đó một thuật toán mà bạn có thể sao chép và dán sẽ có sẵn từ google hoặc tôi nghĩ bạn có thể tự mã hóa nó bằng một mã nhị phân cây.

Khi bạn có nó ở dạng postfix, thì đó là một miếng bánh từ đó trở đi vì bạn đã hiểu ngăn xếp giúp ích như thế nào.


Cuốn sách về rồng có thể hơi thừa đối với một trình đánh giá biểu thức - một trình phân tích cú pháp gốc đệ quy đơn giản là tất cả những gì cần thiết, nhưng nó phải đọc nếu bạn muốn làm bất cứ điều gì sâu rộng hơn trong trình biên dịch.
Nhật thực

1
Wow - thật vui khi biết rằng cuốn "Dragon book" vẫn còn được thảo luận. Tôi nhớ đã học nó - và đọc nó xuyên suốt - ở trường đại học, 30 năm trước.
Schroedingers Cat

4

Tôi khuyên bạn nên gian lận và sử dụng Thuật toán Shunting Yard . Đó là một phương tiện dễ dàng để viết một trình phân tích cú pháp kiểu máy tính đơn giản và được ưu tiên hơn.

Nếu bạn muốn phân tích đúng mọi thứ và có các biến, v.v. thì tôi sẽ tiếp tục và viết một trình phân tích cú pháp gốc đệ quy theo đề xuất của những người khác ở đây, tuy nhiên nếu bạn chỉ cần một trình phân tích cú pháp kiểu máy tính thì thuật toán này sẽ đủ :-)


4

Tôi tìm thấy điều này trên PIClist về thuật toán Shunting Yard :

Harold viết:

Tôi nhớ đã đọc cách đây rất lâu về một thuật toán chuyển đổi các biểu thức đại số thành RPN để dễ dàng đánh giá. Mỗi giá trị infix hoặc toán tử hoặc dấu ngoặc đơn được đại diện bởi một toa xe lửa trên đường ray. Một loại ô tô rẽ sang đường khác và loại còn lại tiếp tục đi thẳng về phía trước. Tôi không nhớ lại các chi tiết (rõ ràng!), Nhưng luôn nghĩ rằng nó sẽ thú vị khi viết mã. Điều này quay lại khi tôi viết mã lắp ráp 6800 (không phải 68000).

Đây là "thuật toán sân shunting" và nó là thứ mà hầu hết các trình phân tích cú pháp máy sử dụng. Xem bài viết về phân tích cú pháp trong Wikipedia. Một cách dễ dàng để viết mã thuật toán sân vận động là sử dụng hai ngăn xếp. Một là ngăn xếp "đẩy" và ngăn còn lại là ngăn xếp "giảm" hoặc "kết quả". Thí dụ:

pstack = () // trống rstack = () input: 1 + 2 * 3 ưu tiên = 10 // giảm thấp nhất = 0 // không giảm

start: token '1': isnumber, đặt trong pstack (push) token '+': isoperator đặt ưu tiên = 2 nếu ưu tiên <trước_operator_precedence rồi giảm () // xem bên dưới đặt '+' trong pstack (push) token '2' : isnumber, đặt vào mã thông báo pstack (push) '*': isoperator, đặt ưu tiên = 1, đặt trong pstack (đẩy) // kiểm tra mức độ ưu tiên như // mã thông báo trên '3': isnumber, đặt vào pstack (đẩy) cuối đầu vào, cần phải giảm (mục tiêu là ngăn trống) giảm () // xong

để giảm, bật các phần tử từ ngăn xếp đẩy và đưa chúng vào ngăn xếp kết quả, luôn hoán đổi 2 mục hàng đầu trên pstack nếu chúng có dạng 'toán tử' 'số':

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

nếu biểu thức sẽ là:

1 * 2 + 3

thì trình kích hoạt giảm sẽ là việc đọc mã thông báo '+' có tiền nghiệm thấp hơn so với '*' đã được đẩy, vì vậy nó sẽ thực hiện:

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

và sau đó nhấn '+' rồi đến '3' và cuối cùng là giảm:

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

Vì vậy, phiên bản ngắn gọn là: số đẩy, khi các toán tử đẩy kiểm tra mức độ ưu tiên của toán tử trước. Nếu nó cao hơn toán tử sẽ được đẩy bây giờ, trước tiên hãy giảm, sau đó đẩy toán tử hiện tại. Để xử lý các parens, chỉ cần lưu mức độ ưu tiên của toán tử 'trước đó' và đặt một dấu vào pstack cho biết hàm số giảm để ngừng giảm khi giải phần bên trong của một cặp dấu ngoặc. Dấu ngoặc đóng sẽ kích hoạt giảm như khi kết thúc nhập, đồng thời xóa dấu ngoặc mở khỏi ngăn và khôi phục quyền ưu tiên 'thao tác trước đó' để quá trình phân tích cú pháp có thể tiếp tục sau dấu ngoặc đóng mà nó đã dừng lại. Điều này có thể được thực hiện với đệ quy hoặc không (gợi ý: sử dụng ngăn xếp để lưu trữ mức độ ưu tiên trước đó khi gặp dấu '(' ...). Phiên bản tổng quát của điều này là sử dụng trình tạo phân tích cú pháp được triển khai thuật số sân vận động, f.ex. sử dụng yacc hoặc bò rừng hoặc taccle (tcl tương tự của yacc).

Peter

-Adam


4

Một tài nguyên khác để phân tích thứ tự ưu tiên là mục phân tích thứ tự ưu tiên Toán tử trên Wikipedia. Bao gồm thuật toán sân shunting của Dijkstra và thuật toán thay thế cây, nhưng đáng chú ý hơn là bao gồm một thuật toán thay thế macro thực sự đơn giản có thể được triển khai nhanh chóng trước bất kỳ trình phân tích cú pháp thiếu ưu tiên nào:

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

Gọi nó là:

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

Điều đó thật tuyệt vời ở sự đơn giản và rất dễ hiểu.


3
Đó là một viên ngọc trai nhỏ xinh. Nhưng mở rộng nó (giả sử, với ứng dụng hàm, phép nhân ẩn, toán tử tiền tố và hậu tố, chú thích kiểu tùy chọn, bất cứ thứ gì) sẽ phá vỡ toàn bộ. Nói cách khác, đó là một vụ hack thanh lịch.
Jared Updike

Tôi không thấy vấn đề. Tất cả những gì điều này làm là thay đổi một vấn đề phân tích ưu tiên toán tử thành một vấn đề phân tích ưu tiên dấu ngoặc đơn.
Marquis of Lorne,

@EJP chắc chắn, nhưng trình phân tích cú pháp trong câu hỏi xử lý dấu ngoặc đơn tốt, vì vậy đây là một giải pháp hợp lý. Tuy nhiên, nếu bạn có một trình phân tích cú pháp không có, thì bạn đã đúng rằng điều này chỉ chuyển vấn đề sang một khu vực khác.
Adam Davis

4

Tôi đã đăng nguồn cho Trình đánh giá toán học Java siêu nhỏ gọn (1 lớp, <10 KiB) trên trang web của tôi. Đây là trình phân tích cú pháp gốc đệ quy thuộc loại đã gây ra vụ nổ sọ cho người đăng câu trả lời được chấp nhận.

Nó hỗ trợ đầy đủ quyền ưu tiên, dấu ngoặc đơn, các biến được đặt tên và các hàm đối số đơn.




2

Tôi hiện đang thực hiện một loạt bài viết xây dựng trình phân tích cú pháp biểu thức chính quy như một công cụ học tập cho các mẫu thiết kế và lập trình có thể đọc được. Bạn có thể xem qua mã có thể đọc được . Bài báo trình bày cách sử dụng thuật toán bãi shunting rõ ràng.


2

Tôi đã viết một trình phân tích cú pháp biểu thức trong F # và viết blog về nó ở đây . Nó sử dụng thuật toán shunting yard, nhưng thay vì chuyển đổi từ infix sang RPN, tôi đã thêm một ngăn xếp thứ hai để tích lũy kết quả tính toán. Nó xử lý chính xác quyền ưu tiên của toán tử, nhưng không hỗ trợ toán tử một ngôi. Tuy nhiên, tôi viết bài này để học F #, không phải để học phân tích cú pháp biểu thức.


2

Có thể tìm thấy giải pháp Python sử dụng pyparsing tại đây . Việc phân tích cú pháp ký hiệu infix với các toán tử khác nhau có mức độ ưu tiên là khá phổ biến và do đó, phân tích cú pháp cũng bao gồm trình tạo biểu thức infixNotation(trước đây operatorPrecedence). Với nó, bạn có thể dễ dàng xác định các biểu thức boolean bằng cách sử dụng "AND", "OR", "NOT", chẳng hạn. Hoặc bạn có thể mở rộng số học bốn hàm của mình để sử dụng các toán tử khác, chẳng hạn như! cho giai thừa, hoặc '%' cho môđun, hoặc thêm toán tử P và C để tính toán hoán vị và kết hợp. Bạn có thể viết một trình phân tích cú pháp infix cho ký hiệu ma trận, bao gồm việc xử lý các toán tử '-1' hoặc 'T' (cho đảo ngược và chuyển vị). Ví dụ về operatorPrecedence về trình phân tích cú pháp 4 chức năng (với '!'.


1

Tôi biết đây là một câu trả lời muộn, nhưng tôi vừa viết một trình phân tích cú pháp nhỏ cho phép tất cả các toán tử (tiền tố, hậu tố và infix-left, infix-right và nonassociative) có quyền ưu tiên tùy ý.

Tôi sẽ mở rộng điều này cho một ngôn ngữ có hỗ trợ DSL tùy ý, nhưng tôi chỉ muốn chỉ ra rằng một ngôn ngữ không cần trình phân tích cú pháp tùy chỉnh để ưu tiên toán tử, người ta có thể sử dụng trình phân tích cú pháp tổng quát không cần bảng và chỉ cần tra cứu mức độ ưu tiên của mỗi toán tử khi nó xuất hiện. Mọi người đã đề cập đến trình phân tích cú pháp Pratt tùy chỉnh hoặc trình phân tích cú pháp shunting yard có thể chấp nhận đầu vào bất hợp pháp - cái này không cần phải được tùy chỉnh và (trừ khi có lỗi) sẽ không chấp nhận đầu vào xấu. Theo một nghĩa nào đó, nó không hoàn chỉnh, nó được viết để kiểm tra thuật toán và đầu vào của nó ở dạng cần một số xử lý trước, nhưng có những nhận xét làm rõ ràng điều đó.

Lưu ý rằng một số loại toán tử phổ biến bị thiếu, chẳng hạn như loại toán tử được sử dụng để lập chỉ mục tức là bảng [chỉ mục] hoặc gọi một hàm chức năng (tham số-biểu thức, ...) Tôi sẽ thêm chúng, nhưng hãy nghĩ cả hai dưới dạng hậu tố các toán tử trong đó những gì nằm giữa các bộ phân tích '[' và ']' hoặc '(' và ')' được phân tích cú pháp với một phiên bản khác của trình phân tích cú pháp biểu thức. Xin lỗi vì đã bỏ qua, nhưng phần postfix vẫn ở trong - việc thêm phần còn lại có thể sẽ gần như gấp đôi kích thước của mã.

Vì trình phân tích cú pháp chỉ là 100 dòng mã vợt, có lẽ tôi chỉ nên dán nó vào đây, tôi hy vọng điều này không dài hơn stackoverflow cho phép.

Một vài chi tiết về các quyết định tùy ý:

Nếu một toán tử tiền tố có mức độ ưu tiên thấp đang cạnh tranh cho các khối tiền tố giống như một toán tử tiền tố có mức độ ưu tiên thấp thì toán tử tiền tố sẽ thắng. Điều này không xuất hiện trong hầu hết các ngôn ngữ vì hầu hết không có các toán tử hậu tố ưu tiên thấp. - ví dụ: ((dữ liệu a) (trái 1 +) (trước 2 không) (dữ liệu b) (đăng 3!) (trái 1 +) (dữ liệu c)) là a + không phải b! + c trong đó không phải là a toán tử tiền tố và! là toán tử hậu tố và cả hai đều có mức độ ưu tiên thấp hơn + vì vậy họ muốn nhóm theo những cách không tương thích như (a + không phải b!) + c hoặc dưới dạng a + (không phải b! + c) trong những trường hợp này, toán tử tiền tố luôn thắng, vì vậy thứ hai là cách nó phân tích cú pháp

Các toán tử infix không liên kết thực sự ở đó để bạn không phải giả vờ rằng các toán tử trả về các kiểu khác nhau mà chúng có ý nghĩa với nhau, nhưng không có các kiểu biểu thức khác nhau cho mỗi kiểu đó là một k bùn. Như vậy, trong thuật toán này, các toán tử không liên kết từ chối liên kết không chỉ với chính họ mà còn với bất kỳ toán tử nào có cùng mức độ ưu tiên. Đó là một trường hợp phổ biến vì <<= ==> = vv không liên kết với nhau trong hầu hết các ngôn ngữ.

Câu hỏi làm thế nào các loại toán tử khác nhau (trái, tiền tố, v.v.) phá vỡ quan hệ về mức độ ưu tiên là một câu hỏi không nên đặt ra, bởi vì nó không thực sự có ý nghĩa khi cho các toán tử thuộc các loại khác nhau cùng một mức độ ưu tiên. Thuật toán này thực hiện điều gì đó trong những trường hợp đó, nhưng tôi thậm chí không buồn tìm hiểu chính xác điều gì vì ngữ pháp như vậy ngay từ đầu là một ý tưởng tồi.

#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

Đây là một giải pháp đệ quy trường hợp đơn giản được viết bằng Java. Lưu ý rằng nó không xử lý các số âm nhưng bạn có thể thêm nó nếu bạn muốn:

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

Thuật toán có thể được mã hóa dễ dàng trong C dưới dạng trình phân tích cú pháp gốc đệ quy.

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

các lib tiếp theo có thể hữu ích: yupana - các phép toán số học nghiêm ngặt; tinyexpr - phép toán số học + hàm toán học C + một hàm do người dùng cung cấp; mpc - tổ hợp phân tích cú pháp

Giải trình

Hãy nắm bắt chuỗi các ký hiệu biểu thị biểu thức đại số. Đầu tiên là một số, đó là một chữ số thập phân được lặp lại một hoặc nhiều lần. Chúng tôi sẽ coi ký hiệu đó là quy tắc sản xuất.

number -> [0..9]+

Toán tử phép cộng với các toán hạng của nó là một quy tắc khác. Nó là một trong hai numberhoặc bất kỳ ký hiệu nào thể hiện sum "*" sumtrình tự.

sum -> number | sum "+" sum

Hãy thử thay thế numbervào sum "+" sumđó sẽ đượcnumber "+" number có thể được mở rộng thành [0..9]+ "+" [0..9]+mà cuối cùng có thể được rút gọn thành 1+8biểu thức cộng đúng.

Các thay thế khác cũng sẽ tạo ra biểu thức đúng: sum "+" sum -> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number->number "+" number "+" number ->12+3+5

Từng chút một, chúng ta có thể giống với tập hợp các quy tắc sản xuất hay còn gọi là ngữ pháp thể hiện tất cả các biểu thức đại số có thể.

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

Để kiểm soát quyền ưu tiên của người vận hành, hãy thay đổi vị trí của quy tắc sản xuất của nó so với những người khác. Nhìn vào ngữ pháp ở trên và lưu ý rằng quy tắc sản xuất cho *được đặt bên dưới+ điều này sẽ buộc productđánh giá trướcsum . Việc triển khai chỉ kết hợp nhận dạng mẫu với đánh giá và do đó phản ánh chặt chẽ các quy tắc sản xuất.

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

Ở đây chúng tôi đánh giá termđầu tiên và trả về nó nếu không có *ký tự nào sau nó, điều này được để lại choise trong quy tắc sản xuất của chúng tôi, ngược lại - đánh giá các ký hiệu sau và trả về term.value * product.value đây là ký tự đúng trong quy tắc sản xuất của chúng tôi, tức làterm "*" product

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.