260 likes | 459 Views
ניתוח תחבירי ( Parsing ) - המשך. Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4. תזכורת: סוגי הניתוח התחבירי. top-down – מהשורש לעלים (נקרא גם – "ניתוח תחזית" – predictive )
E N D
ניתוח תחבירי (Parsing) - המשך Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4
תזכורת: סוגי הניתוח התחבירי • top-down – מהשורש לעלים (נקרא גם – "ניתוח תחזית" – predictive) • bottom-up – מהעלים לשורש – מעבירים למחסנית, או מחליפים צד ימין בסימן מהצד השמאלי של חוק הדקדוק (shift reduce) s x y s x y
תזכורת: Recursive Descent • אלגוריתם Recursive Descent מתרגם דקדוק לקוד באופן הבא: • עבור כל nonterminal מגדירים פונקציה. • המנתח מתחיל לפעול מהפונקציה המתאימה ל-nonterminal הראשון. • כל פונקציה מחקה את החוק שעבורו הוגדרה, באופן הבא: • terminal מתורגם לקריאת הלקסמה המתאימה מהקלט. • nonterminal מתורגם להפעלת הפונקציה המתאימה לו. • אם ישנם כמה חוקי גזירה עבור אותו nonterminal, בוחרים ביניהם בעזרת lookahead.
תזכורת: כתיבת פונקציות בהתאם לדקדוק void E() { if (lookahead {TRUE, FALSE}) LIT(); else if (lookahead = LPAREN) match(LPARENT); E(); OP(); E(); match(RPAREN); else if (lookahead = NOT) match(NOT); E(); else error; } E → LIT | ( E OP E ) | not E LIT → true | false OP → and | or | xor void LIT() { if (lookahead = TRUE) match(TRUE); else if (lookahead = FALSE) match(FALSE); else error; } void OP() { if (lookahead = AND) match(AND); else if (lookahead = OR) match(OR); else if (lookahead = XOR) match(XOR); else error; }
תזכורת: בעיות • איך מתגברים על כללי-ε, או כללים המתחילים ב- ε? • אם יש ε ב-FIRST, זו הופכת להיות ברירת המחדל. • מה קורה עם רקורסיה שמאלית? E → E A E | ( E ) | – E | id A → + | – | * | / | ^ • מחליפים את הדקדוק. • לכל דקדוק עם רקורסיה שמאלית מיידית יש דקדוק שקול נטול רקורסיה שמאלית.
איך זה עוזר לנו? • יופי – יש לנו קוד עם פונקציה מתאימה לכל כלל גזירה. • הקוד מופעל בהתאם לגזירת הקלט מהדקדוק הנתון. • אבל איך זה עוזר לנו לקבל, למשל, עץ גזירה?
הוספת פעולות במהלך הגזירה • בכל פעם שנקראת אחת הפונקציות (למשל, E(), LIT() ו-OP() בדוגמא שלנו), פירוש הדבר ש"איתרנו" צעד בגזירה. • בכל צעד כזה ניתן לבצע פעולות שונות! • בפרט, ניתן די בקלות לבנות עץ בעזרת הפעולות הללו.
הוספת פעולות במהלך הגזירה • דרך אחת לקבל עץ מהפונקציות הקיימות: • כל פונקציה מחזירה רשומה מסוג Node (צומת בעץ). • כל רשומה כזו מכילה רשימה של בנים. • בכל קריאה לפונקציה אחרת (או ל-match), מוסיפים את תוצאת הקריאה ל-Node שנבנה כעת.
הוספת פעולות במהלך הגזירה Node E() { result = new Node(); if (lookahead {TRUE, FALSE}) // E → LIT result.addChild(LIT()); else if (lookahead = LPAREN) // E → ( E OP E ) result.addChild(match(LPARENT)); result.addChild(E());result.addChild(OP()); result.addChild(E()); result.addChild(match(RPAREN)); else if (lookahead = NOT) // E → not E result.addChild(match(NOT));result.addChild(E()); else error; return result; }
הוספת פעולות במהלך הגזירה • ואז, למשל: input = “(not true and false)”; Node treeRoot = E(); E ( E OP E ) LIT not LIT and true false
הוספת פעולות במהלך הגזירה • כאמור, בפועל בד"כ לא באמת בונים עץ. • ייצוג של התוכנית כולה בזכרון יכול להיות מסובך ו"כבד". • אבל באופן שקול, ניתן לבצע כל פעולה שהיא בפונקציות השונות המתקבלות באלגוריתם RD . • פעולות אלה תייצרנה, בסופו של דבר, את הפלט של ה-parser.
התאמת הדקדוק ל-RD • לא כל דקדוק מתאים ל-RD. • הבעיה הקשה: רקורסיה שמאלית. • כזכור, קל לבטל רקורסיה שמאלית ישירה: • מה לגבי רקורסיה שמאלית עקיפה? S → Aa | b A → Ac | Sd | ε
ביטול רקורסיה שמאלית עקיפה: אלגוריתם עשוי לא לעבוד אם הדקדוק מכיל כללי ε ו/או לולאות.
הקטנת הצורך ב-lookahead בעזרת Left Factoring • בעיה נוספת של RD היא התנגשויות ב-FIRST של כללי גזירה שונים לאותו משתנה. • הפתרון: Left Factoring – פירוק שמאלי, אלגוריתם המפיק דקדוק חלופי ללא הבעיה. • למשל:
וזהו? • קיימות טרנספורמציות המייצרות דקדוק ללא רקורסיה שמאלית וללא התנגשויות ב-FIRST מדקדוקים רבים. • אפשר לגזור כל דקדוק שעבר "טיפול" כזה בהצלחה בעזרת RD. • מסקנה: אפשר לגזור שפות רבות ושונות בעזרת RD. • לשם מה אנו זקוקים לאלגוריתמים אחרים?
אלגוריתם LL(k) • אלגוריתם LL(k) הוא אלגוריתם: • top-down, • מבוסס טבלה, • סורק את הקלט משמאל (L) לימין, • מניב את הגזירה השמאלית (L) ביותר, • וזקוק ל-lookahead בגודל k. • המקרה הפשוט ביותר הוא אלגוריתם LL(1).
שפות LL(k) • שפה נקראת LL(k) אם אפשר לגזור אותה בעזרת LL(k) parser. • אלגוריתם RD גוזר שפות LL(k) עבור k בלתי-חסום. • בדרך-כלל גוזרים שפות LL(k) בעזרת אלגוריתמים מבוססי-טבלה. אלגוריתמים אלו הם הידועים בשם LL(k) parsers.
אלגוריתם LL(1) קלט Parser טבלת מעברים מחסנית פלט
טבלת המעברים • ב-LL(1) משתמשים בטבלה המכתיבה, עבור כל מצב נתון, באיזה כלל גזירה להשתמש. • שורות הטבלה: כללי גזירה. • עמודות הטבלה: אסימונים אפשריים בקלט. • תוכן הטבלה: חוקי גזירה.
למשל... (1) E → LIT (2) E → ( E OP E ) (3) E → not E (4) LIT → true (5) LIT → false (6) OP → and (7) OP → or (8) OP → xor
האלגוריתם • אתחול המחסנית: המשתנה הראשון בדקדוק, ו-$ (סימן לסוף הקלט). • המחסנית יכולה להכיל אסימונים או משתנים. "$" הוא אסימון מיוחד, לצורך זה. • אם בראש המחסנית יש אסימון: • אם האסימון הבא בקלט אינו זהה: שגיאה. • אם הוא תואם את הקלט: צרוך את תו הקלט; הסר את האסימון מהמחסנית. (אם האסימון הוא $, סיימנו). • אם בראש המחסנית יש משתנה: • מצא את התא בטבלה המתאים למשתנה זה ולתו שבראש הקלט. • אם התא ריק:שגיאה. • אחרת:הסר את המשתנה מראש המחסנית; הוסף למחסנית את צד ימין של כלל הגזירה שנמצא בטבלה, לפי סדר – החל באסימון/משתנה הימני ביותר וכלה באסימון/משתנה השמאלי ביותר (הוא ישאר בראש המחסנית).
בניית הטבלה • ... בתרגול.
ריבוי-משמעויות בטבלה • דקדוקים מסוימים יגרמו לכך שבטבלה יהיו תאים עם יותר מערך יחיד. • אילו דקדוקים? • כיצד ניתן להתגבר על הבעיה?
אלגוריתמים LL(k) • עבור k>1, הטבלה הנדרשת לאלגוריתם LL(k) היא (במקרה הגרוע) בעלת סיבוכיות אקספוננציאלית. • לכן, עד לא מזמן האמינו שלא יהיה מעשי לבנות parsers לשפות תכנות "אמיתיות" בעזרת אלגוריתם זה. • לכן yacc, bison וחברים מבוססים על אלגוריתמים אחרים. • בתחילת שנות התשעים הדגימו חוקרים מאוניברסיטת Purdue (ארה"ב) שהמקרה הגרוע הוא למעשה נדיר, וניתן לבנות parsers פרקטיים עם LL(k). • הכלי שפיתחו נקרא כיום ANTLR. • כלים אחרים המבוססים על LL(k): JavaCC (משמש לבניית מהדרים ב-Java, כולל מהדר javac עצמו), SableCC (גם הוא ב-Java), ואחרים.