Lexers เป็นเพียงตัวแยกวิเคราะห์อย่างง่าย ๆ ที่ใช้เป็นการเพิ่มประสิทธิภาพการทำงานสำหรับตัวแยกวิเคราะห์หลัก ถ้าเรามี lexer, lexer และ parser จะทำงานร่วมกันเพื่ออธิบายภาษาที่สมบูรณ์ โปรแกรมแยกวิเคราะห์ที่ไม่มีเลเยอร์แยกต่างหากบางครั้งเรียกว่า "สแกนเนอร์"
หากไม่มี lexers โปรแกรมแยกวิเคราะห์จะต้องดำเนินการตามตัวอักษรต่ออักขระ เนื่องจาก parser ต้องเก็บข้อมูลเมตาเกี่ยวกับทุกรายการอินพุตและอาจต้องคำนวณตารางล่วงหน้าสำหรับทุกสถานะของรายการอินพุตซึ่งจะส่งผลให้ปริมาณการใช้หน่วยความจำที่ยอมรับไม่ได้สำหรับขนาดอินพุตขนาดใหญ่ โดยเฉพาะอย่างยิ่งเราไม่จำเป็นต้องมีโหนดแยกต่างหากต่อตัวละครในแผนผังไวยากรณ์แบบนามธรรม
เนื่องจากข้อความบนพื้นฐานแบบตัวอักษรต่ออักขระค่อนข้างคลุมเครือสิ่งนี้จะส่งผลให้เกิดความกำกวมมากกว่าที่น่ารำคาญในการจัดการ R → identifier | "for " identifier
ลองนึกภาพกฎ โดยที่ตัวระบุถูกสร้างขึ้นจากตัวอักษร ASCII ถ้าฉันต้องการหลีกเลี่ยงความคลุมเครือตอนนี้ฉันต้องใช้ lookahead 4 ตัวอักษรเพื่อพิจารณาว่าควรเลือกตัวเลือกใด ด้วย lexer parser จะต้องตรวจสอบว่ามันมี IDENTIFIER หรือ FOR token - 1-token lookahead
ไวยากรณ์สองระดับ
Lexers ทำงานโดยการแปลตัวอักษรเป็นตัวอักษรที่สะดวกกว่า
เครื่องมือแยกวิเคราะห์สแกนเนอร์จะอธิบายไวยากรณ์ (N, Σ, P, S) ที่ไม่ใช่เทอร์มินัล N คือด้านซ้ายมือของกฎในไวยากรณ์ตัวอักษรΣคืออักขระ ASCII โปรดักชั่น P เป็นกฎในไวยากรณ์ และสัญลักษณ์เริ่มต้น S คือกฎระดับสูงสุดของตัวแยกวิเคราะห์
ตอนนี้ lexer กำหนดตัวอักษรของโทเค็น a, b, c, …. สิ่งนี้อนุญาตให้ parser หลักใช้โทเค็นเหล่านี้เป็นตัวอักษร: Σ = {a, b, c, …} สำหรับ lexer โทเค็นเหล่านี้ไม่ใช่เทอร์มินัลและกฎการเริ่มต้น S Lคือ S L →ε | A | b S | c S | …, นั่นคือ: ลำดับของโทเค็นใด ๆ กฎในไวยากรณ์ lexer เป็นกฎทั้งหมดที่จำเป็นในการสร้างโทเค็นเหล่านี้
ประโยชน์ที่มาจากผลการดำเนินงานการแสดงกฎ lexer ในฐานะที่เป็นภาษาประจำ สิ่งเหล่านี้สามารถแยกวิเคราะห์ได้อย่างมีประสิทธิภาพมากกว่าภาษาที่ไม่มีบริบท โดยเฉพาะภาษาปกติสามารถรับรู้ในพื้นที่ O (n) และเวลา O (n) ในทางปฏิบัติผู้สร้างโค้ดสามารถเปลี่ยน lexer ให้เป็นตารางกระโดดที่มีประสิทธิภาพสูงได้
แยกโทเค็นจากไวยากรณ์ของคุณ
ในการสัมผัสกับตัวอย่างของคุณ: digit
และstring
กฎจะแสดงในระดับตัวอักษรต่ออักขระ เราสามารถใช้สิ่งเหล่านี้เป็นโทเค็น ส่วนที่เหลือของไวยากรณ์ยังคงเหมือนเดิม นี่คือ lexer ไวยากรณ์ที่เขียนเป็นไวยากรณ์เชิงเส้นขวาเพื่อให้ชัดเจนว่าเป็นเรื่องปกติ:
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;
แต่เนื่องจากเป็นเรื่องปกติเราจะใช้นิพจน์ทั่วไปเพื่อแสดงไวยากรณ์โทเค็น ต่อไปนี้เป็นคำจำกัดความของโทเค็นข้างต้นว่าเป็น regexes เขียนโดยใช้ไวยากรณ์การยกเว้นคลาสอักขระ. NET และ POSIX charclasses:
digit ~ [0-9]
string ~ "[[:print:]-["]]*"
ไวยากรณ์สำหรับ parser หลักจะมีกฎที่เหลือซึ่งไม่ได้จัดการโดย lexer ในกรณีของคุณนั่นเป็นเพียง:
input = digit | string ;
เมื่อ lexers ไม่สามารถใช้งานได้ง่าย
เมื่อออกแบบภาษาเรามักจะดูแลว่าไวยากรณ์สามารถแยกออกเป็นระดับ lexer และ parser อย่างหมดจดและระดับ lexer อธิบายภาษาปกติ มันเป็นไปไม่ได้เสมอไป
เมื่อฝังภาษา บางภาษาอนุญาตให้คุณใส่รหัสเข้าไปในสตริง"name={expression}"
ได้ ไวยากรณ์ของนิพจน์เป็นส่วนหนึ่งของไวยากรณ์ที่ไม่ใช้บริบทดังนั้นจึงไม่สามารถโทเค็นโดยนิพจน์ทั่วไป เพื่อแก้ปัญหานี้เราทั้ง recombine parser กับ lexer STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END
หรือเราแนะนำราชสกุลเพิ่มเติมเช่น String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END
กฎไวยากรณ์สตริงแล้วอาจมีลักษณะเช่น: แน่นอนว่านิพจน์อาจมีสตริงอื่นซึ่งทำให้เรามีปัญหาต่อไป
เมื่อโทเค็นสามารถมีซึ่งกันและกัน ในภาษาที่เหมือนกับ C คำหลักนั้นแยกไม่ออกจากตัวระบุ สิ่งนี้ถูกแก้ไขใน lexer โดยจัดลำดับความสำคัญของคำหลักมากกว่าตัวระบุ กลยุทธ์ดังกล่าวไม่สามารถทำได้เสมอไป ลองนึกภาพไฟล์Line → IDENTIFIER " = " REST
กำหนดค่าที่ส่วนที่เหลือเป็นตัวอักษรใด ๆ จนถึงจุดสิ้นสุดของบรรทัดแม้ว่าส่วนที่เหลือจะดูเหมือนตัวระบุ a = b c
สายตัวอย่างจะเป็น lexer นั้นโง่จริงๆและไม่ทราบว่าโทเค็นใดอาจเกิดขึ้น ดังนั้นหากเราจัดลำดับความสำคัญ IDENTIFIER กว่าส่วนที่เหลือ lexer IDENT(a), " = ", IDENT(b), REST( c)
จะให้เรา ถ้าเราให้ความสำคัญกับส่วนที่เหลือกว่าตัวระบุ lexer REST(a = b c)
เพียงแค่จะให้เรา
เพื่อแก้ปัญหานี้เราต้องรวมตัวเล็กกับตัวแยกวิเคราะห์อีกครั้ง การแยกสามารถรักษาได้ด้วยการทำให้ lexer ขี้เกียจ: ทุกครั้งที่ parser ต้องการโทเค็นถัดไปมันจะร้องขอมันจาก lexer และบอกให้ lexer เป็นชุดของโทเค็นที่ยอมรับได้ อย่างมีประสิทธิภาพเรากำลังสร้างกฎระดับสูงสุดใหม่สำหรับไวยากรณ์เล็กซ์เซอร์สำหรับแต่ละตำแหน่ง ที่นี่จะส่งผลให้การโทรnextToken(IDENT), nextToken(" = "), nextToken(REST)
และทุกอย่างทำงานได้ดี ต้องมีตัวแยกวิเคราะห์ที่รู้โทเค็นที่ยอมรับได้อย่างสมบูรณ์ในแต่ละสถานที่ซึ่งแสดงถึงตัวแยกวิเคราะห์จากล่างขึ้นบนเช่น LR
เมื่อ lexer ต้องรักษาสถานะ เช่นภาษา Python delimits บล็อกรหัสไม่ได้โดยวงเล็บปีกกา แต่โดยเยื้อง มีวิธีในการจัดการกับไวยากรณ์ที่มีความอ่อนไหวของเลย์เอาต์ภายในไวยากรณ์ แต่เทคนิคเหล่านั้น overkill สำหรับ Python แทนเล็กซัสจะตรวจสอบการเยื้องของแต่ละบรรทัดและปล่อยโทเค็น INDENT หากพบบล็อกที่เยื้องใหม่และโทเค็น DEDENT หากบล็อกนั้นจบลง สิ่งนี้จะทำให้ไวยากรณ์หลักง่ายขึ้นเพราะตอนนี้มันสามารถแกล้งโทเค็นเหล่านั้นได้เหมือนกับการจัดฟันแบบหยิก อย่างไรก็ตาม lexer ต้องการรักษาสถานะ: การเยื้องปัจจุบัน นี่หมายถึง lexer ในทางเทคนิคไม่ได้อธิบายภาษาปกติอีกต่อไป แต่จริงๆแล้วเป็นภาษาที่ไวต่อบริบท โชคดีที่ความแตกต่างนี้ไม่เกี่ยวข้องในทางปฏิบัติและผู้ใช้ Python ยังสามารถใช้งานได้ในเวลา O (n)