ฉันควรใช้ตัวสร้างคำแยกวิเคราะห์หรือฉันควรใช้รหัสตัวกำหนดเองและตัวแยกวิเคราะห์แบบกำหนดเองหรือไม่


81

อะไรคือข้อดีและข้อเสียเฉพาะของแต่ละวิธีในการใช้งานไวยากรณ์ภาษาโปรแกรม

ทำไม / เมื่อไหร่ที่ฉันควรจะม้วนตัวเอง? ทำไม / เมื่อไรฉันจึงควรใช้เครื่องกำเนิดไฟฟ้า?


ให้Boost.Spirit Qi ยิง
Ebrahim Mohammadi

คำตอบ:


78

มีสามตัวเลือกจริงๆทั้งสามตัวเลือกจะดีกว่าในสถานการณ์ที่แตกต่างกัน

ตัวเลือกที่ 1: เครื่องมือสร้างคำแยกวิเคราะห์หรือ 'คุณต้องแยกวิเคราะห์ภาษาและคุณเพียงต้องการให้มันทำงานได้เท่านั้น'

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

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

ข้อดีมีความชัดเจน:

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

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

ตัวเลือกที่ 2: ตัวแยกวิเคราะห์ที่เขียนด้วยมือหรือ 'คุณต้องการสร้างตัวแยกวิเคราะห์ของคุณเองและคุณสนใจที่จะใช้งานง่าย'

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

ในกรณีเหล่านี้การใช้ตัวแยกวิเคราะห์ recursive-descent ที่เขียนด้วยมือน่าจะดีที่สุด ในขณะที่ทำให้ถูกต้องอาจมีความซับซ้อนคุณสามารถควบคุม parser ของคุณได้อย่างสมบูรณ์เพื่อให้คุณสามารถทำสิ่งดี ๆ ทุกประเภทที่คุณไม่สามารถทำกับเครื่องกำเนิดไฟฟ้า parser เช่นข้อความแสดงข้อผิดพลาดและการกู้คืนข้อผิดพลาด (ลองลบเครื่องหมายอัฒภาคทั้งหมด : คอมไพเลอร์ C # จะบ่น แต่จะตรวจพบข้อผิดพลาดอื่น ๆ ส่วนใหญ่ต่อไปโดยไม่คำนึงถึงการมีอัฒภาค)

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

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

ตัวเลือกที่ 3: เครื่องกำเนิดไฟฟ้า parser ที่เขียนด้วยมือหรือ 'คุณกำลังพยายามเรียนรู้มากมายจากโครงการนี้และคุณจะไม่รังเกียจที่จะจบลงด้วยรหัสที่ดีที่คุณสามารถนำมาใช้ใหม่ได้มาก'

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

เพื่อให้แนวคิดแก่คุณว่าการทำโครงการแบบนี้เกี่ยวข้องกับอะไรฉันจะบอกคุณเกี่ยวกับความก้าวหน้าของฉัน

เครื่องกำเนิด lexer

ฉันสร้างตัวสร้าง lexer ของตัวเองก่อน ฉันมักจะออกแบบซอฟแวร์เริ่มต้นด้วยวิธีการใช้รหัสดังนั้นฉันคิดว่าฉันต้องการใช้รหัสของฉันและเขียนรหัสชิ้นนี้ (ใน C #):

Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
    new List<StringTokenPair>()
    { // This is just like a lex specification:
      //                    regex   token
        new StringTokenPair("\\+",  CalculatorToken.Plus),
        new StringTokenPair("\\*",  CalculatorToken.Times),
        new StringTokenPair("(",    CalculatorToken.LeftParenthesis),
        new StringTokenPair(")",    CalculatorToken.RightParenthesis),
        new StringTokenPair("\\d+", CalculatorToken.Number),
    });

foreach (CalculatorToken token in
             calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
    Console.WriteLine(token.Value);
}

// Prints:
// 15
// +
// 4
// *
// 10

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

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

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

หากคุณต้องการทราบวิธีการเขียน lexer ของคุณเองนี่เป็นจุดเริ่มต้นที่ดี

ตัวแยกวิเคราะห์

จากนั้นคุณเขียนตัวแยกวิเคราะห์ของคุณ ฉันอ้างถึงที่นี่อีกครั้งสำหรับภาพรวมเกี่ยวกับ parsers ประเภทต่าง ๆ - เป็นกฎง่ายๆยิ่งพวกเขาสามารถแยกวิเคราะห์ยิ่งช้าพวกเขา

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

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

นี่คือสิ่งที่ชิ้นส่วนของรหัสโดยใช้ตัวแยกวิเคราะห์ของฉันสามารถมีลักษณะ:

Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
    new List<StringTokenPair>()
    {
        new StringTokenPair("\\+",  CalculatorToken.Plus),
        new StringTokenPair("\\*",  CalculatorToken.Times),
        new StringTokenPair("(",    CalculatorToken.LeftParenthesis),
        new StringTokenPair(")",    CalculatorToken.RightParenthesis),
        new StringTokenPair("\\d+", CalculatorToken.Number),
    });

Grammar<IntWrapper, CalculatorToken> calculator
    = new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);

// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();

// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);

// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
                         expr.GetDefault(),
                         CalculatorToken.Plus.GetDefault(),
                         term.AddCode(
                         (x, r) => { x.Result.Value += r.Value; return x; }
                         ));

// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
                         term.GetDefault(),
                         CalculatorToken.Times.GetDefault(),
                         factor.AddCode
                         (
                         (x, r) => { x.Result.Value *= r.Value; return x; }
                         ));

// factor: LeftParenthesis expr RightParenthesis
//         | Number;
calculator.AddProduction(factor,
                         CalculatorToken.LeftParenthesis.GetDefault(),
                         expr.GetDefault(),
                         CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
                         CalculatorToken.Number.AddCode
                         (
                         (x, s) => { x.Result = new IntWrapper(int.Parse(s));
                                     return x; }
                         ));

IntWrapper result = calculator.Parse("15+4*10");
// result == 55

(โปรดทราบว่า IntWrapper เป็นเพียง Int32 ยกเว้นว่า C # ต้องการให้เป็นคลาสดังนั้นฉันจึงต้องแนะนำคลาส wrapper)

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


3
ฉันคิดว่าคุณดูถูกดูแคลนปริมาณงานที่ต้องใช้ในการสร้างตัวแยกวิเคราะห์และตัวเล็กที่มีประสิทธิภาพสูง

ฉันได้สร้างตัวสร้าง lexer ของฉันเสร็จแล้วและฉันก็ค่อนข้างไกลพร้อมกับการสร้างตัวแยกวิเคราะห์ของตัวเองเมื่อฉันตัดสินใจที่จะใช้อัลกอริทึมที่แตกต่างกันแทน ฉันใช้เวลาไม่นานนักในการทำให้มันใช้งานได้ แต่จากนั้นอีกครั้งฉันไม่ได้ตั้งเป้าสำหรับ 'ประสิทธิภาพสูง' เพียงแค่ 'ประสิทธิภาพที่ดี' และ 'ประสิทธิภาพที่ยอดเยี่ยม' - Unicode เป็นผู้หญิงเลวที่ได้รับเวลาทำงานที่ดีสำหรับ และการใช้ C # ได้กำหนดค่าใช้จ่ายด้านประสิทธิภาพไว้แล้ว
Alex สิบ Brink

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

1
มีตัวเลือกที่สี่คือตัวแยกวิเคราะห์คำ
YuriAlbuquerque

@AlextenBrink คุณมีบัญชี Github หรือเปล่า? ฉันอยากได้มือเล็ก ๆ กับ lexer / parser สิ่งที่คุณประทับใจ
Behrooz

22

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

ฉันขอแนะนำให้คุณลองอ่านhttp://compilers.iecc.com/crenshaw/เนื่องจากมันมีทัศนคติที่ไม่คุ้นเคยกับวิธีการทำเช่นนั้น


2
ข้อเสนอแนะที่ดีและลิงค์ที่มีประโยชน์มาก
Maniero

14

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

ข้อดีอีกอย่างของการเขียนของคุณเองก็คือการแยกวิเคราะห์การแสดงที่ง่ายกว่านั้นไม่มีการโต้ตอบกับไวยากรณ์ของคุณแบบหนึ่งต่อหนึ่ง

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

Bjarne Stroustrup พูดถึงวิธีที่เขาใช้ YACC สำหรับการติดตั้ง C ++ เป็นครั้งแรก (ดูThe Design and Evolution of C ++ ) ในกรณีแรกนั้นเขาหวังว่าเขาจะเขียนโปรแกรมวิเคราะห์คำซ้ำลงไปแทน!


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

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

JavaCC อ้างว่ามีข้อความแสดงข้อผิดพลาดที่ดีที่สุด นอกจากนี้สังเกตเห็นข้อผิดพลาด JavaScript และข้อความเตือนบน V8 และ Firefox ฉันคิดว่าพวกเขาไม่ได้ใช้ตัวแยกวิเคราะห์
Ming-Tang

2
@SHiNKiROU: แน่นอนมันอาจไม่ใช่อุบัติเหตุที่ JavaCC ใช้การแยกวิเคราะห์แบบเรียกซ้ำด้วยเช่นกัน
Macneil

10

ตัวเลือก 3: ไม่มี (ตัวสร้าง parser ของคุณเอง)

เพียงเพราะมีเหตุผลที่จะไม่ใช้ANTLR , bison , Coco / R , Grammatica , JavaCC , Lemon , Parboiled , SableCC , Quexและอื่น ๆ - นั่นไม่ได้หมายความว่าคุณควรม้วน parser + lexer ของคุณเองทันที

ระบุว่าทำไมเครื่องมือเหล่านี้ไม่ดีพอ - ทำไมพวกเขาถึงไม่ยอมให้คุณไปถึงเป้าหมาย?

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


1
ฉันเห็นด้วยกับตัวทดลอง parser ก่อนจากนั้นลองใช้โซลูชันที่กำหนดเอง แต่ข้อดี (dis) ที่เฉพาะเจาะจงคืออะไร นี่เป็นคำแนะนำทั่วไปเกือบทั้งหมด
Maniero

1
มันเป็นคำแนะนำทั่วไป - แต่แล้วคุณก็ถามคำถามทั่วไป : P ฉันจะขยายออกด้วยความคิดที่เฉพาะเจาะจงมากขึ้นเกี่ยวกับข้อดีข้อเสียในวันพรุ่งนี้
Peter Boughton

1
ฉันคิดว่าคุณดูถูกดูแคลนปริมาณงานที่ต้องใช้ในการสร้างโปรแกรมวิเคราะห์คำและ lexer ที่กำหนดเอง โดยเฉพาะอย่างยิ่งนำมาใช้ใหม่

8

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

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

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

ในที่สุด: คุณได้พิจารณาการฝังภาษาของคุณใน LISP และให้ล่าม LISP ทำการแปลอย่างหนักสำหรับคุณหรือไม่? AutoCAD ทำเช่นนั้นและพบว่าทำให้ชีวิตของพวกเขาง่ายขึ้นมาก มีล่าม LISP น้ำหนักเบาอยู่สองสามตัวบางอันฝังอยู่ได้


มันเป็นเรื่องที่น่าสนใจที่จะนำเสนอโซลูชั่นที่กำหนดเอง
Maniero

1
ดีมาก. ฉันจะเพิ่มเป็นจุดข้อมูลที่ Fortran ต้องการ lookahead เกือบทั้งหมด (ทั้งบรรทัด) โดยพลการเพื่อแยกวิเคราะห์สิ่งต่าง ๆ ต่อหน้า JOVIAL แต่ในเวลานั้นพวกเขาไม่มีความคิดอื่น ๆ เกี่ยวกับวิธีการสร้าง (หรือนำไปใช้) ภาษา
Macneil

การเดินเป็นวิธีการขนส่งที่ดีที่สุดเพราะให้เวลาคุณคิดว่าการไปในที่ที่คุณไปนั้นคุ้มค่าหรือไม่ มันก็มีสุขภาพดีเช่นกัน
babou

6

ผมเคยเขียน parser สำหรับการประยุกต์ใช้ในเชิงพาณิชย์ครั้งและฉันใช้yacc มีต้นแบบการแข่งขันที่ผู้พัฒนาเขียนทั้งหมดด้วยมือใน C ++ และทำงานช้าลงประมาณห้าเท่า

สำหรับ lexer สำหรับ parser นี้ฉันเขียนมันด้วยมือทั้งหมด มันต้องใช้เวลา - ขอโทษมันเป็นเกือบ 10 ปีที่ผ่านมาดังนั้นผมจึงไม่จำมันได้อย่างแม่นยำ - ประมาณ 1000 เส้นในC

เหตุผลที่ฉันเขียน lexer ด้วยมือคือไวยากรณ์อินพุตของ parser มันเป็นข้อกำหนดบางอย่างที่โปรแกรมแยกวิเคราะห์ของฉันต้องปฏิบัติตามเมื่อเทียบกับสิ่งที่ฉันออกแบบ (แน่นอนฉันจะออกแบบมันแตกต่างกันและดีกว่า!) ไวยากรณ์นั้นขึ้นอยู่กับบริบทอย่างรุนแรงและแม้แต่การพึ่งพาอาศัยความหมายในบางสถานที่ ตัวอย่างเช่นเครื่องหมายอัฒภาคอาจเป็นส่วนหนึ่งของโทเค็นในที่เดียว แต่ตัวคั่นในที่อื่น - ขึ้นอยู่กับการตีความความหมายขององค์ประกอบบางอย่างที่ถูกแยกวิเคราะห์ก่อนหน้านี้ ดังนั้นฉัน "ฝัง" การพึ่งพาความหมายเช่นนี้ใน lexer ที่เขียนด้วยมือและทำให้ฉันมีBNFที่ค่อนข้างตรงไปตรงมาและง่ายต่อการใช้ใน yacc

เพิ่มในการตอบสนองต่อMacneil : yacc ให้สิ่งที่เป็นนามธรรมที่ทรงพลังมากซึ่งทำให้โปรแกรมเมอร์คิดในแง่ของเทอร์มินัลไม่ใช่เทอร์มินัลการผลิตและสิ่งต่าง ๆ เช่นนั้น นอกจากนี้เมื่อใช้งานyylex()ฟังก์ชั่นมันช่วยให้ฉันมุ่งเน้นที่การคืนโทเค็นปัจจุบันและไม่ต้องกังวลกับสิ่งที่เกิดขึ้นก่อนหรือหลังมัน โปรแกรมเมอร์ C ++ ทำงานกับระดับตัวอักษรโดยไม่ได้รับประโยชน์จากนามธรรมและจบลงด้วยการสร้างอัลกอริทึมที่ซับซ้อนและมีประสิทธิภาพน้อยลง เราสรุปได้ว่าความเร็วที่ช้าลงนั้นไม่เกี่ยวข้องกับ C + + หรือไลบรารีใด ๆ เราวัดความเร็วในการแจงบริสุทธิ์ด้วยไฟล์ที่โหลดในหน่วยความจำ หากเรามีปัญหาการบัฟเฟอร์ไฟล์ yacc จะไม่เป็นเครื่องมือของเราในการเลือกแก้ไข

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


ฉันอยากรู้อยากเห็นเกี่ยวกับการใช้ C ++ ที่ช้าลงห้าเท่าด้วยมือ: บางทีมันอาจเป็นไฟล์ที่ไม่ดี มันสามารถสร้างความแตกต่างใหญ่
Macneil

@ Macneil: ฉันจะโพสต์คำตอบของฉัน ความคิดเห็นยาวเกินไป
azheglov

1
++ เป็นประสบการณ์ที่ดี ฉันจะไม่ให้น้ำหนักกับการแสดงมากเกินไป มันง่ายสำหรับโปรแกรมที่ดีอื่น ๆ ที่จะชะลอตัวลงโดยสิ่งที่โง่และไม่จำเป็น ฉันเขียนตัวแยกวิเคราะห์ที่สืบเชื้อสายมาพอที่จะรู้ว่าจะทำอะไรไม่ได้ดังนั้นฉันจึงสงสัยว่าจะมีอะไรเร็วกว่านี้หรือไม่ ท้ายที่สุดจำเป็นต้องอ่านตัวอักษร ฉันสงสัยว่าตัวแยกวิเคราะห์ที่รันบนโต๊ะจะช้าลงเล็กน้อย แต่อาจไม่เพียงพอที่จะสังเกตเห็น
Mike Dunlavey

3

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

เมื่อเร็ว ๆ นี้ฉันมาชอบตัวแยกวิเคราะห์มะนาวซึ่งเป็นเนื้อหาที่ง่ายและง่ายที่สุดที่ฉันเคยใช้ เพื่อประโยชน์ในการทำสิ่งต่าง ๆ ที่ง่ายต่อการบำรุงรักษาฉันแค่ใช้สิ่งนั้นเพื่อความต้องการส่วนใหญ่ SQLite ใช้เช่นเดียวกับโครงการที่มีชื่อเสียงอื่น ๆ

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


3
+1 สำหรับ "คุณจะกลิ้งตัวเองเร็วกว่าที่คุณจะสามารถเรียนรู้จาก lexer ได้หรือไม่"
bobah

ใช่เป็นจุดที่ดี
Maniero

3

ขึ้นอยู่กับว่าเป้าหมายของคุณคืออะไร

คุณกำลังพยายามเรียนรู้วิธีการทำงานของ parsers / compilers จากนั้นเขียนของคุณเองตั้งแต่เริ่มต้น นั่นเป็นวิธีเดียวที่คุณจะได้เรียนรู้ที่จะซาบซึ้งในสิ่งที่พวกเขาทำ ฉันเขียนหนึ่งเดือนที่ผ่านมาและมันเป็นประสบการณ์ที่น่าสนใจและมีค่าโดยเฉพาะอย่างยิ่ง 'ah ดังนั้นนั่นเป็นเหตุผลว่าทำไมภาษา X จึงใช้ช่วงเวลานี้ ... '

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

คุณต้องการบางสิ่งบางอย่างที่คุณต้องการขยายในอีก 10, 20 หรือ 30 ปีหรือไม่ เขียนของคุณเองและใช้เวลาของคุณ มันจะคุ้มค่ามาก


มันเป็นงานแรกของฉันในการคอมไพเลอร์ฉันเรียนรู้ / ทดลองและมันเป็นความตั้งใจของฉันที่จะรักษามันไว้เป็นเวลานาน
Maniero

3

คุณเคยพิจารณาแนวทางการปรับแต่งภาษามาร์ตินฟาวเลอร์หรือไม่? ข้อความจากบทความ

การเปลี่ยนแปลงที่ชัดเจนที่สุดที่การปรับแต่งภาษาทำให้สมการคือความง่ายในการสร้าง DSL ภายนอก คุณไม่ต้องเขียนโปรแกรมแยกวิเคราะห์อีกต่อไป คุณต้องกำหนดรูปแบบนามธรรม - แต่จริงๆแล้วเป็นขั้นตอนการสร้างแบบจำลองข้อมูลที่ค่อนข้างตรงไปตรงมา นอกจากนี้ DSL ของคุณยังได้รับ IDE ที่ทรงพลัง - แม้ว่าคุณจะต้องใช้เวลาในการกำหนดตัวแก้ไขนั้น เครื่องกำเนิดไฟฟ้ายังคงเป็นสิ่งที่คุณต้องทำและความรู้สึกของฉันคือมันไม่ง่ายกว่าที่เคยเป็นมา แต่การสร้างเครื่องกำเนิดสำหรับ DSL ที่ดีและเรียบง่ายเป็นหนึ่งในส่วนที่ง่ายที่สุดของการฝึก

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

แก้ไขเพื่อครอบคลุมความคิดเห็น (และคำถามที่แก้ไขแล้ว)

ข้อดีของการกลิ้งของคุณเอง

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

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

ข้อดีของการใช้ห้องสมุดของคนอื่น

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

ดังนั้นหากคุณต้องการผลลัพธ์สุดท้ายอย่างรวดเร็วให้ใช้ห้องสมุดของคนอื่น

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


มันเป็นทางเลือกที่ยอดเยี่ยมสำหรับการคิด
Maniero

1
@bigown แก้ไขแล้วเพื่อตอบคำถามของคุณได้ดียิ่งขึ้น
Gary Rowe

2

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


ไม่มีประโยชน์อย่างยิ่ง คุณอาจพูดเช่นกันว่า“ ข้อดีของการเรียนรู้ที่จะขับขี่คือคุณสามารถขับได้ ข้อดีของการเรียนขี่มอเตอร์ไซค์ก็คือคุณสามารถขี่มอเตอร์ไซค์ได้”
Zearin

1

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

ในตัวแยกวิเคราะห์ของฉันฉันใช้นิพจน์ทั่วไป (ฉันหมายถึงสไตล์ Perl) เพื่อทำเครื่องหมายและใช้ฟังก์ชั่นอำนวยความสะดวกบางอย่างเพื่อเพิ่มความสามารถในการอ่านโค้ด อย่างไรก็ตามรหัส parser สร้างได้เร็วโดยการทำตารางรัฐและยาวswitch- cases ซึ่งอาจเพิ่มขนาดรหัสที่มาจนกว่าคุณจะ.gitignoreให้พวกเขา

นี่คือสองตัวอย่างของตัวแยกวิเคราะห์ที่ฉันเขียน:

https://github.com/SHiNKiROU/DesignScript - ภาษาเบสิกขั้นพื้นฐานเพราะฉันขี้เกียจเกินกว่าจะเขียน lookaheads ในสัญกรณ์อาร์เรย์ฉันเสียสละคุณภาพข้อความข้อผิดพลาด https://github.com/SHiNKiROU/ExprParser - เครื่องคิดเลขสูตร สังเกตุเห็นถึงเทคนิคการ metaprogramming แปลก ๆ


0

"ฉันควรใช้ 'วงล้อ' ที่ผ่านการทดสอบและผ่านการทดสอบแล้วหรือนำกลับมาใช้ใหม่หรือไม่"


1
คุณพูดถึง "วงล้อ" นี้คืออะไร? ;-)
Jason Whitehorn

IMO นี่ไม่ใช่ความคิดเห็นที่ดีเกี่ยวกับคำถามนี้ นี่เป็นเพียงคำแนะนำทั่วไปที่ไม่เหมาะสมกับกรณีเฉพาะ ฉันเริ่มสงสัยว่าพื้นที่51.stackexchange.com/proposals/7848ข้อเสนอถูกปิดก่อนเวลาอันควร
Maniero

2
หากไม่เคยคิดค้นล้อขึ้นมาใหม่เราจะไม่เดินทางที่ 100kmph + ทุกวัน - ถ้าคุณไม่แนะนำก้อนหินก้อนใหญ่หมุนบนเพลาไม้จะดีกว่ายางสมัยใหม่หลายรุ่น ยานพาหนะมากมาย?
Peter Boughton

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

@ Peter: มันเป็นสิ่งหนึ่งที่จะคิดค้นสิ่งใหม่ (หมายถึงทำมันแตกต่างกันโดยสิ้นเชิง) แต่การปรับแก้ปัญหาที่มีอยู่เพื่อตอบสนองความต้องการเพิ่มเติมจะดีกว่า ฉันทั้งหมดสำหรับ 'ปรับปรุง' แต่กลับไปที่กระดานวาดภาพสำหรับปัญหาที่แก้ไขแล้วดูเหมือนว่าผิด
JBRWilkinson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.