เมื่อใดจึงจะใช้ Parser Combinator เมื่อใดจึงจะใช้ตัวแยกวิเคราะห์


59

ฉันได้ดำน้ำลึกเข้าไปในโลกของ parsers เมื่อเร็ว ๆ นี้ต้องการที่จะสร้างภาษาการเขียนโปรแกรมของตัวเอง

อย่างไรก็ตามฉันพบว่ามีวิธีการเขียนตัวแยกวิเคราะห์ที่แตกต่างกันสองวิธี: Parser Generators และ Parser Combinators

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

ภาพรวมอย่างง่าย:

Parser Generator

Parser Generator ใช้ไฟล์ที่เขียนใน DSL ซึ่งเป็นภาษาถิ่นของExtended Backus-Naurและแปลงให้เป็นซอร์สโค้ดที่สามารถ (เมื่อรวบรวม) กลายเป็น parser สำหรับภาษาที่ป้อนซึ่งอธิบายไว้ใน DSL นี้

ซึ่งหมายความว่ากระบวนการรวบรวมจะทำในสองขั้นตอนแยกกัน น่าสนใจ Parser Generators เองก็เป็นคอมไพเลอร์ด้วย (และหลายคนก็เป็นโฮสติ้งของตัวเอง )

Parser Combinator

Parser Combinator อธิบายฟังก์ชั่นง่าย ๆ ที่เรียกว่าparsersที่รับอินพุตเป็นพารามิเตอร์และพยายามดึงตัวอักษรแรกของอินพุตนี้หากตรงกัน พวกเขาส่งคืน tuple (result, rest_of_input)ซึ่งresultอาจว่างเปล่า (เช่นnilหรือNothing) ถ้า parser ไม่สามารถแยกวิเคราะห์อะไรจากอินพุตนี้ ตัวอย่างจะเป็นโปรแกรมdigitแยกวิเคราะห์ ตัวแยกวิเคราะห์อื่น ๆ สามารถใช้ตัวแยกวิเคราะห์เป็นอาร์กิวเมนต์แรก (อาร์กิวเมนต์สุดท้ายยังคงอยู่ในสตริงอินพุต) เพื่อรวม : เช่นmany1พยายามจับคู่ตัวแยกวิเคราะห์อื่น ๆ หลายครั้งมากที่สุดเท่าที่จะทำได้ (แต่อย่างน้อยหนึ่งครั้ง

ตอนนี้คุณสามารถรวม (เขียน) digitและmany1สร้าง parser ใหม่ได้integerแล้ว

นอกจากนี้ยังchoiceสามารถเขียนโปรแกรมแยกวิเคราะห์ระดับสูงขึ้นซึ่งใช้รายการตัวแยกวิเคราะห์ลองใช้ตัวแยกวิเคราะห์แต่ละตัว

ด้วยวิธีนี้สามารถสร้าง lexers / parsers ที่ซับซ้อนได้ ในภาษาที่รองรับการบรรทุกเกินพิกัดผู้ใช้งานจะมีลักษณะเหมือน EBNF มากแม้ว่ามันจะเขียนโดยตรงในภาษาเป้าหมาย (และคุณสามารถใช้คุณสมบัติทั้งหมดของภาษาเป้าหมายที่คุณต้องการ)

ความแตกต่างง่าย ๆ

ภาษา:

  • Parser Generators ถูกเขียนด้วย EBNF-ish DSL และรหัสที่ข้อความเหล่านี้ควรสร้างขึ้นเมื่อตรงกัน
  • Parser Combinators เขียนเป็นภาษาเป้าหมายโดยตรง

Lexing / แยก:

  • Parser Generators มีความแตกต่างอย่างมากระหว่าง 'lexer' (ซึ่งแยกสตริงเป็นโทเค็นที่อาจติดแท็กเพื่อแสดงประเภทของค่าที่เราติดต่อด้วย) และ 'parser' (ซึ่งใช้รายการเอาต์พุตของโทเค็นจาก lexer และพยายามที่จะรวมพวกเขาไว้เป็นต้นไม้ไวยากรณ์นามธรรม)
  • ตัวแยกวิเคราะห์ Parser ไม่จำเป็นต้องมีความแตกต่างนี้ โดยปกติแล้วตัวแยกวิเคราะห์อย่างง่ายจะทำงานของ 'lexer' และตัวแยกวิเคราะห์ระดับสูงจะเรียกสิ่งที่ง่ายกว่าเหล่านี้เพื่อตัดสินใจว่า AST-node ชนิดใดที่จะสร้าง

คำถาม

อย่างไรก็ตามถึงแม้จะมีความแตกต่างเหล่านี้ (และนี่คือรายการของความแตกต่างอาจจะยังไม่เสร็จสมบูรณ์!) ฉันไม่สามารถเลือกได้ว่าจะใช้เมื่อใด ฉันไม่สามารถเห็นความหมาย / ผลที่ตามมาของความแตกต่างเหล่านี้ได้

คุณสมบัติของปัญหาใดที่บ่งบอกว่าปัญหาจะแก้ไขได้ดีขึ้นโดยใช้ Parser Generator? คุณสมบัติของปัญหาใดที่บ่งบอกว่าปัญหาจะได้รับการแก้ไขได้ดีขึ้นและใช้ Parser Combinator


4
มีอย่างน้อยสองวิธีในการติดตั้ง parser ที่คุณไม่ได้กล่าวถึง: parser interpreters (คล้ายกับ parser generators ยกเว้นแทนที่จะรวบรวมภาษา parser เป็น C หรือ Java ภาษา parser จะถูกดำเนินการโดยตรง) และเพียงเขียน parser ด้วยมือ การเขียนโปรแกรมแยกวิเคราะห์ด้วยมือเป็นรูปแบบที่ต้องการสำหรับการนำไปใช้ในอุตสาหกรรมการผลิตที่ทันสมัยพร้อมกับการใช้ภาษาที่เข้มแข็ง (เช่น GCC, Clang javac, Scala) มันช่วยให้คุณควบคุมสถานะ parser ภายในได้มากที่สุดซึ่งช่วยในการสร้างข้อความแสดงข้อผิดพลาดที่ดี (ซึ่งในปีที่ผ่านมา ...
Jörg W Mittag

3
…ได้กลายเป็นสิ่งสำคัญอันดับต้น ๆ สำหรับผู้พัฒนาภาษา) นอกจากนี้ตัวแยกวิเคราะห์ / ตัวแปล / ตัวรวมคำสั่งที่มีอยู่จำนวนมากไม่ได้ถูกออกแบบมาเพื่อรับมือกับความต้องการที่หลากหลายซึ่งการใช้ภาษาที่ทันสมัยต้องทำให้สำเร็จ เช่นการใช้ภาษาสมัยใหม่จำนวนมากใช้โค้ดเดียวกันสำหรับการแบตช์การคอมไพล์แบ็คกราวน์ IDE, การไฮไลต์ไวยากรณ์, การรีแฟคเตอร์อัตโนมัติ, การกรอกรหัสอัจฉริยะ, การสร้างเอกสารอัตโนมัติ, การสร้างเอกสารอัตโนมัติ, การสร้างอัตโนมัติ . เครื่องมือแยกวิเคราะห์ที่มีอยู่จำนวนมาก…
Jörg W Mittag

1
... กรอบไม่ยืดหยุ่นพอที่จะจัดการกับเรื่องนั้น โปรดสังเกตว่ามีตัวแยกวิเคราะห์เฟรมเวิร์กที่ไม่ได้ยึดตาม EBNF เช่นparsers packrat สำหรับการแยกไวยากรณ์การแสดงออก
Jörg W Mittag

2
ฉันคิดว่ามันหนักขึ้นอยู่กับภาษาที่คุณพยายามรวบรวม มันคือประเภทอะไร (LR, ... )?
qwerty_so

1
ข้อสันนิษฐานข้างต้นของคุณอิงตาม BNF ซึ่งโดยทั่วไปจะเรียบเรียงด้วยชุดค่าผสม lexer / LR แต่ภาษาไม่จำเป็นต้องยึดตามไวยากรณ์ของ LR ดังนั้นคุณวางแผนที่จะรวบรวมอะไร
qwerty_so

คำตอบ:


59

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

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

การจัดแสดงนี้มีความยาว แต่สำคัญ ทนกับฉัน (หรือถ้าคุณใจร้อนให้เลื่อนไปจนจบเพื่อดูผังงาน)


เพื่อทำความเข้าใจความแตกต่างระหว่าง Parser Combinators และ Parser Generators อันดับแรกต้องเข้าใจความแตกต่างระหว่างการแยกวิเคราะห์ที่มีอยู่

วจีวิภาค

การแยกเป็นกระบวนการของการวิเคราะห์สตริงของสัญลักษณ์ตามไวยากรณ์อย่างเป็นทางการ (ในวิทยาศาสตร์คอมพิวเตอร์) การแยกวิเคราะห์ใช้เพื่อให้คอมพิวเตอร์เข้าใจข้อความที่เขียนในภาษาโดยปกติแล้วจะสร้างทรีแยกวิเคราะห์ที่แทนข้อความที่เขียนเก็บความหมายของส่วนที่แตกต่างกันในแต่ละโหนดของต้นไม้ ทรีแยกวิเคราะห์นี้สามารถใช้สำหรับวัตถุประสงค์ที่แตกต่างหลากหลายเช่นการแปลเป็นภาษาอื่น (ใช้ในคอมไพเลอร์จำนวนมาก) ตีความคำแนะนำที่เขียนเป็นลายลักษณ์อักษรโดยตรงในบางวิธี (SQL, HTML) ทำให้เครื่องมือเช่นLinters ทำงานได้ เป็นต้นบางครั้งต้นไม้แยกไม่ชัดเจนสร้างขึ้น แต่เป็นการดำเนินการที่ควรดำเนินการในแต่ละประเภทของโหนดในทรีจะถูกดำเนินการโดยตรง สิ่งนี้จะเพิ่มประสิทธิภาพ แต่ใต้ต้นไม้ยังคงมีต้นไม้แจงโดยนัยอยู่

การแยกวิเคราะห์เป็นปัญหาที่ยากต่อการคำนวณ มีการวิจัยมากกว่าห้าสิบปีในเรื่องนี้ แต่ยังมีอีกมากที่จะเรียนรู้

พูดโดยทั่วไปมีสี่อัลกอริทึมทั่วไปเพื่อให้คอมพิวเตอร์แยกวิเคราะห์อินพุต:

  • การแยก LL (ไม่มีการแยกวิเคราะห์บริบทจากบนลงล่าง)
  • การแยก LR (ไม่มีการแยกบริบทจากล่างขึ้นบน)
  • PEG + Packrat การแยกวิเคราะห์
  • แยกวิเคราะห์ Earley

โปรดทราบว่าการแยกวิเคราะห์ประเภทนี้เป็นคำอธิบายทั่วไปทางทฤษฎี มีหลายวิธีในการใช้อัลกอริธึมเหล่านี้กับเครื่องจริงโดยมีการแลกเปลี่ยนที่แตกต่างกัน

LL และ LR สามารถดูได้เฉพาะไวยากรณ์ที่ไม่มีบริบท (นั่นคือบริบทรอบโทเค็นที่เขียนขึ้นนั้นไม่สำคัญที่จะเข้าใจว่ามีการใช้งานอย่างไร)

PEG / Packrat การแยกวิเคราะห์และการแยกวิเคราะห์Earleyใช้น้อยกว่ามาก: Earley-parsing นั้นดีมากที่สามารถจัดการกับไวยากรณ์ได้มากขึ้น (รวมถึงที่ไม่จำเป็นต้องมีบริบท) แต่มีประสิทธิภาพน้อยกว่า (อ้างสิทธิ์โดยมังกร หนังสือ (ส่วน 4.1.1) ฉันไม่แน่ใจว่าการอ้างสิทธิ์เหล่านี้ยังคงถูกต้องหรือไม่ การแยกวิเคราะห์ Expression Grammar + Packrat-parsingเป็นวิธีที่ค่อนข้างมีประสิทธิภาพและยังสามารถจัดการไวยากรณ์ได้มากกว่า LL และ LR แต่ซ่อนความกำกวมซึ่งจะสัมผัสได้อย่างรวดเร็วที่ด้านล่าง

LL (จากซ้ายไปขวา, ซ้ายสุดมา)

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

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

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

LR (จากซ้ายไปขวา, ขวาสุดมา)

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

โปรแกรมถูกต้องหากในตอนท้ายเราจะจบลงด้วยโหนดเดียวบนสแต็กซึ่งแสดงถึงกฎการเริ่มต้นจากไวยากรณ์ของเรา

มองไปข้างหน้า

ในทั้งสองระบบนี้บางครั้งจำเป็นต้องมองโทเค็นเพิ่มเติมจากอินพุตก่อนจึงจะสามารถตัดสินใจได้ว่าจะเลือกตัวเลือกใด นี่คือ(0), (1), (k)หรือ(*)-syntax คุณเห็นหลังจากที่ชื่อของทั้งสองขั้นตอนวิธีการทั่วไปเช่นหรือLR(1) มักจะหมายถึง 'เท่าที่คุณต้องการไวยากรณ์' ในขณะที่มักจะหมายถึง 'parser นี้ทำการ backtracking' ซึ่งมีประสิทธิภาพมากขึ้น / ใช้งานง่าย แต่มีหน่วยความจำและการใช้เวลาสูงกว่า parser ที่สามารถแยกวิเคราะห์ อย่างเป็นเส้นตรงLL(k)k*

โปรดทราบว่าตัวแยกวิเคราะห์สไตล์ LR มีโทเค็นจำนวนมากบนสแต็กเมื่อพวกเขาอาจตัดสินใจที่จะ 'มองไปข้างหน้า' ดังนั้นพวกเขาจึงมีข้อมูลเพิ่มเติมที่จะจัดส่ง ซึ่งหมายความว่าพวกเขามักจะต้อง 'lookahead' น้อยกว่าตัวแยกวิเคราะห์แบบ LL สำหรับไวยากรณ์เดียวกัน

LL กับ LR: ความกำกวม

เมื่ออ่านคำอธิบายทั้งสองข้างต้นเราอาจสงสัยว่าทำไมการแยกวิเคราะห์ลักษณะ LR มีอยู่เนื่องจากการแยกวิเคราะห์ลักษณะ LL นั้นดูเป็นธรรมชาติมากขึ้น

อย่างไรก็ตามการแยก LL-สไตล์มีปัญหา: ซ้าย Recursion

เป็นเรื่องธรรมดามากที่จะเขียนไวยากรณ์เช่น:

expr ::= expr '+' expr | term term ::= integer | float

แต่ตัวแยกวิเคราะห์สไตล์ LL จะติดอยู่ในวนวนซ้ำแบบวนซ้ำเมื่อแยกวิเคราะห์ไวยากรณ์นี้: เมื่อลองใช้ความเป็นไปได้ที่เหลืออยู่มากที่สุดของexprกฎมันจะย้อนกลับไปที่กฎนี้อีกครั้ง

มีวิธีแก้ไขปัญหานี้ วิธีที่ง่ายที่สุดคือการเขียนไวยากรณ์ของคุณใหม่เพื่อให้การเรียกซ้ำแบบนี้ไม่เกิดขึ้นอีก:

expr ::= term expr_rest expr_rest ::= '+' expr | ϵ term ::= integer | float (ที่นี่ϵหมายถึง 'สตริงว่าง')

ไวยากรณ์นี้ตอนนี้ถูกเรียกซ้ำ โปรดทราบว่ามันเป็นการยากที่จะอ่านในทันที

ในทางปฏิบัติการเรียกซ้ำทางซ้ายอาจเกิดขึ้นทางอ้อมกับขั้นตอนอื่น ๆ อีกมากมายในระหว่างนั้น ทำให้เป็นปัญหาที่ยากที่จะระวัง แต่การพยายามแก้มันทำให้ไวยากรณ์ของคุณอ่านยากขึ้น

ตามมาตรา 2.5 ของ Dragon Book ระบุว่า:

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

ตัวแยกวิเคราะห์สไตล์ LR ไม่มีปัญหาของการวนซ้ำทางซ้ายนี้เนื่องจากสร้างต้นไม้ขึ้นจากล่างขึ้นบน อย่างไรก็ตามการแปลทางใจของไวยากรณ์เช่นด้านบนเป็นตัวแยกวิเคราะห์สไตล์ LR (ซึ่งมักจะถูกนำมาใช้ในฐานะเครื่องจักรออโตไฟไนต์ - รัฐ )
นั้นยากมาก (และผิดพลาดได้ง่าย) ที่จะทำเช่นนั้นบ่อยครั้ง การเปลี่ยนสถานะที่จะต้องพิจารณา นี่คือเหตุผลที่ตัวแยกวิเคราะห์สไตล์ LR มักจะสร้างโดย Parser Generator ซึ่งรู้จักกันในชื่อ 'คอมไพเลอร์คอมไพเลอร์'

วิธีแก้ไขความคลุมเครือ

เราเห็นสองวิธีในการแก้ไขความกำกวมแบบเรียกซ้ำซากซ้าย: 1) เขียนไวยากรณ์ใหม่ 2) ใช้ LR-parser

แต่มีความคลุมเครือประเภทอื่นที่แก้ได้ยากกว่า: จะเกิดอะไรขึ้นถ้ากฎสองข้อที่แตกต่างกันสามารถใช้งานได้พร้อมกัน?

ตัวอย่างทั่วไป ได้แก่ :

ทั้งตัวแยกวิเคราะห์สไตล์ LL และ LR มีปัญหากับสิ่งเหล่านี้ ปัญหาเกี่ยวกับการแยกวิเคราะห์ทางคณิตศาสตร์นิพจน์สามารถแก้ไขได้โดยการแนะนำตัวดำเนินการลำดับความสำคัญ ในทำนองเดียวกันปัญหาอื่น ๆ เช่น Dangling Else สามารถแก้ไขได้โดยเลือกพฤติกรรมที่มีความสำคัญมาก่อนและยึดติดกับมัน (ใน C / C ++ เช่น dangling else จะเป็นของ 'if' ที่ใกล้เคียงที่สุดเสมอ)

'การแก้ปัญหา' อื่นสำหรับเรื่องนี้คือการใช้ Parser Expression Grammar (PEG): สิ่งนี้คล้ายกับ BNF-grammar ที่ใช้ด้านบน แต่ในกรณีที่มีความกำกวมให้เลือก 'ตัวแรก' เสมอ แน่นอนว่านี่ไม่ได้ 'แก้ปัญหา' แต่ซ่อนไว้ว่ามีความคลุมเครือจริง ๆ : ผู้ใช้อาจไม่รู้ว่าตัวแยกวิเคราะห์ตัวเลือกใดและสิ่งนี้อาจนำไปสู่ผลลัพธ์ที่ไม่คาดคิด

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

50 ปีของการวิจัย

แต่ชีวิตดำเนินต่อไป มันกลับกลายเป็นว่า 'การแยกวิเคราะห์สไตล์ LR ปกติ' ถูกนำมาใช้เนื่องจากออโตเมติกสถานะที่ จำกัด มักต้องการการเปลี่ยนสถานะเป็นพัน ๆ ตัว + ซึ่งเป็นปัญหาในขนาดของโปรแกรม ดังนั้นตัวแปรต่างๆเช่นSimple LR (SLR) และLALR (Look-ahead LR) จึงถูกเขียนขึ้นซึ่งรวมเทคนิคอื่น ๆ เพื่อทำให้ออโตเมติกมีขนาดเล็กลงลดขนาดดิสก์และหน่วยความจำของโปรแกรมวิเคราะห์คำ

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

ที่น่าสนใจหลังจากอธิบายอัลกอริธึมLR ทั่วไปมันกลับกลายเป็นว่าวิธีที่คล้ายกันสามารถนำมาใช้ในการใช้งานตัวแยกวิเคราะห์ LL ทั่วไปซึ่งเร็วเหมือนกัน ($ O (n ^ 3) ความซับซ้อนของเวลา $ $ สำหรับไวยากรณ์คลุมเครือ, $ O (n) $ สำหรับแกรมม่าที่ไม่คลุมเครืออย่างสมบูรณ์แม้ว่าจะมีการทำบัญชีมากกว่าตัวแยกวิเคราะห์ LR (LA) อย่างง่ายซึ่งหมายถึงปัจจัยคงที่ที่สูงขึ้น) แต่อนุญาตให้ parser เขียนอีกครั้งในลักษณะวนซ้ำ (บนลงล่าง) ที่เป็นธรรมชาติมากขึ้น เพื่อเขียนและแก้ปัญหา

Parser Combinators, Parser Generators

ดังนั้นด้วยการอธิบายที่ยาวนานนี้เราจึงมาถึงใจกลางของคำถาม:

อะไรคือความแตกต่างของ Parser Combinators และ Parser Generators และควรใช้กับอีกอันเมื่อใด?

พวกมันเป็นสัตว์ต่างชนิดกันจริง ๆ :

Parser combinatorsถูกสร้างขึ้นเพราะคนเขียน parsers จากบนลงล่างและตระหนักว่าหลายเหล่านี้มีจำนวนมากในการร่วมกัน

Parser Generatorsถูกสร้างขึ้นเพราะคนต้องการสร้าง parsers ที่ไม่มีปัญหาที่ตัวแยกวิเคราะห์แบบ LL มี (เช่นตัวแยกแบบ LR) ซึ่งพิสูจน์ได้ยากมากด้วยมือ คนทั่วไปรวมถึง Yacc / Bison ที่ใช้ (LA) LR)

ที่น่าสนใจในปัจจุบันภูมิทัศน์ค่อนข้างสับสน:

  • เป็นไปได้ที่จะเขียนตัวแยกวิเคราะห์ Parser ที่ทำงานกับอัลกอริทึมGLLแก้ไขปัญหาความกำกวมที่ตัวแยกวิเคราะห์สไตล์ LL ดั้งเดิมมีในขณะที่สามารถอ่าน / เข้าใจได้เช่นเดียวกับการแยกวิเคราะห์จากบนลงล่างทุกชนิด

  • Parser Generators สามารถเขียนสำหรับ parsers แบบ LL ได้ ANTLRทำสิ่งนั้นอย่างแน่นอนและใช้การวิเคราะห์พฤติกรรมอื่น (Adaptive LL (*)) เพื่อแก้ไขความคลุมเครือที่ parsers สไตล์ LL แบบดั้งเดิมมี

โดยทั่วไปแล้วการสร้างตัวแยกวิเคราะห์ LR และการดีบักเอาต์พุตของตัวแยกวิเคราะห์แบบ LA (LR) ตัวแยกวิเคราะห์ที่ทำงานบนไวยากรณ์ของคุณนั้นเป็นเรื่องยากเนื่องจากการแปลไวยากรณ์ดั้งเดิมของคุณเป็นรูปแบบ LR บนมืออื่น ๆ , เครื่องมือเช่น Yacc / กระทิงมีหลายปีของ optimisations และเห็นมากของการใช้งานในป่าซึ่งหมายความว่าตอนนี้หลาย ๆ คนคิดว่ามันเป็นวิธีที่จะทำแยกและการสงสัยต่อแนวทางใหม่ ๆ

คุณควรใช้อันไหนขึ้นอยู่กับว่าไวยากรณ์ของคุณยากแค่ไหนและ parser ต้องรวดเร็วแค่ไหน ขึ้นอยู่กับไวยากรณ์หนึ่งในเทคนิคเหล่านี้ (/ การใช้งานของเทคนิคที่แตกต่างกัน) อาจเร็วกว่ามีรอยขนาดเล็กของหน่วยความจำมีขนาดของดิสก์ที่เล็กกว่าหรือขยายได้มากขึ้นหรือง่ายต่อการดีบักมากกว่าวิธีอื่น ไมล์สะสมของคุณอาจแตกต่างกันไป

หมายเหตุด้าน: ในเรื่องของการวิเคราะห์คำศัพท์

การวิเคราะห์คำศัพท์สามารถใช้ทั้งสำหรับ Parser Combinators และ Parser Generators แนวคิดคือมีตัวแยกวิเคราะห์ 'ใบ้' ที่ใช้งานง่าย (และรวดเร็ว) ที่ดำเนินการส่งรหัสผ่านครั้งแรกผ่านซอร์สโค้ดของคุณการลบเช่นการทำซ้ำช่องว่างสีขาวความคิดเห็น ฯลฯ และอาจ 'tokenizing' อย่างมาก วิธีหยาบองค์ประกอบต่าง ๆ ที่ประกอบขึ้นเป็นภาษาของคุณ

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

lexer ในท้ายที่สุดคือ 'just' parser อื่นและสามารถใช้งานได้โดยใช้เทคนิคใด ๆ ข้างต้น เนื่องจากความเรียบง่ายมักใช้เทคนิคอื่นมากกว่าการแยกวิเคราะห์หลักและสำหรับ 'lexer generators' ที่มีอยู่เป็นพิเศษ


tl; ดร:

นี่คือแผนผังลำดับงานที่ใช้กับกรณีส่วนใหญ่: ป้อนคำอธิบายรูปภาพที่นี่


@ Joeerd แน่นอนว่ามันเป็นข้อความจำนวนมากเพราะมันกลายเป็นปัญหาที่ยากมาก หากคุณรู้วิธีที่ฉันสามารถทำให้ย่อหน้าสุดท้ายชัดเจนยิ่งขึ้นฉันทุกคนหู: "อันไหนที่คุณควรใช้ขึ้นอยู่กับว่าไวยากรณ์ของคุณหนักแค่ไหนและ parser ต้องรวดเร็วแค่ไหนขึ้นอยู่กับไวยากรณ์ หนึ่งในเทคนิคเหล่านี้ (/ การใช้งานของเทคนิคที่แตกต่างกัน) อาจเร็วกว่ามีรอยเท้าหน่วยความจำขนาดเล็กกว่ามีรอยเท้าของดิสก์ที่เล็กลงหรือยืดขยายได้ง่ายกว่าหรือดีกว่าการดีบักกว่าระยะทางอื่น ๆ
Qqwy

1
คำตอบอื่นนั้นสั้นกว่าและชัดเจนกว่ามากและทำงานได้ดีกว่ามากในการตอบคำถาม
Sjoerd

1
@ Joeerd เหตุผลที่ฉันเขียนคำตอบนี้เป็นเพราะคำตอบอื่น ๆ กำลังทำให้เกิดปัญหามากเกินไปโดยนำเสนอคำตอบบางส่วนเป็นคำตอบแบบเต็มและ / หรือตกลงไปในกับดักที่เข้าใจผิดโดยสังเขป คำตอบข้างต้นคือการรวมตัวของการอภิปรายJörg W Mittag, Thomas Killian และฉันมีความคิดเห็นในคำถามหลังจากเข้าใจสิ่งที่พวกเขาพูดถึงและนำเสนอโดยไม่ต้องมีความรู้มาก่อน
Qqwy

ไม่ว่าในกรณีใดฉันได้เพิ่ม tl; dr flowchart ให้กับคำถาม @Sjoerd นั่นทำให้คุณพึงพอใจไหม?
Qqwy

2
Parser Combinators ล้มเหลวในการแก้ปัญหาเมื่อคุณไม่ได้ใช้งาน มีผู้รวมตัวกันมากกว่าเพียงแค่|นั้นแหละ การเขียนซ้ำที่ถูกต้องexprนั้นเป็นเรื่องที่ซับซ้อนยิ่งขึ้นexpr = term 'sepBy' "+"(โดยที่เครื่องหมายคำพูดเดี่ยวที่นี่เป็นการทดแทนสำหรับ backticks เพื่อเปลี่ยนฟังก์ชั่นมัดเนื่องจาก mini-markdown ไม่มีการหลบหนีอักขระ) ในกรณีทั่วไปมากขึ้นมีchainBycombinator เช่นกัน ฉันรู้ว่ามันเป็นเรื่องยากที่จะหางานการแยกวิเคราะห์อย่างง่าย ๆ เป็นตัวอย่างที่ไม่เหมาะกับพีซี
Steven Armstrong

8

สำหรับอินพุตที่รับประกันว่าไม่มีข้อผิดพลาดทางไวยากรณ์หรือที่ผ่าน / ไม่ผ่านเกี่ยวกับความถูกต้องทางวากยสัมพันธ์นั้นโอเคตัวแยกวิเคราะห์ parser นั้นทำงานได้ง่ายกว่ามากโดยเฉพาะในภาษาโปรแกรมที่ใช้งานได้ นี่คือสถานการณ์เช่นตัวต่อการเขียนโปรแกรมการอ่านไฟล์ข้อมูล ฯลฯ

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

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


2
ขอบคุณสำหรับคำตอบ! ทำไมการสร้างข้อความแสดงข้อผิดพลาดที่อ่านได้ง่ายขึ้นโดยใช้ Parser Generator มากกว่า Parser Combinator (ไม่ว่าเราจะพูดถึงการนำไปใช้สิ่งใดโดยเฉพาะ) ตัวอย่างเช่นฉันรู้ว่าทั้ง Parsec และ Spirit มีฟังก์ชันการทำงานเพื่อพิมพ์ข้อความแสดงข้อผิดพลาดรวมถึงข้อมูลบรรทัด + คอลัมน์ดังนั้นจึงเป็นไปได้ที่จะทำเช่นนี้ใน Parser Combinators เช่นกัน
Qqwy

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

ด้วย Parser Combinator ตามคำจำกัดความทั้งหมดที่คุณจะได้รับในเงื่อนไขข้อผิดพลาดคือ "เริ่มต้นที่จุดนี้ไม่พบอินพุตที่ถูกกฎหมาย" สิ่งนี้ไม่ได้บอกคุณว่ามีอะไรผิดปกติ ตามทฤษฎีแล้วโปรแกรมแยกวิเคราะห์ส่วนบุคคลที่เรียก ณ จุดนั้นสามารถบอกคุณได้ว่ามันคาดหวังอะไรและไม่พบ แต่สิ่งที่คุณทำได้คือพิมพ์สิ่งนั้นออกมาเพื่อให้เกิดข้อผิดพลาด looooooong
John R. Strohm

1
เครื่องมือสร้าง Parser ไม่ทราบว่าข้อความผิดพลาดที่ดีนั้นตรงไปตรงมาหรือไม่
เส้นทาง Miles

ไม่ใช่โดยค่าเริ่มต้นไม่ใช่ แต่มี hooks ที่สะดวกกว่าสำหรับการเพิ่มข้อความแสดงข้อผิดพลาดที่ดี
Karl Bielefeldt

4

Sam Harwell หนึ่งในผู้ดูแลเครื่องกำเนิดไฟฟ้า ANTLR parser เพิ่งเขียน :

ฉันพบ [combinators] ไม่ตรงกับความต้องการของฉัน:

  1. ANTLR มอบเครื่องมือสำหรับจัดการสิ่งต่าง ๆ เช่นความคลุมเครือให้ฉัน ในระหว่างการพัฒนามีเครื่องมือที่สามารถแสดงผลลัพธ์การแยกวิเคราะห์ที่ไม่ชัดเจนดังนั้นฉันจึงสามารถกำจัดความคลุมเครือเหล่านั้นในไวยากรณ์ ที่รันไทม์ฉันสามารถยกระดับความกำกวมที่เกิดจากการป้อนข้อมูลที่ไม่สมบูรณ์ใน IDE เพื่อสร้างผลลัพธ์ที่แม่นยำยิ่งขึ้นในฟีเจอร์เช่นการทำให้โค้ดสมบูรณ์
  2. ในทางปฏิบัติฉันพบว่าตัวแยกวิเคราะห์คำไม่เหมาะสำหรับการบรรลุเป้าหมายการแสดงของฉัน ส่วนหนึ่งของสิ่งนี้กลับไป
  3. เมื่อใช้การแยกวิเคราะห์ผลลัพธ์สำหรับคุณลักษณะต่าง ๆ เช่นการสรุปการทำให้โค้ดเสร็จสมบูรณ์และการเยื้องแบบสมาร์ทมันง่ายสำหรับการเปลี่ยนแปลงไวยากรณ์เล็กน้อยเพื่อส่งผลต่อความแม่นยำของผลลัพธ์เหล่านั้น ANTLR จัดเตรียมเครื่องมือที่สามารถเปลี่ยนข้อผิดพลาดเหล่านี้ให้เป็นข้อผิดพลาดในการคอมไพล์แม้ในกรณีที่ประเภทจะรวบรวมเป็นอย่างอื่น ฉันสามารถสร้างความมั่นใจต้นแบบคุณลักษณะภาษาใหม่ที่มีผลต่อไวยากรณ์โดยรู้ว่ารหัสเพิ่มเติมทั้งหมดที่เป็น IDE จะให้ประสบการณ์ที่สมบูรณ์สำหรับคุณลักษณะใหม่ตั้งแต่เริ่มต้น My fork of ANTLR 4 (ซึ่งเป็นเป้าหมายของ C # นั้นอิงตาม) เป็นเครื่องมือเดียวที่ฉันรู้ว่าแม้จะพยายามให้ฟีเจอร์นี้

โดยพื้นฐานแล้ว combinators ของ parser เป็นของเล่นที่น่าเล่น แต่มันก็ไม่ได้ถูกตัดออกสำหรับการทำงานหนัก


3

ในฐานะที่เป็นคาร์ลกล่าวถึงตัวแยกวิเคราะห์มักจะมีการรายงานข้อผิดพลาดที่ดีกว่า นอกจากนี้:

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

ในทางกลับกัน combinators 'มีข้อได้เปรียบของตัวเอง:

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

เครื่องสร้าง Parser มีแนวโน้มที่จะรายงานข้อผิดพลาดที่น่ากลัวเมื่อเทียบกับตัวแยกวิเคราะห์ LL recursive-descent ที่ใช้ในโลกแห่งความเป็นจริง เครื่องกำเนิดไฟฟ้า Parser ไม่ค่อยเสนอตะขอการเปลี่ยนแปลงตารางสถานะที่จำเป็นในการเพิ่มการวินิจฉัยที่ดี นี่คือเหตุผลที่คอมไพเลอร์ตัวจริงเกือบทั้งหมดไม่ได้ใช้ตัวแยกวิเคราะห์หรือตัวแยกวิเคราะห์ LL parsers ที่เหมาะสมแบบเรียกซ้ำได้นั้นสร้างได้เล็กน้อยแม้ว่าจะไม่ใช่พีซี / PG ที่ "สะอาด" แต่ก็มีประโยชน์มากกว่า
dhchdhd
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.