เหตุใดจึงต้องใช้ lexer เป็นอาร์เรย์ 2d และสวิตช์ขนาดยักษ์


24

ฉันทำงานอย่างช้าๆเพื่อจบปริญญาของฉันและภาคเรียนนี้คือ Compilers 101 เรากำลังใช้Dragon Bookอยู่ ไม่นานในหลักสูตรและเรากำลังพูดถึงการวิเคราะห์คำศัพท์และวิธีการที่จะสามารถนำไปใช้ผ่านทางออปติคัลไฟไนต์ จำกัด (ต่อไปนี้คือ DFA) ตั้งค่าสถานะ lexer ต่างๆกำหนดช่วงการเปลี่ยนภาพเป็นต้น

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

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

ดังนั้นการจัดการคืออะไร? เหตุใดหนังสือที่ชัดเจนถึงเรื่องที่บอกว่าทำแบบนี้?

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

( หมายเหตุ:เป็นไปได้ที่ซ้ำกัน " ทำไมใช้วิธีการ OO แทนคำสั่งสวิตช์ขนาดใหญ่? " อยู่ใกล้ แต่ฉันไม่สนใจ OO วิธีการใช้งานหรือแม้แต่วิธีการเตือนแบบ saner ด้วยฟังก์ชันแบบสแตนด์อโลนก็ดี)

[a-zA-Z]+และเพื่อเห็นแก่ตัวอย่างให้พิจารณาภาษาที่มีเพียงตัวบ่งชี้และตัวบ่งชี้เหล่านั้น ในการติดตั้ง DFA คุณจะได้รับ:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(แม้ว่าสิ่งที่จะจัดการกับจุดสิ้นสุดของไฟล์อย่างถูกต้อง)

เปรียบเทียบกับสิ่งที่ฉันคาดหวัง:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

ด้วยรหัสในการNextTokenrefactored ออกเป็นฟังก์ชั่นของตัวเองเมื่อคุณมีหลายปลายทางจากจุดเริ่มต้นของ DFA


5
มรดกทางวัฒนธรรมของโบราณ (1977) หลักการของคอมไพเลอร์ออกแบบ ? 40 ปีที่แล้วรูปแบบการเข้ารหัสแตกต่างกันมาก
ริ้น

7
คุณจะใช้ช่วงการเปลี่ยนภาพของรัฐ DFA อย่างไร และสิ่งนี้เกี่ยวกับเทอร์มินัลและไม่ใช่เทอร์มินัล "ที่ไม่ใช่เทอร์มินัล" มักจะอ้างถึงกฎการผลิตในไวยากรณ์ซึ่งจะเกิดขึ้นหลังจากการวิเคราะห์คำศัพท์

10
ตารางเหล่านั้นไม่ได้หมายถึงมนุษย์สามารถอ่านได้พวกมันมีจุดประสงค์เพื่อให้คอมไพเลอร์ใช้งานได้และทำงานได้อย่างรวดเร็ว มันง่ายที่จะกระโดดไปรอบ ๆ โต๊ะเมื่อมองไปข้างหน้าในอินพุต (เช่นจับการวนซ้ำทางซ้ายแม้ว่าในทางปฏิบัติแล้วภาษาส่วนใหญ่จะสร้างขึ้นเพื่อหลีกเลี่ยงปัญหานั้น)

5
หากบางส่วนของการระคายเคืองของคุณมาจากการรู้วิธีการทำงานที่ดีขึ้นและขาดความสามารถในการรับข้อเสนอแนะหรือชื่นชมสำหรับวิธีการที่คุณต้องการ - ทศวรรษที่ผ่านมาในอุตสาหกรรมจะฝึกอบรมเราให้คาดหวังข้อเสนอแนะ คุณควรเขียนการใช้งานที่ดีขึ้นและโพสต์ไว้ที่ CodeReview.SE เพื่อให้ได้สิ่งนั้นเพื่อความสบายใจของคุณเอง
จิมมี่ฮอฟฟา

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

คำตอบ:


16

ในทางปฏิบัติตารางเหล่านี้สร้างขึ้นจากนิพจน์ทั่วไปที่กำหนดโทเค็นของภาษา:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

เรามีระบบสาธารณูปโภคที่จะสร้างการวิเคราะห์คำศัพท์ตั้งแต่ปี 1975 เมื่อlexถูกเขียน

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


4
ฉันไม่แน่ใจว่าฉันกำลังเสนอราคาขายส่ง การแสดงออกปกติจะจัดการกับภาษา (ปกติ) โดยพลการ ไม่มีวิธีการที่ดีกว่าเมื่อทำงานกับภาษาที่เฉพาะเจาะจงหรือ หนังสือเล่มนี้แตะที่วิธีการคาดการณ์ แต่ก็ไม่สนใจตัวอย่าง นอกจากนี้เมื่อทำวิเคราะห์ไร้เดียงสาสำหรับ C # ปีที่ผ่านมาฉันไม่พบว่ามันยากมากที่จะรักษา ไม่มีประสิทธิภาพ? แน่นอน แต่ไม่ได้รับความสามารถอย่างมากในเวลานั้น
Telastyn

1
@Telastyn: แทบเป็นไปไม่ได้เลยที่จะไปได้เร็วกว่า DFA แบบใช้ตาราง: หาตัวละครตัวถัดไปค้นหาสถานะถัดไปในตารางการเปลี่ยนแปลงเปลี่ยนสถานะ หากสถานะใหม่เป็นเทอร์มินัลให้ปล่อยโทเค็น ใน C # หรือ Java วิธีการใด ๆ ที่เกี่ยวข้องกับการสร้างสตริงชั่วคราวใด ๆ จะช้าลง
วินไคลน์

@kevincline - แน่นอน แต่ในตัวอย่างของฉันไม่มีสตริงชั่วคราว แม้แต่ใน C มันก็แค่เป็นดัชนีหรือตัวชี้ที่เดินผ่านสตริง
Telastyn

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

1
@Telastyn คุณคิดว่า "วิธีการที่ดีกว่า" และในสิ่งที่คุณคาดหวังว่าจะเป็น "ดีกว่า"? เนื่องจากเรามีเครื่องมือ lexing ที่ผ่านการทดสอบเป็นอย่างดีและพวกมันสร้างเครื่องมือแยกวิเคราะห์ที่รวดเร็วมาก (อย่างที่คนอื่น ๆ บอกว่า DFA ที่ทำงานบนโต๊ะนั้นรวดเร็วมาก) มันสมเหตุสมผลที่จะใช้มัน ทำไมเราต้องการคิดค้นวิธีการพิเศษใหม่สำหรับภาษาที่เฉพาะเจาะจงเมื่อเราสามารถเขียนไวยากรณ์ lex? ไวยากรณ์ lex นั้นสามารถบำรุงรักษาได้ดีกว่าและ parser ที่ได้นั้นมีแนวโน้มที่จะถูกต้องมากขึ้น
DW

7

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

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


7

วงในของ:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

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

ในทางปฏิบัตินอกเหนือจากนักเรียน CS ที่กำลังศึกษา lexers ไม่มีใครจำเป็นต้องใช้ (หรือ debug) วงในเพราะมันเป็นส่วนหนึ่งของสำเร็จรูปที่มาพร้อมกับเครื่องมือที่สร้างtransitionตาราง


5

จากความทรงจำ - เป็นเวลานานแล้วที่ฉันได้อ่านหนังสือและฉันค่อนข้างแน่ใจว่าฉันไม่ได้อ่านฉบับล่าสุดฉันไม่จำสิ่งที่ดูเหมือน Java - ส่วนนั้นเขียนด้วย รหัสที่ตั้งใจจะเป็นเทมเพลตตารางที่ถูกเติมด้วยตัวสร้าง lex like lexer ยังคงมีจากหน่วยความจำมีส่วนในการบีบอัดตาราง (อีกครั้งจากหน่วยความจำมันถูกเขียนในลักษณะที่มันยังใช้กับ parsers ขับเคลื่อนตารางดังนั้นอาจเพิ่มเติมในหนังสือกว่าสิ่งที่คุณเห็น) ในทำนองเดียวกันหนังสือที่ฉันจำได้สันนิษฐานว่าเป็นชุดอักขระ 8 บิตฉันคาดหวังว่าส่วนหนึ่งเกี่ยวกับการจัดการชุดอักขระที่ใหญ่กว่าในรุ่นหลังซึ่งอาจเป็นส่วนหนึ่งของการบีบอัดตาราง ฉันได้ให้วิธีทางเลือกเพื่อจัดการกับสิ่งนั้นเป็นคำตอบของคำถาม SO

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


2

ก่อนหน้านี้เคยทำงานกับ Dragon Book มาแล้วเหตุผลหลักในการใช้มือจับโต๊ะและตัวแยกกระดาษคือเพื่อให้คุณสามารถใช้นิพจน์ทั่วไปเพื่อสร้าง lexer และ BNF เพื่อสร้างเครื่องมือแยกวิเคราะห์ หนังสือเล่มนี้ยังครอบคลุมถึงวิธีการใช้เครื่องมือเช่น lex และ yacc และเพื่อให้คุณรู้ว่าเครื่องมือเหล่านี้ทำงานอย่างไร นอกจากนี้มันเป็นสิ่งสำคัญสำหรับคุณที่จะทำงานผ่านตัวอย่างการปฏิบัติบางอย่าง

แม้จะมีความคิดเห็นจำนวนมาก แต่ก็ไม่มีอะไรเกี่ยวข้องกับรูปแบบของรหัสที่เขียนในยุค 40, 50, 60 ... มันต้องเกี่ยวข้องกับการทำความเข้าใจในทางปฏิบัติเกี่ยวกับสิ่งที่เครื่องมือกำลังทำเพื่อคุณและสิ่งที่คุณมี ทำเพื่อให้ทำงานได้ มันมีทุกอย่างเกี่ยวกับการทำความเข้าใจพื้นฐานว่าคอมไพเลอร์ทำงานอย่างไรทั้งจากมุมมองเชิงทฤษฎีและปฏิบัติ

หวังว่าอาจารย์ผู้สอนของคุณจะช่วยให้คุณใช้ lex และ yacc (ยกเว้นว่าเป็นระดับระดับบัณฑิตศึกษาและคุณจะได้รับการเขียน lex และ yacc)


0

มางานปาร์ตี้สาย :-) โทเค็นจะถูกจับคู่กับนิพจน์ทั่วไป เนื่องจากมีจำนวนมากคุณจึงมีเอนจิ้นหลาย regex ซึ่งก็คือ DFA ยักษ์

"แย่ไปกว่านั้นฉันยังไม่เห็นว่ามันจะใช้งานได้จากระยะไกลอย่างไรถ้าภาษานั้นมีความสามารถใน UTF"

มันไม่เกี่ยวข้อง (หรือโปร่งใส) นอกจาก UTF มีคุณสมบัติที่ดีเอนทิตี้ของมันจะไม่ทับซ้อนแม้แต่บางส่วน เช่นไบต์ที่ใช้แทนอักขระ "A" (จากตาราง ASCII-7) จะไม่ถูกใช้อีกครั้งสำหรับอักขระ UTF อื่น ๆ

ดังนั้นคุณมี DFA เดี่ยว (ซึ่งเป็นแบบหลาย regex) สำหรับ lexer ทั้งหมด วิธีที่ดีกว่าที่จะเขียนลงกว่าอาร์เรย์ 2d?

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