ฉันได้ทำการวิจัยจำนวนมากในช่วงสองสามวันที่ผ่านมาเพื่อทำความเข้าใจให้ดีขึ้นว่าทำไมเทคโนโลยีเหล่านี้จึงมีอยู่และจุดแข็งและจุดอ่อนของพวกเขาคืออะไร
คำตอบที่มีอยู่แล้วบางส่วนบอกเป็นนัยถึงความแตกต่างบางอย่าง แต่พวกเขาไม่ได้ให้ภาพที่สมบูรณ์และดูเหมือนว่าจะได้รับการวิพากษ์วิจารณ์บ้างซึ่งเป็นสาเหตุที่คำตอบนี้ถูกเขียนขึ้น
การจัดแสดงนี้มีความยาว แต่สำคัญ ทนกับฉัน (หรือถ้าคุณใจร้อนให้เลื่อนไปจนจบเพื่อดูผังงาน)
เพื่อทำความเข้าใจความแตกต่างระหว่าง 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; ดร:
นี่คือแผนผังลำดับงานที่ใช้กับกรณีส่วนใหญ่:
javac
, Scala) มันช่วยให้คุณควบคุมสถานะ parser ภายในได้มากที่สุดซึ่งช่วยในการสร้างข้อความแสดงข้อผิดพลาดที่ดี (ซึ่งในปีที่ผ่านมา ...