มากับโทเค็นสำหรับ lexer


14

ฉันกำลังเขียนโปรแกรมแยกวิเคราะห์สำหรับภาษามาร์กอัปที่ฉันสร้างขึ้น (เขียนด้วยภาษาไพ ธ อน แต่นั่นไม่เกี่ยวข้องกับคำถามนี้จริง ๆ แล้วถ้านี่เป็นความคิดที่ไม่ดีฉันชอบคำแนะนำสำหรับเส้นทางที่ดีกว่า) .

ฉันกำลังอ่านเกี่ยวกับตัวแยกวิเคราะห์ที่นี่: http://www.ferg.org/parsing/index.htmlและฉันกำลังพยายามเขียน lexer ซึ่งถ้าหากฉันเข้าใจถูกต้องให้แบ่งเนื้อหาออกเป็นโทเค็น สิ่งที่ฉันมีปัญหาในการทำความเข้าใจคือสิ่งที่ฉันควรใช้โทเค็นประเภทใดหรือวิธีการสร้างพวกเขา ตัวอย่างเช่นประเภทโทเค็นในตัวอย่างที่ฉันเชื่อมโยงคือ:

  • STRING
  • IDENTIFIER
  • จำนวน
  • ช่องว่าง
  • แสดงความคิดเห็น
  • EOF
  • สัญลักษณ์จำนวนมากเช่น {และ (นับเป็นประเภทโทเค็นของตัวเอง

ปัญหาที่ฉันมีอยู่ก็คือโทเค็นประเภททั่วไปนั้นดูจะเป็นเรื่องที่ฉันไม่ชอบ ตัวอย่างเช่นเหตุใด STRING ถึงมีประเภทโทเค็นแยกต่างหากกับ IDENTIFIER สตริงสามารถแสดงเป็น STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START

สิ่งนี้อาจเกี่ยวข้องกับภาษาของฉันด้วย ยกตัวอย่างเช่นการประกาศตัวแปรถูกเขียนเป็นและนำไปใช้กับ{var-name var value} {var-name}ดูเหมือนว่า'{'และ'}'ควรจะราชสกุลของตัวเอง แต่มี var_name และ VAR_VALUE ประเภทโทเค็นมีสิทธิ์หรือจะเหล่านี้ทั้งสองตกอยู่ภายใต้การ IDENTIFIER? มีอะไรเพิ่มเติมคือ VAR_VALUE สามารถมีช่องว่างได้จริง ช่องว่างหลังจากvar-nameถูกใช้เพื่อบ่งบอกถึงการเริ่มต้นของค่าในการประกาศ .. ช่องว่างอื่น ๆ เป็นส่วนหนึ่งของค่า พื้นที่ว่างนี้กลายเป็นโทเค็นของตัวเองหรือไม่ ช่องว่างมีความหมายในบริบทนี้เท่านั้น ยิ่งกว่านั้น{อาจไม่ใช่จุดเริ่มต้นของการประกาศตัวแปร .. มันขึ้นอยู่กับบริบท (นั่นคือคำนั้นอีกครั้ง!) {:เริ่มการประกาศชื่อและ{ ยังสามารถใช้เป็นส่วนหนึ่งของค่าบางอย่าง

ภาษาของฉันคล้ายกับ Python ในบล็อกนั้นถูกสร้างขึ้นด้วยการเยื้อง ฉันกำลังอ่านเกี่ยวกับวิธีที่ Python ใช้ lexer เพื่อสร้างโทเค็น INDENT และ DEDENT (ที่ให้บริการมากกว่าหรือน้อยกว่าเป็นสิ่งที่{และ}จะทำในภาษาอื่น ๆ มากมาย) Python อ้างว่าปราศจากบริบทซึ่งหมายความว่าอย่างน้อย lexer ไม่ควรสนใจว่ามันอยู่ที่ไหนในกระแสขณะสร้างโทเค็น ตัวเล็กของไพ ธ อนรู้ได้อย่างไรว่ามันสร้างโทเค็น INDENT ที่มีความยาวเฉพาะโดยไม่ทราบเกี่ยวกับอักขระก่อนหน้า (เช่นบรรทัดก่อนหน้าเป็นบรรทัดใหม่ดังนั้นเริ่มสร้างช่องว่างสำหรับ INDENT) ฉันถามเพราะฉันจำเป็นต้องรู้สิ่งนี้ด้วย

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


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

ทำไมต้องคิดค้นล้อแบบกำหนดเองอีกครั้ง แทนที่จะพิจารณาการออกแบบภาษาที่ต้องใช้ตัวสร้างแบบกำหนดเองคุณลองพิจารณาใช้ภาษาที่มีอยู่แล้วที่มาพร้อมกับตัว lexer ในตัวเช่น LISP หรือ FORTH หรือไม่?
John R. Strohm

2
@ JohnR.Strohm เพื่อการศึกษา ภาษานั้นอาจจะไม่มีประโยชน์เลย
ยาระเบิด

คำตอบ:


11

คำถามของคุณ (ตามคำแนะนำในย่อหน้าสุดท้ายของคุณ) ไม่เกี่ยวกับ lexer จริงๆมันเกี่ยวกับการออกแบบอินเตอร์เฟสที่ถูกต้องระหว่าง lexer และ parser อย่างที่คุณอาจจินตนาการว่ามีหนังสือหลายเล่มเกี่ยวกับการออกแบบ lexers และ parsers ฉันบังเอิญชอบหนังสือตัวแยกวิเคราะห์โดย Dick Gruneแต่มันอาจไม่ใช่หนังสือเกริ่นนำที่ดี ฉันเกิดความไม่ชอบหนังสือC-basedอย่างหนักโดย Appelเนื่องจากโค้ดไม่สามารถขยายไปยังคอมไพเลอร์ของคุณได้อย่างเป็นประโยชน์ (เนื่องจากปัญหาการจัดการหน่วยความจำที่มีอยู่ในการตัดสินใจที่จะแกล้ง C เป็นเหมือน ML) การแนะนำตัวเองของฉันคือหนังสือของ PJ Brownแต่มันไม่ได้เป็นการแนะนำทั่วไปที่ดี (แต่ค่อนข้างดีสำหรับนักแปลโดยเฉพาะ) แต่กลับไปที่คำถามของคุณ

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

ซึ่งหมายความว่า (ขึ้นอยู่กับรายละเอียดของภาษา) คุณควรรู้จักสตริงเป็น "ตัวอักษรตามด้วยลำดับของ not-" และจากนั้นอีก "ตัวอักษรกลับไปที่ parser เป็นหน่วยเดียวมีหลาย เหตุผลสำหรับสิ่งนี้ แต่สิ่งสำคัญคือ

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

ตัวแยกวิเคราะห์ส่วนใหญ่สามารถดำเนินการได้ทันทีเมื่อรับโทเค็นจาก lexer ตัวอย่างเช่นทันทีที่ได้รับ IDENTIFIER ตัวแยกวิเคราะห์สามารถทำการค้นหาตารางสัญลักษณ์เพื่อค้นหาว่าสัญลักษณ์นั้นเป็นที่รู้จักอยู่แล้วหรือไม่ ถ้า parser ของคุณแยกวิเคราะห์ค่าคงที่สตริงเป็น QUOTE (IDENTIFIER SPACES) * QUOTE คุณจะทำการค้นหาตารางสัญลักษณ์ที่ไม่เกี่ยวข้องจำนวนมากหรือคุณจะสิ้นสุดการค้นหาตารางสัญลักษณ์ที่สูงขึ้นโครงสร้างของตัวแยกวิเคราะห์ไวยากรณ์ ณ จุดนี้คุณแน่ใจแล้วว่าคุณไม่ได้ดูสตริง

เพื่อย้ำสิ่งที่ฉันพยายามจะพูด แต่ต่างกันผู้ที่มีความกังวลควรกังวลกับการสะกดคำของสิ่งต่าง ๆ และ parser กับโครงสร้างของสิ่งต่าง ๆ

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

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

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

มีวิธีอื่นในการออกแบบระบบแยกวิเคราะห์ภาษา แน่นอนว่ามีระบบการสร้างคอมไพเลอร์ที่อนุญาตให้คุณระบุระบบ lexer และ parser รวมกัน (ฉันคิดว่าANTLRรุ่น Javaทำเช่นนี้) แต่ฉันไม่เคยใช้เลย

บันทึกประวัติศาสตร์ครั้งสุดท้าย ทศวรรษที่ผ่านมามันเป็นสิ่งสำคัญที่ lexer ต้องทำให้ได้มากที่สุดก่อนที่จะส่งมอบ parser เนื่องจากโปรแกรมทั้งสองจะไม่พอดีกับหน่วยความจำในเวลาเดียวกัน การทำเพิ่มเติมใน lexer ทำให้หน่วยความจำเหลืออยู่มากขึ้นเพื่อทำให้ parser ฉลาด ฉันเคยใช้คอมไพเลอร์Whitesmiths Cเป็นเวลาหลายปีและถ้าฉันเข้าใจอย่างถูกต้องมันจะทำงานใน RAM เพียง 64KB (เป็นโปรแกรม MS-DOS รุ่นเล็ก) และมันแปลตัวแปรของ C ที่ อยู่ใกล้กับ ANSI C มาก


บันทึกประวัติศาสตร์ที่ดีเกี่ยวกับขนาดหน่วยความจำเป็นเหตุผลหนึ่งในการแบ่งงานเป็น lexers และ parsers ในตอนแรก
สตีฟ

3

ฉันจะตอบคำถามสุดท้ายของคุณซึ่งไม่ใช่เรื่องจริงโง่ ตัวแยกวิเคราะห์สามารถสร้างโครงสร้างที่ซับซ้อนได้แบบตัวอักษรต่ออักขระ ถ้าฉันจำได้ไวยากรณ์ใน Harbison และ Steele ("C - คู่มืออ้างอิง") ​​มีโปรดักชั่นที่ใช้อักขระเดียวเป็นเทอร์มินัลและสร้างตัวระบุสตริงตัวเลขและอื่น ๆ ที่ไม่ใช่เทอร์มินัลจากอักขระเดี่ยว

จากมุมมองของภาษาที่เป็นทางการสิ่งใดก็ตามที่ lexer ที่ใช้นิพจน์ปกติสามารถรับรู้และจัดหมวดหมู่เป็น "ตัวอักษรสตริง", "ตัวระบุ", "หมายเลข", "หมายเลข", "คำหลัก" เป็นต้นแม้กระทั่งตัวแยกวิเคราะห์ LL (1) ดังนั้นจึงไม่มีปัญหาทางทฤษฎีในการใช้ตัวแยกวิเคราะห์เพื่อจดจำทุกสิ่ง

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

ฉันจะบอกว่าการพิจารณาในทางปฏิบัติทำให้คนตัดสินใจมี lexers และ parsers แยกกัน


ใช่ - และมาตรฐาน C เองก็ทำสิ่งเดียวกันราวกับว่าฉันจำได้ถูกต้องทั้ง Kernighan และ Ritchie ทำทั้งสองอย่าง
James Youngman

3

ดูเหมือนว่าคุณกำลังพยายามเขียน lexer / parser โดยไม่เข้าใจไวยากรณ์จริงๆ โดยทั่วไปเมื่อผู้คนเขียน lexer และ parser พวกเขากำลังเขียนพวกเขาเพื่อให้สอดคล้องกับไวยากรณ์บางอย่าง lexer ควรส่งกลับโทเค็นในไวยากรณ์ขณะที่ parser ใช้โทเค็นเหล่านั้นเพื่อจับคู่กฎ / ไม่ใช่เทอร์มินัลควรกลับราชสกุลในโรงเรียนมัธยมในขณะที่ตัวแยกวิเคราะห์ที่ใช้ราชสกุลเหล่านั้นเพื่อให้ตรงกับกฎ หากคุณสามารถแยกวิเคราะห์อินพุตของคุณเพียงแค่ไปทีละไบต์ไบต์จากนั้น lexer และ parser อาจ overkill

Lexers ทำให้สิ่งต่าง ๆ ง่ายขึ้น

ภาพรวมไวยากรณ์ : ไวยากรณ์คือชุดของกฎสำหรับลักษณะของไวยากรณ์หรืออินพุตที่ควรมี ตัวอย่างเช่นนี่คือไวยากรณ์ของเล่น (simple_command คือสัญลักษณ์เริ่มต้น):

simple_command:
 WORD DIGIT AND_SYMBOL
simple_command:
     addition_expression

addition_expression:
    NUM '+' NUM

ไวยากรณ์นี้หมายความว่า -
คำสั่ง simple_command ประกอบด้วย
A) WORD ตามด้วย DIGIT ตามด้วย AND_SYMBOL (นี่คือ "โทเค็น" ที่ฉันกำหนด)
B) "นอกจากนี้คำอธิบาย" (นี่คือกฎหรือ "ไม่ใช่เทอร์มินัล")

นอกจากนี้แสดงออกถึง:
NUM ตามด้วย '+' ตามด้วย NUM (NUM คือ "โทเค็น" ที่ฉันกำหนด '+' เป็นเครื่องหมายบวกตามตัวอักษร)

ดังนั้นเนื่องจาก simple_command คือ "สัญลักษณ์เริ่มต้น" (สถานที่ที่ฉันเริ่มต้น) เมื่อฉันได้รับโทเค็นฉันจึงตรวจสอบเพื่อดูว่ามันเหมาะกับ simple_command หรือไม่ หากโทเค็นแรกในอินพุตคือ WORD และโทเค็นถัดไปคือ DIGIT และโทเค็นถัดไปเป็น AND_SYMBOL แสดงว่าฉันได้จับคู่ simple_command แล้วและสามารถดำเนินการบางอย่างได้ มิฉะนั้นฉันจะพยายามจับคู่กับกฎอื่น ๆ ของ simple_command ซึ่งเป็น ดังนั้นหากโทเค็นแรกคือ NUM ตามด้วย '+' ตามด้วย NUM ดังนั้นฉันจึงจับคู่คำสั่ง simple_command และฉันดำเนินการบางอย่าง หากไม่ใช่สิ่งเหล่านั้นฉันมีข้อผิดพลาดทางไวยากรณ์

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

ใช้การจัดเรียง lexer / parser ต่อไปนี้เป็นตัวอย่างของการแยกวิเคราะห์ของคุณ:

bool simple_command(){
   if (peek_next_token() == WORD){
       get_next_token();
       if (get_next_token() == DIGIT){
           if (get_next_token() == AND_SYMBOL){
               return true;
           } 
       }
   }
   else if (addition_expression()){
       return true;
   }

   return false;
}

bool addition_expression(){
    if (get_next_token() == NUM){
        if (get_next_token() == '+'){
             if (get_next_token() == NUM){
                  return true;
             }
        }
    }
    return false;
}

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

สุจริต lexers และ parsers ไม่ใช่หัวข้อพื้นฐานที่สุดในโลก ฉันขอแนะนำให้อ่านก่อนและทำความเข้าใจกับไวยากรณ์แล้วอ่านเล็กน้อยเกี่ยวกับ lexers / parsers แล้วดำน้ำ


คุณมีคำแนะนำสำหรับการเรียนรู้เกี่ยวกับไวยากรณ์หรือไม่?
ยาระเบิดใน

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

1

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

นี่มันไม่ใช่เรื่องโง่มันเป็นแค่ความจริง

แต่ความสามารถในการใช้งานจริงขึ้นอยู่กับเครื่องมือและวัตถุประสงค์ของคุณ ตัวอย่างเช่นหากคุณใช้ yacc โดยไม่มี lexer และคุณต้องการอนุญาตให้ใช้ตัวอักษร unicode ในตัวระบุคุณจะต้องเขียนกฎขนาดใหญ่และน่าเกลียดที่ explicity จะระบุตัวอักษรที่ถูกต้องทั้งหมด ในขณะที่ใน lexer คุณอาจจะถามรูทีนไลบรารีว่าตัวละครเป็นสมาชิกของหมวดหมู่จดหมาย

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

ดังนั้นในที่สุดคุณสามารถแยกวิเคราะห์ในระดับบิต


0
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

ไม่มันไม่สามารถทำได้ เกี่ยวกับ"("อะไร ตามที่คุณพูดนั่นไม่ใช่สตริงที่ถูกต้อง และหลบหนี?

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

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