270 likes | 588 Views
תורת הקומפילציה 236360 הרצאה 4 ניתוח תחבירי ( Parsing ) של דקדוקי LL(1). Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4 Cooper and Torczon – Chapter 3. תזכורת: front-end שלב הניתוח. תוכנית מקור Back end. Lexical Analysis. token string. syntax analysis Parsing.
E N D
תורת הקומפילציה 236360הרצאה 4 ניתוח תחבירי (Parsing) של דקדוקי LL(1) Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4 Cooper and Torczon – Chapter 3
תזכורת:front-end שלב הניתוח תוכנית מקור Back end Lexical Analysis token string syntax analysis Parsing symbol table error messages parse tree semantic analysis decorated syntax tree
האינטרקציה בין המנתח לקסיקלי וה-parser תוכנית מקור error message manager Lexical analysis get next token token parser
תזכורת: סוגי הניתוח התחבירי • top-down – מהשורש לעלים (נקרא גם – "ניתוח תחזית" – predictive) • bottom-up – מהעלים לשורש – מעבירים למחסנית, או מחליפים צד ימין בסימן מהצד השמאלי של חוק הדקדוק (shift reduce) s x y s x y
ניתוח top-down תוך כדי הפעלת פונקציות:Recursive Descent • אלגוריתם Recursive Descent מתרגם דקדוק באופן הבא: • מטרה:להתחיל במשתנה התחילי ולמצוא גזירה. • עבור כל משתנה בדקדוק (nonterminal) מגדירים פונקציה. • המנתח מתחיל לפעול מהפונקציה המתאימה ל-nonterminal התחילי. • כל פונקציה מחקה את החוק שעבורו הוגדרה, באופן הבא: • terminal מתורגם לקריאת האסימון המתאים מהקלט. • nonterminal מתורגם להפעלת הפונקציה המתאימה לו. • אם ישנם כמה חוקי גזירה עבור אותו nonterminal, בוחרים ביניהם בעזרת lookahead. • הקבוצה first עבור כלל גזירה Ai→ β מכילה את קבוצת הטרמינלים שעשויים להופיע ראשונים בגזירה כלשהי של β.
תזכורת: כתיבת פונקציות בהתאם לדקדוק 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; }
התאמת הדקדוק ל-Recursive Descent • לא כל דקדוק ניתן לגזירה יעילה top-down. כאשר הדקדוק לא מתאים מנסים לתקן אותו. • בעיה לדוגמא: רקורסיה שמאלית. • ביטול רקורסיה ישירה: נחליף את הכללים • A → Aα1 | Aα2 | ··· | Aαn | β1 | β2 | ··· | βn • בכללים • A → β1A’ | β2A’ | ··· | βnA’ • A’ → α1A’ | α2A’| ··· | αnA’ | Є • אבל יש גם רקורסיה עקיפה. למשל: • S → Aa | b • A → Ac | Sd | Є • ועבורה האלגוריתם מעט יותר מורכב.
אלגוריתם להעלמת רקורסיה שמאלת (עקיפה וישירה) מדקדוק • קלט:דקדוק G שאולי יש בו רקורסיה שמאלית, ללא מעגלים, וללא כללי ε. • פלט:דקדוק שקול ללא רקורסיה שמאלית. • דוגמא לכלל אפסילון: A → Є. • דוגמא למעגל: A → B; B → A;. • ניתן לבטל כללי אפסילון ומעגלים בדקדוק (באופן אוטומטי). • רעיון האלגוריתם לסילוק רקורסיה שמאלית:נסדר את המשתנים לפי סדר כלשהו:A1, A2, …, An • נעבור על המשתנים לפי הסדר, ולכל Ai נדאג שכל כלל שלו יהיה מהצורה • Ai→ Ajβ with i > j . • מדוע זה מספיק?
אלגוריתם להעלמת רקורסיה שמאלת (עקיפה וישירה) מדקדוק • Input: Grammar G possibly left-recursive, no cycles, no ε productions. • Output: An equivalent grammar with no left-recursion • Method: Arrange the nonterminals in some order A1, A2, …, An • for i:=1 to n do begin for s:=1 to i-1 do begin replace each production of the form Ai → Asβ by the productions Ai→ d1β |d2β|…|dkβ where As-> d1 | d2 | …| dk are all the current As-productions; end eliminate immediate left recursion among the Ai-productionsend
ניתוח האלגוריתם • נראה שבסיום האלגוריתם כל חוק גזירה מהצורה Ak→Atβ מקיים t > k . • שמורה 1: כשגומרים את הלולאה הפנימית עבור s כלשהו (עם Ai בלולאה החיצונית) אז כל כללי הגזירה של Ai מתחילים בטרמינלים, או במשתנים Aj עבורם j>s. • שמורה 2:כשמסיימים עם המשתנה Ai, כל כללי הגזירה שלו מתחילים במשתנים Aj עבורם j>i או בטרמינלים. הוכחת שתי השמורות יחד באינדוקציה על i ו-s. • מסקנה:בסיום האלגוריתם אין רקורסיה שמאלית (ישירה או עקיפה). נובע משמורה 2.
הערות • מדוע לא עובד אם יש כלל ε? • כי אם יש:A5→ A4A3 אז אנו דואגים שה-A4 ייעלם מהתחלת הכלל ויוחלף, למשל ב-A6, ואז יכול להתקבל A5→ A6A3. אבל אם A6→ε, אז בעצם ניתן לגזור A5→ A3 ואז השמורות לא תקיפות ועלולים לקבל רקורסיה שמאלית עקיפה. בעיקרון, טיפלנו רק במשתנה השמאלי ואסור לו להיעלם!
הקטנת הצורך ב-lookahead בעזרת Left Factoring • בעיה נוספת של Recursive Descent היא התנגשויות ב-FIRST של כללי גזירה שונים לאותו משתנה. • הפתרון: Left Factoring – פירוק שמאלי, אלגוריתם המפיק דקדוק חלופי ללא הבעיה. • אינטואיציה:כאשר לא ברור כרגע איך להחליט בין שני כללים, ניצור כלל משותף לתחילת הגזירה ונפריד לשני כללים בהמשך. למשל:
אלגוריתםLeft Factoring • Input: Grammar G • Output: An equivalent left-factored grammar • Method: For each nonterminal A find the longest (non empty) prefix a common to two or more of its production rules. Namely: A →α b1 | α b2 | …| α bn. Replace all the A productions A →α b1 | α b2 | …| αbnby A →α A’ A’→ b1 | b2 | … | bn (A’ is a new nonterminal) • Repeatedly apply this transformation until no such common prefix exists.
עוד טרנספורמציות ? • קיימות עוד טרנספורמציות שמטרתן לייצר דקדוק ללא התנגשויות ב-FIRST. • הטרנספורמציות הללו מאפשרות גזירת top-down של שפות רבות. • אפשר לגזור כל דקדוק שעבר "טיפול" כזה בהצלחה בעזרת Recursive Descent. • ישנן תכונות של שפות שלא ניתנות לזיהוי ע"י שום דקדוק חסר הקשר. למשל, הדרישה של C ו-Java שכל משתנה יוגדר לפני השימוש בו. • w2w | w is in (0|1)* אבסטרקציה של הבעיה: • בדיקת דרישות כאלו תיכלל בניתוח הסמנטי.
מחלקת הדקדוקים LL(k) • דקדוק הוא במחלקה LL(k) אם הוא ניתן לגזירה: • top-down, • סורקת את הקלט משמאל (L) לימין, • מניבה את הגזירה השמאלית (L) ביותר, • וזקוקה ל-lookahead בגודל k. • שפה היא LL(k)אם יש לה דקדוק LL(k). • המקרה הפשוט ביותר הוא אלגוריתם LL(1).
אלגוריתם הגזירה • ניתן למצוא גזירת מילה לדקדוק LL(1) באמצעות Recursive Descent שהוא האלגוריתם הכללי לגזירת top-down. • אבל בד"כ משתדלים להשתמש באלגוריתם ישיר מבוסס טבלה: • מאפשר לעבוד עם "טבלת דקדוק". • מסיר רקורסיה מהמערכת. הרקורסיה מוחלפת במחסנית המכילה את התבנית שנותר לגזור. • אלגוריתמים מבוססי-טבלה לניתוח שפות LL(k) ידועים בשם LL(k) parsers.
טבלת המעברים • ב-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(1) משתמש בטבלה ומחסנית נשתמש במחסנית לשמור את התבנית הפסוקית שעוד נותר לגזור. a * c + b $ קלט Parser טבלת מעברים מחסנית פלט
מבנה נתונים בזמן ריצת האלגוריתם: מחסנית: if ( E ) then Stmt else Stmt ; Stmt ; … $ top Remaining Input: if ( id < id ) then id = id + num else break; id = id * id; …
האלגוריתם • אתחול המחסנית: המשתנה (nonterminal) התחילי, ו-$ (סימן לסוף הקלט). • המחסנית יכולה להכיל אסימונים (terminals) או משתנים. "$" הוא אסימון מיוחד, לצורך סימון סוף הקלט. • אם בראש המחסנית יש אסימון: • אם האסימון הבא בקלט אינו זהה: שגיאה. • אם הוא תואם את הקלט: עבור לאסימון הקלט הבא; הסר את האסימון מהמחסנית. (אם האסימון הוא $, סיימנו). • אם בראש המחסנית יש משתנה: • מצא את התא בטבלה המתאים למשתנה זה ולתו שבראש הקלט. • אם התא ריק:שגיאה. • אחרת:הסר את המשתנה מראש המחסנית; הוסף למחסנית את צד ימין של כלל הגזירה שנמצא בטבלה, לפי סדר – החל באסימון/משתנה הימני ביותר וכלה באסימון/משתנה השמאלי ביותר (הוא ישאר בראש המחסנית).
בניית הטבלה • בתרגול ראיתם איך בונים את First ו-follows, והם ישמשו לבניית הטבלה. • הבניה עצמה תפורט בתרגול.
דוגמא טריביאלית (בתרגול דוגמאות רבות) דקדוק: A →aAb | c טבלה: • נריץ את האלגוריתם על המילה aacbb: • איתחול מחסנית: A$, ומתחילים מאות הקלט הראשונה.
אלגוריתמים LL(k) • עבור k>1, הטבלה הנדרשת לאלגוריתם LL(k) היא (במקרה הגרוע) בעלת סיבוכיות אקספוננציאלית ב-k. • לכן, עד לא מזמן האמינו שלא יהיה מעשי לבנות parsers מעשיים לדקדוקי , LL(k) עבור k-ים יותר גדולים. • בתחילת שנות התשעים הדגימו חוקרים מאוניברסיטת Purdue (ארה"ב) שהמקרה הגרוע הוא למעשה נדיר, וניתן לבנות parsers פרקטיים עם LL(k). • הכלי שפיתחו נקרא כיום ANTLR. • כלים אחרים המבוססים על LL(k): JavaCC (משמש לבניית מהדרים ב-Java, כולל מהדר javac עצמו), SableCC (גם הוא ב-Java), ואחרים.
LL(k) or not LL(k) ? • השפה an(b|c)nהיא שפה ב-(1)LL; "דקדוק טבעי":נבצע left-factoring ונקבל את הדקדוק:(*) מה לגבי השפות?anbn|ancn ?an(a|b)n • דקדוק/שפה שאינם LL(k): S1 → aS1b | aS1c | ε S1 → aS1X | ε X → b|c S → A | B A → aAb | c B → aBbb | d ancbn| andb2n
LL(k) or not LL(k) ? S → akb | akc • דקדוק ב-LL(k+1) שאיננו ב-(LL(k:left-factoring) יניב דקדוק שקול ב-LL(1).) • דקדוק/שפה ב-LL(k+1) שאיננה ב-LL(k):רק כשרואים b או c יודעים שצריך להפעיל את המעבר של S ל-ε. S → aSA | ε A → akbS | c