ตัวแยกวิเคราะห์สมการ (นิพจน์) ที่มีลำดับความสำคัญ?


105

ฉันได้พัฒนาตัวแยกวิเคราะห์สมการโดยใช้อัลกอริธึมสแต็กแบบง่ายที่จะจัดการตัวดำเนินการไบนารี (+, -, |, &, *, / ฯลฯ ) ตัวดำเนินการยูนารี (!) และวงเล็บ

อย่างไรก็ตามการใช้วิธีนี้ทำให้ทุกอย่างมีลำดับความสำคัญเหมือนกันโดยจะประเมินจากซ้ายไปขวาโดยไม่คำนึงถึงตัวดำเนินการแม้ว่าจะบังคับใช้ลำดับความสำคัญได้โดยใช้วงเล็บก็ตาม

ตอนนี้ "1 + 11 * 5" จะคืนค่า 60 ไม่ใช่ 56 อย่างที่คาดไว้

แม้ว่าสิ่งนี้จะเหมาะกับโครงการปัจจุบัน แต่ฉันต้องการมีกิจวัตรวัตถุประสงค์ทั่วไปที่สามารถใช้สำหรับโครงการในภายหลังได้

แก้ไขเพื่อความชัดเจน:

อัลกอริทึมที่ดีสำหรับการแยกวิเคราะห์สมการที่มีลำดับความสำคัญคืออะไร?

ฉันสนใจสิ่งที่ง่ายในการติดตั้งและเข้าใจว่าฉันสามารถเขียนโค้ดด้วยตัวเองเพื่อหลีกเลี่ยงปัญหาการออกใบอนุญาตกับโค้ดที่มี

ไวยากรณ์:

ฉันไม่เข้าใจคำถามไวยากรณ์ - ฉันเขียนด้วยมือ ง่ายพอที่ฉันไม่เห็นความจำเป็นสำหรับ YACC หรือ Bison ฉันแค่ต้องคำนวณสตริงด้วยสมการเช่น "2 + 3 * (42/13)"

ภาษา:

ฉันกำลังทำสิ่งนี้ในภาษา C แต่ฉันสนใจอัลกอริทึมไม่ใช่วิธีแก้ปัญหาเฉพาะภาษา C อยู่ในระดับต่ำพอที่จะแปลงเป็นภาษาอื่นได้ง่ายหากจำเป็น

ตัวอย่างรหัส

ฉันโพสต์รหัสทดสอบสำหรับตัวแยกวิเคราะห์นิพจน์ทั่วไปที่ฉันพูดถึงข้างต้น ความต้องการของโครงการเปลี่ยนแปลงไปดังนั้นฉันจึงไม่จำเป็นต้องเพิ่มประสิทธิภาพโค้ดเพื่อประสิทธิภาพหรือพื้นที่เนื่องจากไม่ได้รวมอยู่ในโครงการ อยู่ในรูปแบบ verbose ดั้งเดิมและควรเข้าใจได้ง่าย ถ้าฉันทำอะไรเพิ่มเติมในแง่ของลำดับความสำคัญของตัวดำเนินการฉันอาจจะเลือกมาโครแฮ็คเพราะตรงกับส่วนที่เหลือของโปรแกรมอย่างเรียบง่าย ถ้าฉันเคยใช้สิ่งนี้ในโปรเจ็กต์จริงฉันจะใช้โปรแกรมแยกวิเคราะห์ที่กะทัดรัด / รวดเร็วกว่านี้

คำถามที่เกี่ยวข้อง

การออกแบบที่ชาญฉลาดของโปรแกรมแยกวิเคราะห์ทางคณิตศาสตร์?

- อดัม


ฉันเขียนตัวแยกวิเคราะห์นิพจน์ใน C #ในบล็อกของฉัน มันจะ infix เพื่อ postfix โดยไม่ต้องกองซ้อนในอัลกอริธึม shunting yard ใช้อาร์เรย์เท่านั้น
Guge

อย่างที่ฉันเข้าใจว่าคุณต้องแยกวิเคราะห์นิพจน์ทางคณิตศาสตร์เท่านั้น ใช้Reverse Polish Notation
mishadoff

คำตอบ:


69

วิธีที่ยาก

คุณต้องการparser โคตร recursive

เพื่อให้ได้ลำดับความสำคัญคุณต้องคิดซ้ำ ๆ เช่นใช้สตริงตัวอย่างของคุณ

1+11*5

การทำเช่นนี้ด้วยตนเองคุณจะต้องอ่าน1แล้วดูบวกและเริ่มต้นใหม่ทั้งหมด recursive แจง "เซสชั่น" เริ่มต้นด้วย11... และให้แน่ใจว่าจะแยกออกเป็นปัจจัยของตัวเองยอมต้นไม้แยกด้วย11 * 51 + (11 * 5)

ทั้งหมดนี้รู้สึกเจ็บปวดมากแม้จะพยายามอธิบายโดยเฉพาะอย่างยิ่งกับความไร้อำนาจที่เพิ่มขึ้นของ C ดูหลังจากแยกวิเคราะห์ 11 แล้วถ้า * เป็น + แทนจริงๆคุณจะต้องละทิ้งความพยายามในการสร้างคำและแยกวิเคราะห์แทน11ตัวเองเป็นปัจจัย หัวของฉันจะระเบิดแล้ว เป็นไปได้ด้วยกลยุทธ์ที่ดีซ้ำซาก แต่มีวิธีที่ดีกว่า ...

วิธีที่ง่าย (ขวา)

หากคุณใช้เครื่องมือ GPL เช่น Bison คุณอาจไม่ต้องกังวลเกี่ยวกับปัญหาการออกใบอนุญาตเนื่องจาก GPL ไม่ครอบคลุมรหัส C ที่สร้างโดยวัวกระทิง (IANAL แต่ฉันค่อนข้างมั่นใจว่าเครื่องมือ GPL ไม่บังคับให้เปิด GPL สร้างรหัส / ไบนารีตัวอย่างเช่น Apple รวบรวมรหัสเช่นพูดว่า Aperture กับ GCC และขายโดยไม่ต้องให้ GPL กล่าวรหัส)

ดาวน์โหลด Bison (หรือสิ่งที่เทียบเท่า 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 ดูเหมือนBNFมากหรือเหมือนเครื่องมือพิเศษหรือภาษาเฉพาะโดเมนสำหรับการแยกวิเคราะห์ (โค้ดตัวอย่าง [3]) แต่จริงๆแล้วมันเป็นเพียงไลบรารีทั่วไปใน Haskell ซึ่งหมายความว่ามันจะรวบรวมในขั้นตอนการสร้างเดียวกันกับส่วนที่เหลือ รหัส Haskell ของคุณและคุณสามารถเขียนโดยพลรหัส Haskell และโทรว่าภายใน parser ของคุณและคุณสามารถผสมและตรงกับห้องสมุดอื่น ๆทั้งหมดในรหัสเดียวกัน (การฝังภาษาแยกวิเคราะห์เช่นนี้ในภาษาอื่นที่ไม่ใช่ Haskell ส่งผลให้เกิดปัญหาทางวากยสัมพันธ์มากมายฉันทำสิ่งนี้ใน C # และทำงานได้ค่อนข้างดี แต่มันไม่ค่อยสวยและรวบรัด)

หมายเหตุ:

1 Richard Stallman กล่าวว่าในเหตุใดคุณจึงไม่ควรใช้ Tcl

บทเรียนหลักของ Emacs คือภาษาสำหรับส่วนขยายไม่ควรเป็นเพียง "ภาษาส่วนขยาย" ควรเป็นภาษาโปรแกรมจริงที่ออกแบบมาสำหรับการเขียนและการบำรุงรักษาโปรแกรมจำนวนมาก เพราะคนจะอยากทำ!

[2] ใช่ฉันมีแผลเป็นตลอดกาลจากการใช้ "ภาษา" นั้น

โปรดทราบว่าเมื่อฉันส่งรายการนี้การแสดงตัวอย่างนั้นถูกต้อง แต่ตัวแยกวิเคราะห์น้อยกว่าที่เพียงพอจะกินแท็กจุดยึดปิดของฉันในย่อหน้าแรกซึ่งพิสูจน์ได้ว่าตัวแยกวิเคราะห์ไม่ใช่เรื่องที่น่าเบื่อเพราะถ้าคุณใช้ regexes แล้วแฮ็กคุณ อาจจะได้รับสิ่งที่ละเอียดอ่อนและผิดเล็กน้อย

[3] ตัวอย่างข้อมูลของตัวแยกวิเคราะห์ Haskell โดยใช้พาร์เซก: เครื่องคิดเลขสี่ฟังก์ชันที่ขยายด้วยเลขชี้กำลังวงเล็บช่องว่างสำหรับการคูณและค่าคงที่ (เช่น 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 ผิด ถูกใจทุกคนและแยกวิเคราะห์ได้ถูกต้อง!
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 = [] Ops = []

  • อ่าน 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] Ops = [+]
  • คุณหมดอินพุตดังนั้นคุณจึงต้องการล้างสแต็กตอนนี้ ซึ่งคุณจะได้รับผลลัพธ์ 11.

ที่นั่นไม่ยากเลยใช่ไหม และไม่มีการเรียกใช้ไวยากรณ์หรือตัวสร้างตัวแยกวิเคราะห์ใด ๆ


6
คุณไม่จำเป็นต้องมีสองสแต็กตราบใดที่คุณสามารถเห็นสิ่งที่สองบนสแต็กโดยไม่ต้องโผล่ด้านบน คุณสามารถใช้สแต็กเดียวที่สลับตัวเลขและตัวดำเนินการแทนได้ ในความเป็นจริงนี้สอดคล้องกับสิ่งที่ตัวสร้างตัวแยกวิเคราะห์ LR (เช่นวัวกระทิง) ทำ
Chris Dodd

2
คำอธิบายที่ดีจริงๆเกี่ยวกับอัลกอริทึมที่ฉันเพิ่งใช้ตอนนี้ นอกจากนี้คุณไม่ได้แปลงเป็น postfix ซึ่งก็ดีเช่นกัน การเพิ่มการรองรับสำหรับวงเล็บก็ทำได้ง่ายเช่นกัน
Giorgi

4
สามารถดูเวอร์ชันที่เรียบง่ายสำหรับอัลกอริทึม shunting-yard ได้ที่นี่: andreinc.net/2010/10/05/… (พร้อมการใช้งานใน Java และ python)
Andrei Ciobanu

1
ขอบคุณสำหรับสิ่งนี้สิ่งที่ฉันต้องการ!
Joe Green

ขอบคุณมากสำหรับการพูดถึงด้านซ้าย - เชื่อมโยง ฉันติดอยู่กับตัวดำเนินการ ternary: วิธีแยกวิเคราะห์นิพจน์ที่ซับซ้อนด้วย "?:" ที่ซ้อนกัน ฉันตระหนักว่าทั้งสอง '?' และ ':' ต้องมีลำดับความสำคัญเท่ากัน และถ้าเราตีความ '?' ด้านขวา - เชื่อมโยงและ ':' เป็นด้านซ้าย - เชื่อมโยงอัลกอริทึมนี้ทำงานร่วมกับพวกเขาได้เป็นอย่างดี นอกจากนี้เรายังสามารถยุบตัวดำเนินการ 2 ตัวได้ก็ต่อเมื่อทั้งคู่เหลืออยู่ - เชื่อมโยงกัน
Vladislav

25

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

คำอธิบายที่ดีมากเกี่ยวกับแนวทางต่างๆ:

  • การรับรู้การสืบเชื้อสาย
  • อัลกอริทึมการแบ่งลาน
  • โซลูชันคลาสสิก
  • ลำดับความสำคัญของการปีนเขา

เขียนด้วยภาษาง่ายๆและรหัสหลอก

ฉันชอบ 'ลำดับความสำคัญของการปีนเขา'


ลิงก์ดูเหมือนจะเสีย สิ่งที่จะทำให้ได้คำตอบที่ดีกว่าคือการถอดความแต่ละวิธีเพื่อที่เมื่อลิงก์นั้นหายไปข้อมูลที่เป็นประโยชน์บางส่วนจะถูกเก็บรักษาไว้ที่นี่
Adam White

18

มีบทความที่ดีเป็นที่นี่เกี่ยวกับการรวมตัวแยกวิเคราะห์ recursive โคตรง่ายกับผู้ประกอบการแยกความสำคัญ หากคุณเพิ่งเขียนโปรแกรมแยกวิเคราะห์เมื่อไม่นานมานี้มันควรจะน่าสนใจและให้คำแนะนำในการอ่าน


16

นานมาแล้วฉันได้สร้างอัลกอริธึมการแยกวิเคราะห์ของตัวเองขึ้นมาซึ่งไม่พบในหนังสือเกี่ยวกับการแยกวิเคราะห์ (เช่น Dragon Book) เมื่อมองไปที่ตัวชี้ไปยังอัลกอริทึม Shunting Yard ฉันเห็นความคล้ายคลึงกัน

ประมาณ 2 ปีที่ผ่านมาผมทำโพสต์เกี่ยวกับเรื่องนี้สมบูรณ์ด้วยรหัสที่มา Perl บนhttp://www.perlmonks.org/?node_id=554516 ง่ายต่อการพอร์ตเป็นภาษาอื่น: การใช้งานครั้งแรกที่ฉันทำคือในแอสเซมเบลอร์ Z80

เหมาะอย่างยิ่งสำหรับการคำนวณโดยตรงด้วยตัวเลข แต่คุณสามารถใช้เพื่อสร้างโครงสร้างแยกวิเคราะห์ได้หากคุณต้องการ

อัปเดตเนื่องจากมีคนอ่าน (หรือเรียกใช้) Javascript ได้มากขึ้นฉันจึงติดตั้งโปรแกรมแยกวิเคราะห์ใน Javascript อีกครั้งหลังจากจัดระเบียบรหัสใหม่แล้ว ตัวแยกวิเคราะห์ทั้งหมดอยู่ต่ำกว่า 5k ของโค้ด Javascript (ประมาณ 100 บรรทัดสำหรับตัวแยกวิเคราะห์ 15 บรรทัดสำหรับฟังก์ชัน wrapper) รวมถึงการรายงานข้อผิดพลาดและความคิดเห็น

คุณสามารถค้นหาการสาธิตสดที่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' (กล่าวคือมีลำดับความสำคัญของตัวดำเนินการ) . ตัวอย่างเช่น Googling สำหรับ "ไวยากรณ์สำหรับนิพจน์เลขคณิต" ควรให้คำแนะนำที่ดี ไวยากรณ์ดังกล่าวไม่จำเป็นต้องซับซ้อน:

<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/… . ฉันจะไม่เข้าใจเลยว่าทำไมผู้คนถึงยังใช้ตัวแยกวิเคราะห์ขนาดเล็กของตัวเอง
stephan

5

ในขณะที่คุณตั้งคำถามคุณไม่จำเป็นต้องมีการเรียกซ้ำ คำตอบคือสามสิ่ง: สัญกรณ์ Postfix บวกอัลกอริทึม Shunting Yard บวกการประเมินนิพจน์ Postfix:

1). สัญกรณ์ Postfix = คิดค้นขึ้นเพื่อขจัดความจำเป็นในการกำหนดลำดับความสำคัญที่ชัดเจน อ่านเพิ่มเติมบนเน็ต แต่นี่คือส่วนสำคัญของมัน: infix expression (1 + 2) * 3 ในขณะที่มนุษย์อ่านและประมวลผลได้ง่ายไม่ค่อยมีประสิทธิภาพสำหรับการคำนวณผ่านเครื่อง คืออะไร? กฎง่ายๆที่ระบุว่า "เขียนนิพจน์ซ้ำโดยการแคชตามลำดับความสำคัญแล้วประมวลผลจากซ้ายไปขวาเสมอ" ดังนั้น infix (1 + 2) * 3 จะกลายเป็น postfix 12 + 3 * POST เนื่องจากตัวดำเนินการถูกวางไว้หลังตัวถูกดำเนินการเสมอ

2). การประเมินนิพจน์ postfix ง่าย. อ่านตัวเลขจากสตริง postfix กดลงบนกองจนกว่าจะเห็นตัวดำเนินการ ตรวจสอบประเภทตัวดำเนินการ - unary? ไบนารี่? ตติยภูมิ? แสดงตัวถูกดำเนินการออกจากสแต็กได้มากเท่าที่จำเป็นเพื่อประเมินตัวดำเนินการนี้ ประเมิน. ส่งผลกลับไปที่กอง! และเกือบเสร็จแล้ว ทำไปเรื่อย ๆ จนกว่าสแต็กจะมีเพียงรายการเดียว = ค่าที่คุณกำลังมองหา

ลองทำ (1 + 2) * 3 ซึ่งอยู่ใน postfix คือ "12 + 3 *" อ่านตัวเลขแรก = 1 กดลงบนสแต็ก อ่านต่อไป Number = 2 กดลงบนสแต็ก อ่านต่อไป ตัวดำเนินการ. อันไหน? +. ชนิดไหน? Binary = ต้องการตัวถูกดำเนินการสองตัว ป๊อปสแต็กสองครั้ง = argright คือ 2 และ argleft คือ 1 1 + 2 คือ 3 ดัน 3 กลับไปที่สแต็ก อ่านถัดไปจากสตริง postfix มันเป็นตัวเลข 3. ดัน อ่านต่อไป ตัวดำเนินการ. อันไหน? *. ชนิดไหน? ไบนารี = ต้องการสองตัวเลข -> ป๊อปสแต็กสองครั้ง ปรากฏครั้งแรกใน argright ครั้งที่สองใน argleft ประเมินการดำเนินการ - 3 ครั้ง 3 คือ 9 กด 9 บนกอง อ่าน postfix ถ่านถัดไป มันเป็นโมฆะ สิ้นสุดการป้อนข้อมูล Pop stack onec = นั่นคือคำตอบของคุณ

3). Shunting Yard ใช้เพื่อเปลี่ยนนิพจน์ infix ที่อ่านได้ของมนุษย์ (อย่างง่าย) ให้เป็นนิพจน์ postfix (นอกจากนี้มนุษย์ยังสามารถอ่านได้ง่ายหลังจากการฝึกฝนบางอย่าง) ง่ายต่อการเขียนโค้ดด้วยตนเอง ดูความเห็นข้างบนและ net


4

มีภาษาที่คุณต้องการใช้หรือไม่? ANTLRจะช่วยให้คุณทำสิ่งนี้จากมุมมองของ Java เอเดรีย Kuhn มีดีเขียนขึ้นเกี่ยวกับวิธีการเขียนไวยากรณ์ที่ปฏิบัติการในรูบี; ในความเป็นจริงตัวอย่างของเขาเกือบจะตรงกับตัวอย่างนิพจน์เลขคณิตของคุณ


ฉันต้องยอมรับว่าตัวอย่างของฉันที่ให้ไว้ในบล็อกโพสต์นั้นมีการเรียกซ้ำด้านซ้ายผิดกล่าวคือ a - b - c ประเมินเป็น (a - (b -c)) แทน ((a -b) - c) อันที่จริงนั่นทำให้ฉันนึกถึงการเพิ่มสิ่งที่ต้องทำที่ฉันควรแก้ไขบทความในบล็อก
akuhn

4

ขึ้นอยู่กับว่า "ทั่วไป" คุณต้องการให้เป็นอย่างไร

ถ้าคุณต้องการให้มันเป็นแบบทั่วไปจริงๆเช่นสามารถแยกวิเคราะห์ฟังก์ชันทางคณิตศาสตร์ได้เช่นเดียวกับ sin (4 + 5) * cos (7 ^ 3) คุณอาจจะต้องมีต้นไม้แยกวิเคราะห์

ซึ่งฉันไม่คิดว่าการใช้งานที่สมบูรณ์จะเหมาะสมที่จะวางที่นี่ ผมขอแนะนำให้คุณลองดู " หนังสือมังกร " ที่น่าอับอายเล่มหนึ่ง

แต่ถ้าคุณต้องการเพียงแค่การสนับสนุนลำดับความสำคัญคุณสามารถทำได้โดยการแปลงนิพจน์เป็นรูปแบบ postfix ก่อนซึ่งอัลกอริทึมที่คุณสามารถคัดลอกและวางควรมีให้จากgoogleหรือฉันคิดว่าคุณสามารถเขียนโค้ดด้วยตัวเองด้วยไบนารี ต้นไม้.

เมื่อคุณมีในรูปแบบ postfix แล้วมันก็เป็นชิ้นส่วนของเค้กต่อจากนั้นเนื่องจากคุณเข้าใจแล้วว่าสแต็กช่วยอย่างไร


หนังสือมังกรอาจจะมากเกินไปสำหรับผู้ประเมินนิพจน์ - ตัวแยกวิเคราะห์การสืบเชื้อสายแบบเรียกซ้ำง่าย ๆ เป็นสิ่งที่จำเป็น แต่ต้องอ่านหากคุณต้องการทำอะไรที่ครอบคลุมมากขึ้นในคอมไพเลอร์
Eclipse

1
ว้าว - ดีใจที่ได้ทราบว่า "หนังสือมังกร" ยังคงมีการพูดคุยกันอยู่ ฉันจำได้ว่าเคยเรียนและอ่านมาตลอด - สมัยเรียนมหาวิทยาลัยเมื่อ 30 ปีก่อน
Schroedingers Cat

4

ผมจะแนะนำการโกงและการใช้แบ่งลานอัลกอริทึม เป็นวิธีง่ายๆในการเขียนโปรแกรมแยกวิเคราะห์ประเภทเครื่องคิดเลขอย่างง่ายและคำนึงถึงความสำคัญ

หากคุณต้องการโทเค็นสิ่งต่าง ๆ อย่างถูกต้องและมีตัวแปร ฯลฯ ที่เกี่ยวข้องฉันจะดำเนินการต่อและเขียนตัวแยกวิเคราะห์การสืบเชื้อสายซ้ำตามคำแนะนำของผู้อื่นที่นี่อย่างไรก็ตามหากคุณต้องการตัวแยกวิเคราะห์แบบเครื่องคิดเลขอัลกอริทึมนี้ก็น่าจะเพียงพอแล้ว :-)


4

ฉันพบสิ่งนี้ใน PIClist เกี่ยวกับอัลกอริทึม Shunting Yard :

แฮโรลด์เขียน:

ฉันจำได้ว่าเคยอ่านอัลกอริทึมที่แปลงนิพจน์พีชคณิตเป็น RPN เมื่อนานมาแล้วเพื่อการประเมินที่ง่าย ค่าอินฟิกซ์แต่ละตัวหรือตัวดำเนินการหรือวงเล็บถูกแทนด้วยรถรางบนราง รถประเภทหนึ่งแยกออกไปอีกทางหนึ่งและอีกประเภทหนึ่งขับตรงไปข้างหน้า ฉันจำรายละเอียดไม่ได้ (ชัด ๆ !) แต่คิดเสมอว่าการเขียนโค้ดมันน่าสนใจ ย้อนกลับไปตอนที่ฉันเขียนรหัสแอสเซมบลี 6800 (ไม่ใช่ 68000)

นี่คือ "shunting yard algorythm" และเป็นสิ่งที่ตัวแยกวิเคราะห์เครื่องส่วนใหญ่ใช้ ดูบทความเกี่ยวกับการแยกวิเคราะห์ใน Wikipedia วิธีง่ายๆในการเขียนโค้ด algorythm ลานแบ่งคือการใช้สองกอง หนึ่งคือสแต็ก "push" และอีกสแต็ก "ลด" หรือ "ผลลัพธ์" ตัวอย่าง:

pstack = () // ว่าง rstack = () อินพุต: 1 + 2 * 3 ลำดับความสำคัญ = 10 // ลดต่ำสุด = 0 // ไม่ลด

start: token '1': isnumber ใส่ pstack (push) token '+': isoperator set precedence = 2 if precedence <previous_operator_precedence then reduce () // ดูด้านล่างใส่ '+' ใน pstack (push) token '2' : isnumber ใส่ pstack (push) token '*': isoperator, set precedence = 1, put in pstack (push) // check precedence as // above token '3': isnumber, put in pstack (push) end of อินพุตจำเป็นต้องลด (เป้าหมายคือ pstack ว่างเปล่า) ลด () // เสร็จสิ้น

เพื่อลดองค์ประกอบป๊อปจาก push stack และใส่ลงในกองผลลัพธ์ให้สลับ 2 รายการบนสุดใน pstack เสมอหากอยู่ในรูปแบบ 'operator' 'number':

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

ถ้านิพจน์จะเป็น:

1 * 2 + 3

จากนั้นทริกเกอร์ลดจะเป็นการอ่านโทเค็น '+' ซึ่งมี precendece ต่ำกว่า '*' ที่ผลักไปแล้วดังนั้นจึงจะทำ:

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

จากนั้นกด '+' และ '3' จากนั้นจึงลดลงในที่สุด:

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

ดังนั้นเวอร์ชันสั้น ๆ คือ: หมายเลขพุชเมื่อตัวดำเนินการกดตรวจสอบลำดับความสำคัญของตัวดำเนินการก่อนหน้า ถ้ามันสูงกว่าตัวดำเนินการที่จะผลักตอนนี้ให้ลดก่อนแล้วดันตัวดำเนินการปัจจุบัน ในการจัดการ parens เพียงแค่บันทึกลำดับความสำคัญของตัวดำเนินการ 'ก่อนหน้า' และใส่เครื่องหมายบน pstack ที่บอกให้ลด algorythm เพื่อหยุดการลดเมื่อแก้ปัญหาด้านในของคู่พาเรน พาเรนปิดจะทริกเกอร์การลดลงเช่นเดียวกับการสิ้นสุดของอินพุตและยังลบเครื่องหมายพาเรนที่เปิดออกจาก pstack และเรียกคืนลำดับความสำคัญของ 'การดำเนินการก่อนหน้า' เพื่อให้การแยกวิเคราะห์สามารถดำเนินต่อไปได้หลังจากพาเรนปิดซึ่งค้างไว้ ซึ่งสามารถทำได้ด้วยการเรียกซ้ำหรือไม่มี (คำใบ้: ใช้สแต็กเพื่อจัดเก็บลำดับความสำคัญก่อนหน้านี้เมื่อพบกับ '(' ... ) เวอร์ชันทั่วไปของสิ่งนี้คือการใช้ตัวสร้างพาร์เซอร์ที่ใช้ shunting yard algorythm, f.ex. ใช้ yacc หรือ bison หรือ taccle (tcl analog ของ yacc)

ปีเตอร์

- อดัม


4

แหล่งข้อมูลอื่นสำหรับการแยกวิเคราะห์ลำดับความสำคัญคือรายการตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการในวิกิพีเดีย ครอบคลุมอัลกอริทึมการแบ่งหลาของ Dijkstra และอัลกอริธึมทางเลือกแบบต้นไม้ แต่จะครอบคลุมอัลกอริธึมการแทนที่มาโครที่เรียบง่ายจริงๆซึ่งสามารถนำไปใช้งานได้เล็กน้อยต่อหน้าตัวแยกวิเคราะห์ที่ไม่มีลำดับความสำคัญ:

#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

ฉันไม่เห็นประเด็น ทั้งหมดนี้คือการเปลี่ยนปัญหาการแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการให้เป็นปัญหาการแยกวิเคราะห์ในวงเล็บ
Marquis of Lorne

@EJP แน่นอน แต่ตัวแยกวิเคราะห์ในคำถามจัดการกับวงเล็บได้ดีดังนั้นนี่จึงเป็นวิธีแก้ปัญหาที่สมเหตุสมผล หากคุณมีโปรแกรมแยกวิเคราะห์ที่ไม่มีแสดงว่าคุณคิดถูกแล้วที่จะย้ายปัญหาไปยังพื้นที่อื่น
Adam Davis

4

ฉันได้โพสต์แหล่งที่มาของJava Math Evaluatorขนาดกะทัดรัดพิเศษ (1 คลาส <10 KiB) บนเว็บไซต์ของฉัน นี่คือตัวแยกวิเคราะห์การสืบเชื้อสายซ้ำประเภทที่ทำให้เกิดการระเบิดของกะโหลกสำหรับโปสเตอร์ของคำตอบที่ยอมรับ

สนับสนุนลำดับความสำคัญเต็มวงเล็บชื่อตัวแปรและฟังก์ชันอาร์กิวเมนต์เดี่ยว




2

ฉันกำลังทำงานเกี่ยวกับชุดบทความที่สร้างตัวแยกวิเคราะห์นิพจน์ทั่วไปเป็นเครื่องมือการเรียนรู้สำหรับรูปแบบการออกแบบและการเขียนโปรแกรมที่อ่านได้ คุณสามารถดูที่readablecode บทความนี้นำเสนอการใช้อัลกอริทึมการแบ่งหลาที่ชัดเจน


2

ฉันเขียน parser แสดงออกใน F # และblogged เกี่ยวกับที่นี่ มันใช้อัลกอริธึมการแบ่งหลา แต่แทนที่จะแปลงจาก infix เป็น RPN ฉันได้เพิ่มสแต็คที่สองเพื่อรวบรวมผลลัพธ์ของการคำนวณ จัดการกับลำดับความสำคัญของตัวดำเนินการได้อย่างถูกต้อง แต่ไม่สนับสนุนตัวดำเนินการแบบยูนารี ฉันเขียนสิ่งนี้เพื่อเรียนรู้ F # ไม่ใช่เพื่อเรียนรู้การแยกวิเคราะห์นิพจน์


2

วิธีการแก้ปัญหาโดยใช้ Python pyparsing สามารถพบได้ที่นี่ การแยกสัญกรณ์ infix กับตัวดำเนินการต่างๆที่มีลำดับความสำคัญเป็นเรื่องธรรมดาดังนั้นการแยกวิเคราะห์จึงรวมถึงตัวสร้างนิพจน์infixNotation(เดิมoperatorPrecedence) ด้วย ด้วยวิธีนี้คุณสามารถกำหนดนิพจน์บูลีนโดยใช้ "AND", "OR", "NOT" ได้อย่างง่ายดาย หรือคุณสามารถขยายเลขคณิตสี่ฟังก์ชันเพื่อใช้ตัวดำเนินการอื่น ๆ เช่น! สำหรับแฟกทอเรียลหรือ '%' สำหรับโมดูลัสหรือเพิ่มตัวดำเนินการ P และ C เพื่อคำนวณการเรียงสับเปลี่ยนและการรวมกัน คุณสามารถเขียนตัวแยกวิเคราะห์ infix สำหรับสัญกรณ์เมทริกซ์ซึ่งรวมถึงการจัดการตัวดำเนินการ '-1' หรือ 'T' (สำหรับการผกผันและทรานสโพส) ตัวดำเนินการตัวอย่างลำดับความสำคัญของตัวแยกวิเคราะห์ 4 ฟังก์ชัน (ด้วย '!'.


1

ฉันรู้ว่านี่เป็นคำตอบที่ล่าช้า แต่ฉันเพิ่งเขียนตัวแยกวิเคราะห์เล็ก ๆ ที่อนุญาตให้โอเปอเรเตอร์ทั้งหมด (คำนำหน้าโพสต์ฟิคและอินฟิกซ์ซ้ายอินฟิกซ์ขวาและไม่ใช่แอสโซซิเอทีฟ) มีลำดับความสำคัญตามอำเภอใจ

ฉันจะขยายสิ่งนี้สำหรับภาษาที่รองรับ DSL โดยพลการ แต่ฉันแค่อยากจะชี้ให้เห็นว่าไม่จำเป็นต้องมีตัวแยกวิเคราะห์ที่กำหนดเองสำหรับลำดับความสำคัญของตัวดำเนินการเราสามารถใช้ตัวแยกวิเคราะห์ทั่วไปที่ไม่ต้องการตารางเลยและ เพียงแค่ค้นหาลำดับความสำคัญของแต่ละตัวดำเนินการตามที่ปรากฏ ผู้คนพูดถึงตัวแยกวิเคราะห์แพรตต์แบบกำหนดเองหรือตัวแยกวิเคราะห์หลาที่สามารถรับอินพุตที่ผิดกฎหมาย - อันนี้ไม่จำเป็นต้องปรับแต่งและ (เว้นแต่จะมีข้อผิดพลาด) จะไม่ยอมรับอินพุตที่ไม่ถูกต้อง มันไม่สมบูรณ์ในแง่หนึ่งมันถูกเขียนขึ้นเพื่อทดสอบอัลกอริทึมและอินพุตอยู่ในรูปแบบที่จะต้องมีการประมวลผลล่วงหน้า แต่มีความคิดเห็นที่ทำให้ชัดเจน

โปรดทราบว่าตัวดำเนินการทั่วไปบางประเภทขาดหายไปเช่นประเภทของตัวดำเนินการที่ใช้ในการจัดทำดัชนีเช่น table [index] หรือเรียกฟังก์ชัน function (parameter-expression, ... ) ฉันจะเพิ่มสิ่งเหล่านี้ แต่คิดว่าทั้งสองเป็น postfix ตัวดำเนินการที่สิ่งที่อยู่ระหว่างตัวคั่น '[' และ ']' หรือ '(' และ ')' จะถูกแยกวิเคราะห์ด้วยอินสแตนซ์ที่แตกต่างกันของตัวแยกนิพจน์ ขออภัยที่ปล่อยไว้ แต่ส่วน postfix อยู่ในการเพิ่มส่วนที่เหลืออาจจะใหญ่กว่าโค้ดเกือบสองเท่า

เนื่องจากตัวแยกวิเคราะห์เป็นรหัสแร็กเก็ตเพียง 100 บรรทัดบางทีฉันควรวางไว้ที่นี่ฉันหวังว่าจะไม่นานเกินกว่าที่ stackoverflow อนุญาต

รายละเอียดเล็กน้อยเกี่ยวกับการตัดสินใจโดยพลการ:

หากตัวดำเนินการ postfix ที่มีลำดับความสำคัญต่ำกำลังแข่งขันกันเพื่อให้บล็อกอินฟิกซ์เดียวกันกับตัวดำเนินการคำนำหน้าต่ำตัวดำเนินการคำนำหน้าจะชนะ สิ่งนี้ไม่ได้เกิดขึ้นในภาษาส่วนใหญ่เนื่องจากส่วนใหญ่ไม่มีตัวดำเนินการ postfix ที่มีความสำคัญต่ำ - ตัวอย่างเช่น: ((data a) (left 1 +) (pre 2 not) (data b) (post 3!) (left 1 +) (data c)) คือ a + not b! + c โดยที่ไม่ใช่ a ตัวดำเนินการคำนำหน้าและ! เป็นตัวดำเนินการ postfix และทั้งคู่มีลำดับความสำคัญต่ำกว่า + ดังนั้นพวกเขาจึงต้องการจัดกลุ่มในรูปแบบที่เข้ากันไม่ได้ไม่ว่าจะเป็น (a + ไม่ใช่ b!) + c หรือเป็น + (ไม่ใช่ b! + c) ในกรณีเหล่านี้ตัวดำเนินการคำนำหน้าจะชนะเสมอดังนั้น ประการที่สองคือวิธีการแยกวิเคราะห์

Nonassociative infix ตัวดำเนินการมีอยู่จริงดังนั้นคุณจึงไม่ต้องแสร้งทำเป็นว่าโอเปอเรเตอร์ที่ส่งคืนประเภทต่างๆมากกว่าที่จะเข้ากันได้ดี แต่หากไม่มีประเภทนิพจน์ที่แตกต่างกันสำหรับแต่ละประเภทก็เป็น kludge ด้วยเหตุนี้ในอัลกอริทึมนี้ตัวดำเนินการที่ไม่เชื่อมโยงจึงปฏิเสธที่จะไม่เชื่อมโยงไม่เพียง แต่กับตัวเอง แต่กับตัวดำเนินการใด ๆ ที่มีลำดับความสำคัญเดียวกัน นั่นเป็นกรณีทั่วไปที่ <<= ==> = etc ไม่เชื่อมโยงกันในภาษาส่วนใหญ่

คำถามที่ว่าตัวดำเนินการประเภทต่างๆ (ด้านซ้ายคำนำหน้าและอื่น ๆ ) ทำลายความสัมพันธ์ในลำดับความสำคัญได้อย่างไรเป็นสิ่งที่ไม่ควรเกิดขึ้นเพราะมันไม่สมเหตุสมผลเลยที่จะให้ตัวดำเนินการประเภทต่างๆมีความสำคัญเหมือนกัน อัลกอริทึมนี้ทำอะไรบางอย่างในกรณีเหล่านั้น แต่ฉันไม่ได้กังวลที่จะเข้าใจว่าเพราะอะไรไวยากรณ์ดังกล่าวเป็นความคิดที่ไม่ดีในตอนแรก

#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.