Рівняння (вираз) парсера з перевагою?


104

Я розробив аналізатор рівнянь, використовуючи простий алгоритм стека, який буде обробляти бінарні (+, -, |, &, *, / і т.д.) оператори, одинарні (!) Оператори та дужки.

Однак, використовуючи цей метод, ми залишаємо все, що має однаковий пріоритет - він оцінюється зліва направо незалежно від оператора, хоча пріоритет може бути застосований за допомогою дужок.

Тож зараз "1 + 11 * 5" повертає 60, а не 56, як можна було очікувати.

Незважаючи на те, що це підходить для поточного проекту, я хочу мати загальну програму, яку я можу використовувати для подальших проектів.

Відредаговано для наочності:

Який хороший алгоритм розбору рівнянь з перевагою?

Мене цікавить щось просте в реалізації та зрозуміти, що я можу кодувати себе, щоб уникнути проблем з ліцензуванням наявного коду.

Граматика:

Я не розумію граматичного питання - я написав це вручну. Це досить просто, що я не бачу потреби в YACC або Bison. Мені просто потрібно обчислити рядки з рівняннями, такими як "2 + 3 * (42/13)".

Мова:

Я роблю це на C, але мене цікавить алгоритм, а не мовне рішення. C достатньо низький, щоб його було легко перетворити на іншу мову, якщо виникне потреба.

Приклад коду

Я розмістив тестовий код для простого аналізатора виразів, про який я говорив вище. Вимоги до проекту були змінені, і тому мені ніколи не потрібно було оптимізувати код для продуктивності чи простору, оскільки він не був включений у проект. Це в оригінальній багатослівній формі і має бути легко зрозумілою. Якщо я з цим що-небудь робити далі з точки зору переваги оператора, я, мабуть, виберу макро-хак, оскільки він просто відповідає решті програми. Якщо я коли-небудь використаю це в реальному проекті, я буду переходити до більш компактного / швидкого аналізатора.

Супутнє питання

Розумний дизайн математичного аналізатора?

-Адам


Я написав синтаксичний аналізатор в C # на своєму блозі. Він робить інфікування на постфікс без стека в алгоритмі маневрових дворів. Він використовує лише масив.
Гуге

Як я розумію, вам потрібно розбирати лише арифметичне вираження. Використовуйте зворотні польські позначення
mishadoff

Відповіді:


69

Важкий шлях

Ви хочете рекурсивний аналізатор спуску .

Щоб отримати перевагу, потрібно думати рекурсивно, наприклад, використовуючи зразок рядка,

1+11*5

щоб зробити це вручну, вам доведеться прочитати 1, а потім побачити плюс і почати цілком новий рекурсивний "сеанс" розбору, починаючи з 11... і переконайтесь, що він проаналізував 11 * 5власний фактор, отримавши дерево розбору 1 + (11 * 5).

Це все відчуває настільки болісно навіть для спроб пояснення, особливо з додатковою безсильністю C. Дивіться, після розбору 11, якщо * насправді було +, замість цього вам доведеться відмовитися від спроби ввести термін і замість цього розібрати 11себе як фактор. Голова в мене вже вибухає. Це можливо за допомогою рекурсивної гідної стратегії, але є кращий спосіб ...

Простий (правильний) спосіб

Якщо ви використовуєте такий інструмент GPL, як Bison, вам, ймовірно, не потрібно турбуватися про проблеми з ліцензуванням, оскільки код C, породжений зубрами, не охоплюється GPL (IANAL, але я впевнений, що інструменти GPL не змушують GPL вмикати згенерований код / ​​двійкові файли; наприклад, Apple компілює код, як, скажімо, Aperture with GCC, і вони продають його без необхідності GPL-коду).

Завантажте Bison (або щось еквівалентне, ANTLR тощо).

Зазвичай існує зразок коду, на який можна просто запустити зубр і отримати потрібний код С, який демонструє цей чотири функціональний калькулятор:

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

Подивіться на згенерований код і побачите, що це не так просто, як це звучить. Крім того , переваги використання такого інструменту , як Bison є : 1) ви дізнаєтеся що - то (особливо якщо ви читали книгу Дракон і дізнатися про граматику), 2) уникнути НИЗ намагається винайти колесо. Завдяки справжньому інструменту генератора аналізаторів, ви насправді маєте надію на збільшення масштабів пізніше, показуючи іншим людям, яким ви знаєте, що парсери - це область інструментів розбору.


Оновлення:

Люди тут пропонували багато обгрунтованих порад. Моє єдине застереження від пропуску інструментів розбору або просто використання алгоритму Shunting Yard або ручного рекурсивного рекурсивного пристойного аналізатора - це те, що маленькі мови іграшок 1 можуть колись перетворитись на великі фактичні мови з функціями (sin, cos, log) та змінними, умовами і для петлі.

Flex / Bison може бути надмірним для невеликого, простого перекладача, але одноразовий аналізатор + оцінювач може спричинити проблеми внизу лінії, коли потрібно внести зміни або потрібно додати функції. Ваша ситуація буде різною, і вам потрібно буде використовувати своє судження; просто не карайте інших людей за ваші гріхи [2] і будуйте менш, ніж адекватний інструмент.

Мій улюблений інструмент для розбору

Найкращим у світі інструментом для роботи є бібліотека Parsec (для рекурсивних пристойних аналізаторів), яка постачається з мовою програмування Haskell. Це дуже схоже на BNF , або як якийсь спеціалізований інструмент або специфічну для домену мову для розбору (зразок коду [3]), але насправді це лише звичайна бібліотека в Haskell, що означає, що вона компілюється в тому ж кроці збірки, що і решта вашого коду Haskell, і ви можете записати довільний код Haskell і зателефонувати в межах свого аналізатора, і ви зможете змішувати і співставляти інші бібліотеки, все в тому ж коді . (Вбудована така мова розбору в мову, відмінну від Haskell, до речі призводить до навантаження синтаксичної крихти. Я це зробив у C #, і це працює досить добре, але це не так симпатично і лаконічно.)

Примітки:

1 Річард Сталлман каже: В Чому не слід використовувати Tcl

Основний урок Emacs полягає в тому, що мова для розширень не повинна бути просто "мовою розширення". Це повинна бути справжня мова програмування, розроблена для написання та підтримки суттєвих програм. Тому що люди захочуть це зробити!

[2] Так, мені назавжди страшно користуватися цією "мовою".

Також зауважте, що коли я надсилав цей запис, попередній перегляд був правильним, але SO менш ніж адекватний аналізатор з'їв мій тег прив’язки якорів у першому абзаці , доводячи, що парсери - це не те, з чим можна потрохувати, тому що якщо ви використовуєте регулярні вирази та один вимикає, ви хакуєте напевно, вийде щось тонке і маленьке неправильно .

[3] Фрагмент аналізатора Haskell за допомогою Parsec: чотирифункціональний калькулятор, розширений експонентами, дужками, пробілом для множення та константами (наприклад, pi та 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
Щоб підкреслити мою думку, зауважте, що розмітка в моєму дописі не розбирається правильно (і це змінюється між розміткою, відображеною статично, і такою, що відображається в попередньому перегляді WMD). Було кілька спроб виправити це, але я думаю, що PARSER помиляється. Зроби всі прихильність і отримай розбір правильно!
Джаред Updike

155

Алгоритм маневрового двору є правильним інструментом для цього. Вікіпедія насправді з цим збиває з пантелику, але в основному алгоритм працює так:

Скажіть, ви хочете оцінити 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], Ops = [+]
  • Прочитайте 2. N = [1 2], Ops = [+]
  • Прочитайте *. N = [1 2], Ops = [+ *]
  • Прочитайте 3. N = [1 2 3], Ops = [+ *]
  • Читати +. N = [1 2 3], Ops = [+ *]
    • Спустіть 3, 2 та виконайте 2 *3, а результат натисніть на N. N = [1 6], Ops = [+]
    • +залишається асоціативним, тому ви хочете також спливати 1, 6 і виконати +. N = [7], Ops = [].
    • Нарешті натисніть [+] на стек оператора. N = [7], Ops = [+].
  • Прочитайте 4. N = [7 4]. Опс = [+].
  • У вас закінчується вхід, тому ви хочете спорожнити стеки зараз. Після цього ви отримаєте результат 11.

Там це не так складно, чи не так? І це не викликає жодних граматичних чи парсерних генераторів.


6
Насправді вам не потрібні дві стеки, поки ви зможете побачити друге, що стоїть на стеці, не спливаючи верх. Натомість можна використовувати один стек, який чергує числа та оператори. Це насправді відповідає саме тому, що робить генератор парного аналізатора LR (наприклад, зубр).
Кріс Додд

2
Дійсно приємне пояснення алгоритму, який я щойно реалізував. Крім того, ви не перетворюєте його на постфікс, що також добре. Додавання підтримки в дужках теж дуже просто.
Джорджі

4
Спрощену версію алгоритму маневрового двору можна знайти тут: andreinc.net/2010/10/05/… (з реалізаціями на Java та python)
Андрій Чобану

1
Дякую за це, саме за те, що я прошу!
Джо Грін

Дякую за згадку про ліворуч - асоціативне. Я затримався з потрійним оператором: як розбирати складні вирази з вкладеними "?:". Я зрозумів, що обидва '?' і ":" повинні мати однаковий пріоритет. І якщо ми інтерпретуємо "?" як право - асоціативний, так і ":", як лівий - асоціативний, цей алгоритм працює з ними дуже добре. Також ми можемо звалити 2 оператори лише тоді, коли вони залишилися обоє - асоціативні.
Владислав

25

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

Дуже вдале пояснення різних підходів:

  • Рекурсивно-розпускне розпізнавання
  • Алгоритм маневрового двору
  • Класичне рішення
  • Прецедентне сходження

Написана простою мовою та псевдокодом.

Мені подобається «пріоритетне сходження».


Здається, посилання розірвана. Кращою відповіддю було б перефразовувати кожен метод, щоб, коли це посилання зникло, частина цієї корисної інформації збереглася б тут.
Адам Уайт

18

Там хороша стаття тут про поєднання простого рекурсивного спуску парсер з оператором-старшинства розборі. Якщо ви нещодавно писали парсери, читати слід дуже цікаво та повчально.


16

Давним-давно я створив власний алгоритм розбору, який не міг знайти в жодних книгах про розбір (як-от Книга Драконів). Дивлячись на вказівники на алгоритм Shunting Yard, я бачу подібність.

Близько 2 років тому я опублікував публікацію про неї разом із вихідним кодом Perl на веб-сайті http://www.perlmonks.org/?node_id=554516 . Портати на інші мови легко: перша реалізація була в асемблері Z80.

Він ідеально підходить для прямого обчислення з числами, але ви можете використовувати його для створення дерева розбору, якщо потрібно.

Оновлення Оскільки більше людей можуть читати (або запускати) Javascript, я повторно доповнив свій аналізатор у Javascript, після реорганізації коду. Весь аналізатор знаходиться під 5 к. Код 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 ? Це дозволяє писати схожі на EBNF граматики на C ++ так:

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/… . Я ніколи не зрозумію, чому люди все ще реалізують там власні міні-аналізатори.
Стефан

5

Коли ви ставите своє запитання, рекурсії не потрібно. Відповідь три речі: Позначення Postfix плюс алгоритм Shunting Yard плюс Оцінка вираження Postfix:

1). Позначення Postfix = винайдено, щоб усунути необхідність явної специфікації пріоритету. Детальніше читайте в мережі, але ось суть цього: вираз виправлення (1 + 2) * 3, а люди легко читають і обробляють не дуже ефективні для обчислень за допомогою машини. Що? Просте правило, яке говорить "перепишіть вираз кешуванням в пріоритеті, а потім завжди обробляйте його зліва направо". Так інфікс (1 + 2) * 3 стає постфіксом 12 + 3 *. POST, оскільки оператор розміщується завжди ПІСЛЯ операндів.

2). Оцінка вираження постфікса. Легко. Читання рядків після рядка постфікса. Натисніть на стек, поки оператор не побачить. Перевірити тип оператора - одинарний? двійкові? третинний? Попустіть стільки операндів із стека, скільки потрібно для оцінки цього оператора. Оцініть. Відсуньте результат назад на стек! І ур майже зроблено. Продовжуйте робити це, поки стек не має лише одного запису = значення ур.

Зробимо (1 + 2) * 3, який у постфіксі - «12 + 3 *». Прочитайте перше число = 1. Натисніть на стек. Читайте далі. Число = 2. Натисніть на стек. Читайте далі. Оператор. Який? +. Який? Бінарний = потребує двох операндів. Поп стек двічі = argright - 2, а argleft - 1. 1 + 2 - 3. Натисніть 3 назад на стек. Читати далі з рядка Postfix. Його номер. 3.Натисніть Читайте далі. Оператор. Який? *. Який? Binary = потребує двох чисел -> pop stack двічі. Перший спливає в аргумент, другий раз у арґлефт. Оцініть операцію - 3 рази 3 - це 9. Натисніть 9 на стек. Читайте наступну таблицю постфікса. Це недійсне. Кінець введення. Поп стек onec = ось ваша відповідь.

3). Маневровий двір застосовується для перетворення людського (легко) читаного виразів інфіксації в експресію постфікса (також людину легко читати після певної практики). Легко кодувати вручну. Дивіться коментарі вище та в мережі.


4

Чи є мова, якою ви хочете користуватися? ANTLR дозволить вам зробити це з точки зору Java. Адріан Kuhn має відмінну рецензію про те , як написати виконувану граматику в Рубіні; насправді, його приклад є майже точно вашим прикладом арифметичного вираження.


Я мушу визнати, що мої приклади, наведені в дописі блогу, стають помилково лівими рекурсіями, тобто a - b - c оцінює до (a - (b -c)) замість ((a -b) - c). Насправді це нагадує мені додати тодо, що я повинен виправити повідомлення в блозі.
акун

4

Це залежить від того, наскільки "загальним" ви хочете це бути.

Якщо ви хочете, щоб це було насправді загальним, таким як вміти розбирати математичні функції так само, як sin (4 + 5) * cos (7 ^ 3), напевно, вам знадобиться дерево розбору.

У якому, я не думаю, що повноцінна реалізація є належною для вставки тут. Я б запропонував вам переглянути одну з сумнозвісних " Книг Драконів ".

Але якщо ви просто хочете підтримати пріоритет , ви можете зробити це, спершу перетворивши вираз у форму постфікса, в якій алгоритм, який ви можете скопіювати та вставити, повинен бути доступний у google або я думаю, ви можете кодувати його самостійно за допомогою двійкового файлу дерево.

Коли ви маєте його у формі постфіксу, тоді це шматок пирога, оскільки ви вже розумієте, як допомагає стек.


Книга драконів може бути трохи надмірною для оцінювача виразів - простий рекурсивний аналізатор спуску - це все, що потрібно, але це обов’язково читати, якщо ви хочете зробити щось більш обширне у компіляторах.
Затемнення

1
Нічого собі - приємно знати, що "Книга Дракона" все ще обговорюється. Я пам'ятаю, як вивчав її - і читав усе це - в університеті, 30 років тому.
Schroedingers Cat

4

Я б запропонував шахрайство та використання алгоритму « Шунтовий двір» . Це простий засіб написання простого аналізатора типу калькулятора, який має перевагу.

Якщо ви хочете правильно впорядкувати речі, включати змінні тощо, то я б продовжував записувати рекурсивний аналізатор спуску, як це запропонували інші, але якщо вам просто потрібен аналізатор стилю калькулятора, цього алгоритму має бути достатньо :-)


4

Я знайшов це в списку PIC щодо алгоритму Shunting Yard :

Гарольд пише:

Я пам'ятаю, як давно читав алгоритм, який перетворював алгебраїчні вирази в RPN для легкої оцінки. Кожне значення інфіксу або оператор чи дужки було представлено залізничним вагоном на колії. Один тип автомобіля відрізався на інший трек, а другий продовжував прямо вперед. Я не пригадую деталей (очевидно!), Але завжди думав, що це буде цікаво кодувати. Це повернулося, коли я писав 6800 (не 68000) код складання.

Це "алгоритм маневрених дворів", і саме це використовується більшість машинних аналізаторів. Дивіться статтю про розбір у Вікіпедії. Простий спосіб кодування алгоритму маневреного двору - це використання двох стеків. Один - стек "push", а другий - стек "зменшення" або "результат". Приклад:

pstack = () // порожній rstack = () вхід: 1 + 2 * 3 пріоритет = 10 // найнижчий зменшити = 0 // не зменшити

start: token '1': isnumber, введено в pstack (push) токен '+': ізоператор встановив пріоритет = 2, якщо пріоритет <previous_operator_precedence, то зменшити () // див. нижче поставити '+' у pstack (push) токен '2' : isnumber, покласти в pstack (push) токен '*': ізоператор, встановити пріоритет = 1, поставити в pstack (push) // перевірити пріоритет як // вище токена '3': isnumber, поставити в pstack (push) кінець введення, потрібно зменшити (ціль порожній pstack) зменшити () // зроблено

щоб зменшити, поп-елементи з push-стеку та помістіть їх у стек результатів, завжди міняйте місцями найпопулярніші 2 пункти на pstack, якщо вони мають форму 'operator' 'number':

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 '' + '

Отже, коротка версія така: push push, коли штовхаючі оператори перевіряють пріоритет попереднього оператора. Якщо вона була вищою за операторську, яку потрібно натиснути зараз, спочатку зменшіть, а потім натисніть поточного оператора. Для обробки паронів просто збережіть пріоритет оператора "попередній" та поставте позначку на pstack, яка повідомляє альгоритму зменшення зменшити при вирішенні внутрішньої частини батьківської пари. Закриваючий параметр викликає зменшення, як і кінець введення, а також видаляє відкриту позначку батьків з pstack і відновлює пріоритет "попередньої операції", щоб синтаксичний аналіз може продовжуватися після закритого пароля, де він припинився. Це можна зробити за допомогою рекурсії або без (підказка: використовуйте стек, щоб зберігати попередній пріоритет, коли зустрічаєте знак '(' ...). Узагальнена версія цього полягає у використанні генератора парсера, реалізованого маневрового алгоритму двору, f.ex. використовуючи yacc або зубр або такл (аналог tcl yacc).

Петро

-Адам


4

Інший ресурс для розбору пріоритету - це запис аналізатора пріоритету Оператора у Вікіпедії. Охоплює алгоритм маневрового двору Дійкстри та алгоритм альтернативного дерева, але більш чітко охоплює дійсно простий алгоритм заміни макросу, який можна тривіально реалізувати перед будь-яким невідомим аналізатором пріоритету:

#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

Я розмістив джерело для ультра компактного Java-математичного оцінювача (1 клас, <10 KiB) на своєму веб-сайті. Це рекурсивний аналізатор спуску типу, який спричинив вибух черепа для плаката прийнятої відповіді.

Він підтримує повний пріоритет, круглі дужки, названі змінні та функції одного аргументу.




2

Зараз я працюю над низкою статей, що будують регулярний аналізатор виразів як інструмент навчання для дизайнерських моделей та читаного програмування. Ви можете подивитися читабельний код . У статті представлено чітке використання алгоритму маневрових дворів.


2

Я написав синтаксичний аналізатор у F # і розмістив про це тут . Він використовує алгоритм маневрового двору, але замість перетворення з інфіксації в RPN я додав другий стек для накопичення результатів обчислень. Він правильно обробляє перевагу оператора, але не підтримує одинакових операторів. Я написав це, щоб дізнатися F #, а не для вивчення розбору виразів.


2

Рішення Python, використовуючи піпарсинг, можна знайти тут . Позначення інфіксування розбору з різними операторами з пріоритетом є досить поширеним явищем, тому розшифровка також включає в себе infixNotation(раніше operatorPrecedence) конструктор виразів. З його допомогою ви можете легко визначити булеві вирази, використовуючи, наприклад, "І", "АБО", "НЕ". Або ви можете розширити чотирифункціональну арифметику для використання інших операторів, таких як! для факториалу, або "%" для модуля, або додайте оператори P і C для обчислення перестановок і комбінацій. Ви можете написати інфіксний аналізатор для матричного позначення, який включає обробку операторів '-1' або 'T' (для інверсії та транспозиції). Приклад operatorPrecedence 4-функційного аналізатора (з '!'


1

Я знаю, що це пізня відповідь, але я щойно написав крихітний аналізатор, який дозволяє всім операторам (префікс, постфікс та інфікс-ліворуч, інфікс-право та неасоціативно) мати довільний пріоритет.

Я збираюся розширити це на мову з довільною підтримкою DSL, але я просто хотів зазначити, що для пріоритетності оператора не потрібні спеціальні парсери, можна використовувати узагальнений аналізатор, якому таблиці взагалі не потрібні, і просто шукає пріоритет кожного оператора, як він з'являється. Люди згадують власні парсери Pratt або маневрені парсери, які можуть приймати незаконні введення даних - цього не потрібно налаштовувати, і (якщо немає помилки) не прийматимуть неправильних даних. У певному сенсі це не завершено, він був написаний для перевірки алгоритму, і його введення знаходиться у формі, яка потребує певної попередньої обробки, але є коментарі, які дозволяють зрозуміти це.

Зверніть увагу, що деякі загальні типи операторів відсутні, наприклад тип оператора, який використовується для індексації, тобто таблиця [індекс] або виклик функції функції (параметр-вираз, ...) Я збираюся додати їх, але думаю про обидва як постфікс оператори, де те, що відбувається між деліметром '[' і ']' або '(' і ')', аналізується з іншим екземпляром аналізатора виразів. Вибачте, що покинули це, але частина постфіксу є - додавання решти, ймовірно, майже вдвічі перевищить розмір коду.

Оскільки в аналізаторі є лише 100 рядків коду ракетки, можливо, я повинен просто вставити його сюди, я сподіваюся, що це не довше, ніж дозволяє stackoverflow.

Кілька деталей щодо довільних рішень:

Якщо оператор постфікса з низьким пріоритетом конкурує за ті ж блоки інфіксів, що і оператор префікса з низьким пріоритетом, виграє оператор префікса. Це не існує більшості мов, оскільки більшість не мають операторів постфікса з низьким пріоритетом. - наприклад: ((дані a) (зліва 1 +) (до 2 немає) (дані b) (пост 3!) (зліва 1 +) (дані c)) є + не b! + c, де не є a оператор префікса і! є оператором postfix і обидва мають нижчий пріоритет, ніж +, тому вони хочуть групувати несумісними способами або як (a + not b!) + c або як + (не 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;
}

наступні мочки можуть бути корисні: юпана - строго арифметичні операції; 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.