ฉันจะระบุไวยากรณ์สำหรับ parser ได้อย่างไร


12

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

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

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

ฉันสนใจหลักในปัญหาของการระบุไวยากรณ์ที่เป็นตัวแทนของตัวอย่างที่เป็นรูปธรรม (บวกและลบ) อย่างเป็นทางการ ซึ่งแตกต่างจากปัญหาของการออกแบบใหม่ไวยากรณ์ ขอบคุณ Macneil ที่ชี้ให้เห็นความแตกต่างนี้

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

มีการระบุไวยกรณ์สำหรับ parser อย่างไร มีหนังสือหรือเอกสารอ้างอิงนั่นคือมาตรฐาน de-facto สำหรับอธิบายวิธีปฏิบัติที่ดีที่สุดวิธีการออกแบบและข้อมูลที่เป็นประโยชน์อื่น ๆ เกี่ยวกับการระบุไวยากรณ์สำหรับโปรแกรมวิเคราะห์คำหรือไม่? เมื่ออ่านเกี่ยวกับไวยากรณ์ parser ฉันควรเน้นเรื่องใด


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

8
@kjo โชคร้าย หากสิ่งที่คุณต้องการคือรายการของการอ้างอิงคุณไม่ได้ใช้ Stack Exchange เพื่อศักยภาพสูงสุด เมตาปัญหาของคุณไม่ได้ใช้เว็บไซต์ตามที่ตั้งใจไว้ คำถามประจำรายการแทบหมดกำลังใจในการแลกเปลี่ยนแบบสแต็คเพราะไม่เหมาะกับคำถามและคำตอบแบบจำลองที่ดีมาก ผมขอแนะนำให้คุณเปลี่ยนความคิดของคุณที่มีต่อการถามคำถามที่มีคำตอบไม่ได้รายการความคิดหรือความคิดเห็น
อดัมเลียร์

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

5
@kjo: โปรดอย่าท้อแท้! การแก้ไขของ Anna เก็บส่วนสำคัญและหัวใจของคำถามของคุณไว้และเธอก็ช่วยเหลือคุณด้วยการทำแบบฟอร์มคำถามเพิ่มเติมที่เราคาดหวังไว้ใน Programmers.SE ฉันรู้ว่าไม่มีการอ้างอิงที่ชัดเจนที่คุณต้องการ แต่ก็สามารถให้คำตอบได้ [OTOH ฉันรู้การอ้างอิงนี้หรือไม่ฉันจะรวมมันไว้ด้วย] เราต้องการสนับสนุนคำตอบเพิ่มเติมเช่นฉันเพราะในกรณีเฉพาะนี้ฉันไม่เชื่อว่ามีการอ้างอิงถึงสิ่งที่คุณต้องการเพียงแค่ ประสบการณ์จากการพูดคุยกับผู้อื่น
Macneil

4
@kjo ฉันย้อนกลับไปที่การแก้ไขของ Anna และพยายามรวมการเรียกเฉพาะสำหรับการอ้างอิงแบบบัญญัติตามคำแนะนำของเราสำหรับคำแนะนำหนังสือ : มีข้อมูลที่ดีจำนวนมากในคำตอบที่ให้ไว้และทำให้เป็นโมฆะโดยทำให้ขอบเขต คำถามเกี่ยวกับการหาหนังสือเพียงเล่มเดียวจะเป็นการสิ้นเปลือง ตอนนี้ถ้าเราทุกคนสามารถหยุดกับการแก้ไขการรบมันจะยอดเยี่ยม

คำตอบ:


12

จากไฟล์ตัวอย่างคุณจะต้องตัดสินใจตามจำนวนที่คุณต้องการสรุปจากตัวอย่างเหล่านั้น สมมติว่าคุณมีตัวอย่างสามตัวอย่างต่อไปนี้: (แต่ละไฟล์แยกกัน)

f() {}
f(a,b) {b+a}
int x = 5;

คุณสามารถระบุไวยากรณ์สองอันที่จะยอมรับตัวอย่างเหล่านี้ได้เล็กน้อย:

Trivial Grammar One:

start ::= f() {} | f(a,b) {b+a} | int x = 5;

ไวยากรณ์เล็กน้อยที่สอง:

start ::= tokens
tokens ::= token tokens | <empty>
token ::= identifier | literal | { | } | ( | ) | , | + | = | ;

คนแรกเป็นเรื่องไม่สำคัญเพราะมันยอมรับเพียงสามตัวอย่าง อันที่สองนั้นไม่สำคัญเพราะยอมรับทุกสิ่งที่อาจใช้โทเค็นประเภทนั้น [สำหรับการสนทนานี้ฉันจะสมมติว่าคุณไม่ได้กังวลเกี่ยวกับการออกแบบ tokenizer มาก: มันง่ายที่จะสมมติตัวระบุตัวเลขและเครื่องหมายวรรคตอนเป็นโทเค็นของคุณและคุณสามารถยืมชุดโทเค็นจากภาษาสคริปต์ใด ๆ ที่คุณต้องการ ยังไงก็ตาม]

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

methods ::= method methods | empty

ซึ่งระบุไว้ดีกว่าในEBNFเป็น:

methods ::= {method}

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

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

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

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


1
+1 สำหรับข้อความแสดงข้อผิดพลาดที่ดีขึ้น ผู้ใช้ภาษาส่วนใหญ่ของคุณจะไม่เป็นผู้เชี่ยวชาญไม่ว่าจะเป็น 10 หรือ 10 ล้านคน ทฤษฎีการแยกวิเคราะห์ได้ละเลยแง่มุมนี้ไปนานเกินไป
MSalters

10

ฉันจะเรียนรู้วิธีระบุไวยากรณ์สำหรับโปรแกรมแยกวิเคราะห์ได้ที่ไหน

สำหรับตัวแยกวิเคราะห์ส่วนใหญ่มักเป็นรูปแบบของรูปแบบของBackus-Naur <nonterminal> ::= expressionฉันจะทำต่อไปโดยสมมติว่าคุณกำลังใช้อะไรแบบนั้นอยู่และไม่พยายามสร้างเครื่องมือแยกวิเคราะห์ด้วยมือ หากคุณสามารถสร้างตัวแยกวิเคราะห์สำหรับรูปแบบที่คุณได้รับไวยากรณ์ (ฉันได้รวมปัญหาตัวอย่างด้านล่าง) การระบุไวยากรณ์ไม่ใช่ปัญหาของคุณ

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

... ฉันไม่เคยแน่ใจว่าไวยากรณ์ที่ฉันได้มานั้นดี (โดยการวัด "ดี" ที่สมเหตุสมผล)

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

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


ปัญหาตัวอย่าง

เขียนไวยากรณ์ที่จะแยกไฟล์ข้อความที่มีรายการตามที่กำหนดโดยกฎด้านล่าง:

  • รายการประกอบด้วยศูนย์หรือมากกว่าสิ่ง
  • สิ่งที่ประกอบด้วยการระบุการรั้งเปิดเป็นรายการรายการและรั้งปิด
  • _item_list_ ประกอบด้วยศูนย์หรือมากกว่ารายการ
  • รายการ constsis ของตัวบ่งชี้ , เครื่องหมายเท่ากับอีกตัวบ่งชี้และอัฒภาค
  • ระบุเป็นลำดับหนึ่งหรือมากกว่าของตัวอักษร az, AZ, 0-9 หรือขีดเส้นใต้ที่
  • ช่องว่างจะถูกละเว้น

ตัวอย่างอินพุต (ใช้ได้ทั้งหมด):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }

2
และตรวจสอบให้แน่ใจว่าใช้ "Backus-Naur บางตัว" และไม่ใช้ BNF BNF สามารถแสดงไวยากรณ์ได้ แต่มันทำให้เกิดแนวคิดทั่วไปมากมายเช่นรายการซึ่งซับซ้อนเกินกว่าที่พวกเขาต้องการ มีเวอร์ชันที่ได้รับการปรับปรุงต่าง ๆ เช่น EBNF ซึ่งปรับปรุงตามปัญหาเหล่านี้
Mason Wheeler

7

คำตอบของ Macneil และ Blrfl นั้นยอดเยี่ยมมาก ฉันแค่ต้องการเพิ่มความคิดเห็นเกี่ยวกับจุดเริ่มต้นของกระบวนการ

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

คุณอาจบอกว่าโปรแกรมเป็นชุดของคลาส ตกลงนั่นทำให้เรา

program ::= class*

เป็นจุดเริ่มต้น หรือคุณอาจจะต้องเขียนมัน

program ::= ε
         |  class program

ตอนนี้คลาสคืออะไร? มันมีชื่อ ข้อกำหนดคุณสมบัติ superclass เพิ่มเติม และกลุ่มตัวสร้างวิธีการและการประกาศฟิลด์ นอกจากนี้คุณต้องมีวิธีการจัดกลุ่มชั้นเรียนเป็นหน่วยเดียว (ไม่ชัดเจน) และที่ควรเกี่ยวข้องกับการใช้งานบางอย่าง (เช่นติดแท็กด้วยคำสงวนclass) ตกลง:

class ::= "class" identifier extends-clause? "{" class-member-decl * "}"

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

class ::= "(" "class" identifier extends-clause "(" class-member-decl* ")" ")"

หรือ

class ::= "class" identifier "=" "CLASS" extends-clause? class-member-decl* "END"

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

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

สุดท้ายอย่าพยายามเป็นตัวแทนทุกอย่างเกี่ยวกับภาษาของคุณในไวยากรณ์ ตัวอย่างเช่นคุณอาจต้องการห้ามโค้ดบางประเภทที่ไม่สามารถเข้าถึงได้ (เช่นคำสั่งหลัง a returnในภาษาจาวา) คุณอาจไม่ควรลองยัดลงไปในไวยากรณ์เพราะคุณอาจจะพลาดสิ่งต่าง ๆ (อ๊ะอะไรreturnคือสิ่งที่อยู่ในวงเล็บปีกกาหรือถ้าทั้งสองสาขาifกลับมา?) หรือคุณทำให้ไวยากรณ์ของคุณซับซ้อนเกินไป เพื่อจัดการ. มันเป็นข้อ จำกัด ตามบริบท เขียนเป็นบัตรผ่านแยก อีกตัวอย่างที่พบบ่อยมากของข้อ จำกัด ตามบริบทคือระบบพิมพ์ คุณสามารถปฏิเสธการแสดงออกเช่น1 + "a"ในไวยากรณ์ถ้าคุณทำงานหนักพอ แต่คุณไม่สามารถปฏิเสธ1 + x(ที่xมีสตริงประเภท) ดังนั้นหลีกเลี่ยงข้อ จำกัดในการอบครึ่งในไวยากรณ์และทำอย่างถูกต้องเป็นผ่านแยก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.