อ่าน 6 นาที
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการ
ใน วิทยาการคอมพิวเตอร์ ตัวแยก วิเคราะห์ ลำดับความสำคัญของตัวดำเนินการ (operator-precedence parser) คือตัว แยกวิเคราะห์แบบ จากล่างขึ้นบน (bottom-up parser ) ที่ตีความไวยากรณ์...
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการ
ในวิทยาการคอมพิวเตอร์ตัวแยก วิเคราะห์ ลำดับความสำคัญของตัวดำเนินการ(operator-precedence parser)คือตัว แยกวิเคราะห์แบบ จากล่างขึ้นบน (bottom-up parser ) ที่ตีความไวยากรณ์ ลำดับความสำคัญของตัวดำเนินการ ตัวอย่างเช่น เครื่องคิดเลขส่วนใหญ่ใช้ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการเพื่อแปลงจากสัญกรณ์แบบอินฟิกซ์ ที่มนุษย์อ่านได้ ซึ่งอาศัยลำดับการดำเนินการไปเป็นรูปแบบที่เหมาะสมที่สุดสำหรับการประเมินผล เช่นสัญกรณ์แบบ Reverse Polish Notation (RPN)
อัลกอริทึมลานสับเปลี่ยนขบวนรถของEdsger Dijkstraมักถูกนำมาใช้ในการสร้างตัวแยกวิเคราะห์ลำดับความสำคัญของผู้ปฏิบัติงาน
ความสัมพันธ์กับตัวแยกวิเคราะห์อื่นๆ
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการคือตัวแยกวิเคราะห์แบบ shift-reduce ที่ เรียบง่าย ซึ่งสามารถแยกวิเคราะห์ไวยากรณ์LR(1)บางส่วนได้ กล่าวคือ ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการสามารถแยกวิเคราะห์ไวยากรณ์ LR(1) ทั้งหมดได้ โดยที่nonterminal สองตัวที่อยู่ติดกัน และepsilonจะไม่ปรากฏในด้านขวามือของกฎใดๆ
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการไม่ค่อยได้ใช้ในทางปฏิบัติบ่อยนัก อย่างไรก็ตาม มันมีคุณสมบัติบางอย่างที่ทำให้มีประโยชน์ในการออกแบบที่ใหญ่กว่า ประการแรก มันง่ายพอที่จะเขียนด้วยมือ ซึ่งโดยทั่วไปแล้วจะไม่เป็นเช่นนั้นสำหรับตัวแยกวิเคราะห์แบบเลื่อนขวาและลดรูปที่ซับซ้อนกว่า ประการที่สอง สามารถเขียนให้ตรวจสอบตารางตัวดำเนินการในขณะรันไทม์ได้ซึ่งทำให้เหมาะสำหรับภาษาที่สามารถเพิ่มหรือเปลี่ยนแปลงตัวดำเนินการในขณะที่ทำการแยกวิเคราะห์ได้ (ตัวอย่างเช่นHaskellซึ่งอนุญาตให้ผู้ใช้กำหนดตัวดำเนินการแบบอินฟิกซ์ที่มีการเชื่อมโยงและลำดับความสำคัญที่กำหนดเองได้ ดังนั้นจึงต้องเรียกใช้ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการกับโปรแกรมหลังจากแยกวิเคราะห์โมดูลที่อ้างอิงทั้งหมดแล้ว)
Rakuแทรกตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการไว้ระหว่างตัวแยกวิเคราะห์แบบเรียกซ้ำ สองตัว เพื่อให้ได้สมดุลระหว่างความเร็วและความยืดหยุ่นตัวแยกวิเคราะห์ C และ C++ ของGCC ซึ่งเป็นตัวแยกวิเคราะห์แบบเรียกซ้ำที่เขียนด้วยมือ ต่างก็ได้รับการเร่งความเร็วด้วยตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการที่สามารถตรวจสอบนิพจน์ทางคณิตศาสตร์ได้อย่างรวดเร็ว นอกจากนี้ ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการยังถูกฝังอยู่ภายในตัวแยกวิเคราะห์ที่สร้าง โดยคอมไพเลอร์เพื่อเร่งความเร็ววิธีการเรียกซ้ำในการแยกวิเคราะห์นิพจน์อย่างเห็นได้ชัด[ 1 ]
วิธีการไต่ระดับลำดับความสำคัญ
วิธีการไต่ลำดับความสำคัญเป็นอัลกอริทึมที่กระชับ มีประสิทธิภาพ และยืดหยุ่นสำหรับการแยกวิเคราะห์นิพจน์ ซึ่งได้รับการอธิบายครั้งแรกโดย Martin Richards และ Colin Whitby-Strevens [ 2 ]
ไวยากรณ์นิพจน์แบบอินฟิกซ์ใน รูปแบบ EBNFโดยทั่วไปจะมีลักษณะดังนี้:
นิพจน์ :: = นิพจน์ความเท่าเทียมกัน นิพจน์ความเท่าเทียมกัน :: = นิพจน์การบวก( ( ' == ' | '! = ' ) นิพจน์การบวก) * นิพจน์การบวก :: = นิพจน์การคูณ( ( '+' | '-' ) นิพจน์การคูณ) * นิพจน์การคูณ :: = หลัก( ( ' * ' | ' / ' ) หลัก) * หลัก :: = ' ( ' นิพจน์ ' ) ' | ตัวเลข | ตัวแปร | '-' หลักเนื่องจากมีลำดับความสำคัญหลายระดับ การนำไวยากรณ์นี้ไปใช้กับตัวแยกวิเคราะห์แบบเรียกซ้ำเชิงทำนายอาจไม่มีประสิทธิภาพ ตัวอย่างเช่น การแยกวิเคราะห์ตัวเลขอาจต้องเรียกใช้ฟังก์ชันถึงห้าครั้ง: ครั้งละหนึ่งครั้งสำหรับแต่ละสัญลักษณ์ที่ไม่ใช่เทอร์มินัลในไวยากรณ์ จนกว่าจะถึงสัญลักษณ์ หลัก
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการสามารถทำสิ่งเดียวกันได้อย่างมีประสิทธิภาพมากขึ้น[ 1 ] แนวคิดคือเราสามารถเชื่อมโยงการดำเนินการทางคณิตศาสตร์ไปทางซ้ายได้ตราบใดที่เราพบตัวดำเนินการที่มีลำดับความสำคัญเท่ากัน แต่เราต้องบันทึกผลลัพธ์ชั่วคราวเพื่อประเมินตัวดำเนินการที่มีลำดับความสำคัญสูงกว่า อัลกอริทึมที่นำเสนอในที่นี้ไม่จำเป็นต้องใช้สแต็กที่ชัดเจน แต่จะใช้การเรียกซ้ำเพื่อใช้งานสแต็กแทน
อัลกอริทึมนี้ไม่ใช่ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการอย่างแท้จริงเหมือนกับอัลกอริทึมลานสับเปลี่ยนของเอ็ดส์เกอร์ ไดจ์กสตรามันถือว่าสัญลักษณ์ ที่ไม่ใช่เทอร์มินัล หลักจะถูกแยกวิเคราะห์ในรูทีนย่อยที่แยกต่างหาก เช่นเดียวกับในตัวแยกวิเคราะห์แบบเรียกซ้ำ
รหัสเทียม
รหัสเทียมสำหรับอัลกอริธึมมีดังนี้ ตัวแยกวิเคราะห์เริ่มต้นที่ฟังก์ชันparse_expressionระดับความสำคัญมากกว่าหรือเท่ากับ 0
parse_expression() return parse_expression_1(parse_primary(), 0)
parse_expression_1(lhs, min_precedence) lookahead := peek next token ในขณะที่lookaheadเป็นตัวดำเนินการไบนารีที่มีลำดับความสำคัญ >= min_precedence op := lookahead เลื่อนไปยังโทเค็นถัดไป rhs := parse_primary () lookahead := peek next token ในขณะที่lookaheadเป็นตัวดำเนินการไบนารีที่มีลำดับความสำคัญมากกว่า มากกว่าopหรือตัวดำเนินการเชื่อมโยงทางขวา ซึ่งมีลำดับความสำคัญเท่ากับrhsของ op := parse_expression_1 ( rhs , ลำดับความสำคัญของop + (1 ถ้า ลำดับความสำคัญ ของ lookaheadมากกว่า มิฉะนั้น 0)) lookahead := ดูโทเค็นถัดไป lhs := ผลลัพธ์ของการใช้opกับตัวถูกดำเนินการlhsและrhs ส่งคืนlhs
โปรดทราบว่าในกรณีของกฎการผลิตแบบนี้ (ซึ่งตัวดำเนินการสามารถปรากฏได้เพียงครั้งเดียว):
นิพจน์ความเท่าเทียมกัน :: = นิพจน์การบวก( ' == ' | '! = ' ) นิพจน์การบวกต้องปรับเปลี่ยนอัลกอริธึมให้ยอมรับเฉพาะตัวดำเนินการไบนารีที่มีลำดับความสำคัญมากกว่าmin_precedenceเท่านั้น
ตัวอย่างการทำงานของอัลกอริธึม
ตัวอย่างการประมวลผลนิพจน์ 2 + 3 * 4 + 5 == 19 มีดังนี้ เรากำหนดลำดับความสำคัญ 0 ให้กับนิพจน์ความเท่าเทียมกัน 1 ให้กับนิพจน์การบวก และ 2 ให้กับนิพจน์การคูณ
parse_expression_1 ( lhs = 2, min_precedence = 0)
- โทเค็นการมองล่วงหน้าคือ + โดยมีลำดับความสำคัญ 1 ลูป while ภายนอกจึงเริ่มทำงาน
- opคือ + (ลำดับความสำคัญ 1) และอินพุตคือ advanced
- rhsคือ 3
- โทเค็นการมองล่วงหน้าคือ * โดยมีลำดับความสำคัญ 2 ลูป while ภายในถูกเรียกใช้parse_expression_1 ( lhs = 3, min_precedence = 2)
- โทเค็นการมองล่วงหน้าคือ * โดยมีลำดับความสำคัญ 2 ลูป while ภายนอกจึงเริ่มทำงาน
- opคือ * (ลำดับความสำคัญ 2) และอินพุตเป็นขั้นสูง
- rhsคือ 4
- โทเค็นถัดไปคือ + ซึ่งมีลำดับความสำคัญ 1 ลูป while ภายในจะไม่ถูกเรียกใช้งาน
- lhsถูกกำหนดให้เป็น 3*4 = 12
- โทเค็นถัดไปคือ + ซึ่งมีลำดับความสำคัญ 1 ลูป while ภายนอกจึงอยู่ทางซ้าย
- 12 ได้รับการส่งคืนแล้ว
- โทเค็นการมองล่วงหน้าคือ + โดยมีลำดับความสำคัญ 1 ลูป while ภายในจะไม่ถูกเรียกใช้งาน
- lhsถูกกำหนดให้เป็น 2+12 = 14
- โทเค็นการมองล่วงหน้าคือ + โดยมีลำดับความสำคัญ 1 ลูป while ภายนอกไม่ได้อยู่ทางซ้าย
- opคือ + (ลำดับความสำคัญ 1) และอินพุตคือ advanced
- rhsคือ 5
- โทเค็นถัดไปคือ == ซึ่งมีลำดับความสำคัญ 0 ลูป while ภายในจะไม่ถูกเรียกใช้งาน
- lhsได้รับการกำหนดค่า 14+5 = 19
- โทเค็นถัดไปคือ == ซึ่งมีลำดับความสำคัญ 0 ลูป while ภายนอกไม่ได้ออกจากด้านซ้าย
- opคือ == (ลำดับความสำคัญ 0) และอินพุตคือ advanced
- rhsคือ 19
- โทเค็นถัดไปคือสิ้นสุดบรรทัดซึ่งไม่ใช่ตัวดำเนินการ ลูป while ภายในจึงไม่ถูกเรียกใช้งาน
- ตัวแปร lhsจะได้รับค่าผลลัพธ์จากการประเมิน 19 == 19 เช่น 1 (ตามมาตรฐานภาษา C)
- โทเค็นถัดไปคือend-of-lineซึ่งไม่ใช่ตัวดำเนินการ ลูป while ภายนอกจึงอยู่ทางซ้าย
ส่งคืนค่า 1
การแยกวิเคราะห์ของแพรตต์
ตัวแยกวิเคราะห์ลำดับความสำคัญอีกตัวหนึ่งที่เรียกว่าการแยกวิเคราะห์แบบ Pratt ได้รับการอธิบายครั้งแรกโดยVaughan Prattในบทความปี 1973 เรื่อง "Top Down Operator Precedence" [ 3 ]โดยอิงจากการลดระดับแบบเรียกซ้ำแม้ว่าจะมีมาก่อนการไต่ระดับลำดับความสำคัญ แต่ก็สามารถมองได้ว่าเป็นการวางนัยทั่วไปของการไต่ระดับลำดับความสำคัญ[ 4 ]
Pratt ออกแบบตัวแยกวิเคราะห์เพื่อใช้งาน ภาษาโปรแกรม CGOL ในตอนแรก และมีการกล่าวถึงในรายละเอียดมากขึ้นในวิทยานิพนธ์ระดับปริญญาโทภายใต้การดูแลของเขา[ 5 ]
บทแนะนำและการนำไปใช้งาน:
- Douglas Crockford ใช้การแยกวิเคราะห์ แบบ Pratt เป็นพื้นฐานสำหรับตัวแยกวิเคราะห์JavaScript ในJSLint [ 6 ]
- การเปรียบเทียบการใช้งานการจัดลำดับความสำคัญ (precedence climbing) และการแยกวิเคราะห์แบบ Pratt ในภาษา Python: "การแยกวิเคราะห์แบบ Pratt และการจัดลำดับความสำคัญเป็นอัลกอริทึมเดียวกัน" (2016) โดย Andy Chu
- บทช่วยสอนโดยใช้Rust : "การแยกวิเคราะห์ Pratt ที่เรียบง่าย แต่ทรงพลัง" (2020) โดย Aleksey Kladov
- คู่มือการใช้งานRust : "เทคนิคการวิเคราะห์ไวยากรณ์แบบ Pratt" (2024) โดย William Rågstad
- คู่มือการใช้งานPython : "การแยกวิเคราะห์แบบ Top-Down อย่างง่ายใน Python" (2008) โดย Fredrik Lundh เก็บถาวรเมื่อ 28 กุมภาพันธ์ 2015 ที่Wayback Machine
- คู่มือการใช้งานJava : "Pratt Parsers: Expression Parsing Made Easy" (2011) โดย Bob Nystrom ผู้เขียนหนังสือ Crafting Interpreters
- การใช้งานในC# : "Gratt: ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการแบบบนลงล่างของ Vaughn Pratt ทั่วไปสำหรับ .NET Standard" ( เวอร์ชัน ทั่วไปที่ได้รับแรงบันดาลใจจากการใช้งานใน Java ที่นำเสนอโดย Bob Nystrom ใน"Pratt Parsers: Expression Parsing Made Easy" )
วิธีการทางเลือก
ยังมีวิธีอื่นๆ ในการใช้กฎลำดับความสำคัญของตัวดำเนินการ วิธีหนึ่งคือการสร้างโครงสร้างต้นไม้ของนิพจน์ดั้งเดิม แล้วจึงใช้กฎการเขียนใหม่ของโครงสร้างต้นไม้นั้นกับมัน
ต้นไม้ดังกล่าวไม่จำเป็นต้องสร้างขึ้นโดยใช้โครงสร้างข้อมูลที่ใช้กันทั่วไปสำหรับต้นไม้เสมอไป แต่สามารถจัดเก็บโทเค็นไว้ในโครงสร้างแบบแบน เช่น ตาราง ได้ โดยการสร้างรายการลำดับความสำคัญไปพร้อมกัน ซึ่งระบุว่าควรประมวลผลองค์ประกอบใดในลำดับใด
การใส่วงเล็บเต็ม
อีกแนวทางหนึ่งคือการใส่วงเล็บให้กับนิพจน์ทั้งหมดก่อน โดยใส่วงเล็บจำนวนหนึ่งรอบตัวดำเนินการแต่ละตัว เพื่อให้ได้ลำดับความสำคัญที่ถูกต้องแม้ว่าจะแยกวิเคราะห์ด้วยตัวแยกวิเคราะห์เชิงเส้นจากซ้ายไปขวา อัลกอริทึมนี้ถูกใช้ใน คอมไพเลอร์ FORTRAN I รุ่นแรกๆ : [ 7 ]
คอมไพเลอร์ Fortran I จะขยายโอเปอเรเตอร์แต่ละตัวด้วยลำดับของวงเล็บ ในรูปแบบที่ง่ายขึ้นของอัลกอริทึม มันจะเป็นเช่นนี้
- แทนที่
+และ–ด้วย))+((และ))-((ตามลำดับ- แทนที่
*และ/ด้วย)*(และ)/(ตามลำดับ- เพิ่ม
((ที่จุดเริ่มต้นของแต่ละนิพจน์และหลังวงเล็บเปิดแต่ละอันในนิพจน์เดิม และ- เพิ่ม
))ต่อท้ายนิพจน์และก่อนวงเล็บปิดแต่ละอันในนิพจน์เดิมแม้จะไม่ชัดเจน แต่อัลกอริทึมนั้นถูกต้อง และตามคำพูดของKnuth “สูตรที่ได้นั้นมีวงเล็บที่เหมาะสม เชื่อหรือไม่ก็ตาม” [ 8 ]
ตัวอย่างโค้ดของแอปพลิเคชัน C อย่างง่ายที่จัดการการใส่วงเล็บให้กับตัวดำเนินการทางคณิตศาสตร์พื้นฐาน ( +, -, *, /, ^, (และ)):
#include <stdio.h> #include <string.h>// ขอบเขตอาร์กิวเมนต์บรรทัดคำสั่งคือตัวแยกวิเคราะห์ของเราint main ( int argc , char * argv []) { int i ; printf ( "((((" ); for ( i = 1 ; i != argc ; i ++ ) { // strlen(argv[i]) == 2 if ( argv [ i ] && ! argv [ i ][ 1 ]) { switch ( * argv [ i ]) { case '(' : printf ( "((((" ); continue ; case ')' : printf ( "))))" ); continue ; case '^' : printf ( ")^(" ); continue ; case '*' : printf ( "))*((" ) ; continue ; case '/' : printf ( "))/((" ); continue ; case '+' : // ตรวจสอบเอกภาค: ตัวแรกหรือมีตัวดำเนินการที่คาดหวังอาร์กิวเมนต์รองif ( i == 1 || strchr ( "(^*/+-" , * argv [ i -1 ])) printf ( "+" ); else printf ( ")))+(((" ); continue ; case '-' : if ( i == 1 || strchr ( "(^*/+-" , * argv [ i -1 ])) printf ( "-" ); else printf ( ")))-(((" ); continue ; } } printf ( "%s" ,argv [ i ]); } printf ( ")))) \n " ); return0 ; }ขั้นแรก คุณต้องคอมไพล์โปรแกรมของคุณก่อน สมมติว่าโปรแกรมของคุณเขียนด้วยภาษา C และซอร์สโค้ดอยู่ในไฟล์ชื่อ program.c คุณจะใช้คำสั่งต่อไปนี้:
gcc program.c -o program
คำสั่งข้างต้นบอกให้ gcc คอมไพล์ไฟล์ program.c และสร้างไฟล์ปฏิบัติการชื่อ program
คำสั่งสำหรับเรียกใช้โปรแกรมพร้อมพารามิเตอร์ ตัวอย่างเช่น; a * b + c ^ d / e
./program a '*' b + c '^' d / e
มันผลิต
((((a))*((b)))+(((c)^(d))/((e))))
แสดงผลออกทางคอนโซล
ข้อจำกัดของกลยุทธ์นี้คือ ตัวดำเนินการเอกภาคจะต้องมีลำดับความสำคัญสูงกว่าตัวดำเนินการอินฟิกซ์ทั้งหมด ตัวดำเนินการ "ลบ" ในโค้ดข้างต้นมีลำดับความสำคัญสูงกว่าการยกกำลัง การรันโปรแกรมด้วยข้อมูลป้อนเข้านี้
- a ^ 2
สร้างผลลัพธ์นี้
((((-a)^(2))))
ซึ่งอาจไม่ใช่สิ่งที่ตั้งใจไว้
ลิงก์ภายนอก
- Clarke, Keith (26 พฤษภาคม 1992). "เรื่อง: การแยกวิเคราะห์นิพจน์แบบเรียกซ้ำที่กระชับ" . สืบค้นเมื่อ 24 มกราคม 2012 .
- ตัวอย่างโค้ด C++ โดย Keith Clarke สำหรับการแยกวิเคราะห์นิพจน์แบบอินฟิกซ์โดยใช้วิธีการไต่ลำดับความสำคัญ
- Samelson, Klaus ; Friedrich L. Bauer (กุมภาพันธ์ 1960). "การแปลสูตรตามลำดับ" . Communications of the ACM . 3 (2): 76– 83. doi : 10.1145/366959.366968 .
- ตัวแยกวิเคราะห์นิพจน์ที่มีสัญกรณ์แบบอินฟิกซ์โดยใช้รายการลำดับความสำคัญ
สรุปเนื้อหา
ข้อมูลสำคัญจากบทความ
ข้อมูลสำคัญเกี่ยวกับ ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการ
ใน วิทยาการคอมพิวเตอร์ ตัวแยก วิเคราะห์ ลำดับความสำคัญของตัวดำเนินการ (operator-precedence parser) คือตัว แยกวิเคราะห์แบบ จากล่างขึ้นบน (bottom-up parser ) ที่ตีความไวยากรณ์...
ความสัมพันธ์กับตัวแยกวิเคราะห์อื่นๆ
ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการคือ ตัวแยกวิเคราะห์แบบ shift-reduce ที่ เรียบง่าย ซึ่งสามารถแยกวิเคราะห์ไวยากรณ์ LR(1) บางส่วนได้ กล่าวคือ ตัวแยกวิเคราะห์ลำดับความสำคัญของตัวดำเนินการสามารถแยกวิเคราะห์ไวยากรณ์ LR(1) ทั้งหมดได้ โดยที่ nonterminal...
วิธีการไต่ระดับลำดับความสำคัญ
วิธีการไต่ลำดับความสำคัญเป็นอัลกอริทึมที่กระชับ มีประสิทธิภาพ และยืดหยุ่นสำหรับการแยกวิเคราะห์นิพจน์ ซึ่งได้รับการอธิบายครั้งแรกโดย Martin Richards และ Colin Whitby-Strevens [ 2 ]
รหัสเทียม
รหัสเทียมสำหรับอัลกอริธึมมีดังนี้ ตัวแยกวิเคราะห์เริ่มต้นที่ฟังก์ชัน parse_expression ระดับความสำคัญมากกว่าหรือเท่ากับ 0