Download presentation
Presentation is loading. Please wait.
1
ניתוח תחבירי (Parsing) של דקדוקי LL(1)
תורת הקומפילציה הרצאה 4 ניתוח תחבירי (Parsing) של דקדוקי LL(1) Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4 Cooper and Torczon – Chapter 3
2
תזכורת:front-end שלב הניתוח
תוכנית מקור Back end Lexical Analysis token string symbol table syntax analysis Parsing error messages parse tree semantic analysis decorated syntax tree
3
האינטרקציה בין המנתח לקסיקלי וה-parser
תוכנית מקור error message manager Lexical analysis get next token token parser
4
תזכורת: סוגי הניתוח התחבירי
top-down – מהשורש לעלים (נקרא גם – "ניתוח תחזית" – predictive) bottom-up – מהעלים לשורש – מעבירים למחסנית, או מחליפים צד ימין בסימן מהצד השמאלי של חוק הדקדוק (shift reduce) s x y s x y
5
ניתוח top-down תוך כדי הפעלת פונקציות: Recursive Descent
מטרה: להתחיל במשתנה התחילי ולמצוא גזירה. עבור כל משתנה בדקדוק (nonterminal) מגדירים פונקציה. המנתח מתחיל לפעול מהפונקציה המתאימה ל-nonterminal התחילי. כל פונקציה מחקה את החוק שעבורו הוגדרה, באופן הבא: terminal מתורגם לקריאת האסימון המתאים מהקלט. nonterminal מתורגם להפעלת הפונקציה המתאימה לו. אם ישנם כמה חוקי גזירה עבור אותו nonterminal, בוחרים ביניהם בעזרת lookahead. הקבוצה first עבור כלל גזירה Ai → β מכילה את קבוצת הטרמינלים שעשויים להופיע ראשונים בגזירה כלשהי של β.
6
תזכורת: כתיבת פונקציות בהתאם לדקדוק
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; }
7
התאמת הדקדוק ל-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 | Є ועבורה האלגוריתם מעט יותר מורכב.
8
אלגוריתם להעלמת רקורסיה שמאלת (עקיפה וישירה) מדקדוק
קלט: דקדוק G שאולי יש בו רקורסיה שמאלית, ללא מעגלים, וללא כללי ε. פלט: דקדוק שקול ללא רקורסיה שמאלית. דוגמא לכלל אפסילון: A → Є. דוגמא למעגל: A → B; B → A;. ניתן לבטל כללי אפסילון ומעגלים בדקדוק (באופן אוטומטי). רעיון האלגוריתם לסילוק רקורסיה שמאלית: נסדר את המשתנים לפי סדר כלשהו: A1, A2, …, An נעבור על המשתנים לפי הסדר, ולכל Ai נדאג שכל כלל שלו יהיה מהצורה Ai → Ajβ with i > j . מדוע זה מספיק?
9
אלגוריתם להעלמת רקורסיה שמאלת (עקיפה וישירה) מדקדוק
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-productions end
10
ניתוח האלגוריתם נראה שבסיום האלגוריתם כל חוק גזירה מהצורה Ak → Atβ מקיים t > k . שמורה 1: כשגומרים את הלולאה הפנימית עבור s כלשהו (עם Ai בלולאה החיצונית) אז כל כללי הגזירה של Ai מתחילים בטרמינלים, או במשתנים Aj עבורם j>s. שמורה 2: כשמסיימים עם המשתנה Ai, כל כללי הגזירה שלו מתחילים במשתנים Aj עבורם j>i או בטרמינלים. הוכחת שתי השמורות יחד באינדוקציה על i ו-s. מסקנה: בסיום האלגוריתם אין רקורסיה שמאלית (ישירה או עקיפה). נובע משמורה 2.
11
הערות מדוע לא עובד אם יש כלל ε?
כי אם יש: A5 → A4A3 אז אנו דואגים שה-A4 ייעלם מהתחלת הכלל ויוחלף, למשל ב-A6, ואז יכול להתקבל A5 → A6A3. אבל אם A6 → ε, אז בעצם ניתן לגזור A5 → A3 ואז השמורות לא תקיפות ועלולים לקבל רקורסיה שמאלית עקיפה. בעיקרון, טיפלנו רק במשתנה השמאלי ואסור לו להיעלם!
12
הקטנת הצורך ב-lookahead בעזרת Left Factoring
בעיה נוספת של Recursive Descent היא התנגשויות ב-FIRST של כללי גזירה שונים לאותו משתנה. הפתרון: Left Factoring – פירוק שמאלי, אלגוריתם המפיק דקדוק חלופי ללא הבעיה. אינטואיציה: כאשר לא ברור כרגע איך להחליט בין שני כללים, ניצור כלל משותף לתחילת הגזירה ונפריד לשני כללים בהמשך. למשל: S → if E then S S’ | T S’→ else S | ε S → if E then S else S | if E then S
13
אלגוריתם 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 | …| α bn by A → α A’ A’ → b1 | b2 | … | bn (A’ is a new nonterminal) Repeatedly apply this transformation until no such common prefix exists.
14
עוד טרנספורמציות ? קיימות עוד טרנספורמציות שמטרתן לייצר דקדוק ללא התנגשויות ב-FIRST. הטרנספורמציות הללו מאפשרות גזירת top-down של שפות רבות. אפשר לגזור כל דקדוק שעבר "טיפול" כזה בהצלחה בעזרת Recursive Descent. ישנן תכונות של שפות שלא ניתנות לזיהוי ע"י שום דקדוק חסר הקשר. למשל, הדרישה של C ו-Java שכל משתנה יוגדר לפני השימוש בו. w2w | w is in (0|1)* אבסטרקציה של הבעיה: בדיקת דרישות כאלו תיכלל בניתוח הסמנטי.
15
אלגוריתם LL(1)
16
מחלקת הדקדוקים LL(k) דקדוק הוא במחלקה LL(k) אם הוא ניתן לגזירה:
top-down, סורקת את הקלט משמאל (L) לימין, מניבה את הגזירה השמאלית (L) ביותר, וזקוקה ל-lookahead בגודל k. שפה היא LL(k) אם יש לה דקדוק LL(k). המקרה הפשוט ביותר הוא אלגוריתם LL(1).
17
אלגוריתם הגזירה ניתן למצוא גזירת מילה לדקדוק LL(1) באמצעות Recursive Descent שהוא האלגוריתם הכללי לגזירת top-down. אבל בד"כ משתדלים להשתמש באלגוריתם ישיר מבוסס טבלה: מאפשר לעבוד עם "טבלת דקדוק". מסיר רקורסיה מהמערכת. הרקורסיה מוחלפת במחסנית המכילה את התבנית שנותר לגזור. אלגוריתמים מבוססי-טבלה לניתוח שפות LL(k) ידועים בשם LL(k) parsers.
18
טבלת המעברים ב-LL(1) משתמשים בטבלה המכתיבה, עבור כל מצב נתון, באיזה כלל גזירה להשתמש. שורות הטבלה: משתנים. עמודות הטבלה: אסימונים אפשריים בקלט. תוכן הטבלה: חוקי גזירה.
19
למשל... (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 $ xor or and false true not ) ( 1 3 2 E 5 4 LIT 8 7 6 OP
20
אלגוריתם LL(1) משתמש בטבלה ומחסנית
נשתמש במחסנית לשמור את התבנית הפסוקית שעוד נותר לגזור. a * c + b $ קלט Parser טבלת מעברים מחסנית פלט
21
מבנה נתונים בזמן ריצת האלגוריתם:
מחסנית: top if ( E ) then Stmt else Stmt ; Stmt ; … $ Remaining Input: if ( id < id ) then id = id + num else break; id = id * id; …
22
האלגוריתם אתחול המחסנית: המשתנה (nonterminal) התחילי, ו-$ (סימן לסוף הקלט). המחסנית יכולה להכיל אסימונים (terminals) או משתנים. "$" הוא אסימון מיוחד, לצורך סימון סוף הקלט. אם בראש המחסנית יש אסימון: אם האסימון הבא בקלט אינו זהה: שגיאה. אם הוא תואם את הקלט: עבור לאסימון הקלט הבא; הסר את האסימון מהמחסנית. (אם האסימון הוא $, סיימנו). אם בראש המחסנית יש משתנה: מצא את התא בטבלה המתאים למשתנה זה ולתו שבראש הקלט. אם התא ריק: שגיאה. אחרת: הסר את המשתנה מראש המחסנית; הוסף למחסנית את צד ימין של כלל הגזירה שנמצא בטבלה, לפי סדר – החל באסימון/משתנה הימני ביותר וכלה באסימון/משתנה השמאלי ביותר (הוא ישאר בראש המחסנית).
23
בניית הטבלה בתרגול ראיתם איך בונים את First ו-follows, והם ישמשו לבניית הטבלה. הבניה עצמה תפורט בתרגול.
24
דוגמא טריביאלית (בתרגול דוגמאות רבות)
דקדוק: A → aAb | c a b c A A → aAb (error) A → c טבלה: נריץ את האלגוריתם על המילה aacbb: איתחול מחסנית: A$, ומתחילים מאות הקלט הראשונה. שארית הקלט המחסנית כלל מתאים aacbb$ A$ A → aAb aAb$ התאמת טרמינל acbb$ Ab$ aAbb$ cbb$ Abb$ A → c …
25
אלגוריתמים LL(k) עבור k>1, הטבלה הנדרשת לאלגוריתם LL(k) היא (במקרה הגרוע) בעלת סיבוכיות אקספוננציאלית ב-k. לכן, עד לא מזמן האמינו שלא יהיה מעשי לבנות parsers מעשיים לדקדוקי , LL(k) עבור k-ים יותר גדולים. בתחילת שנות התשעים הדגימו חוקרים מאוניברסיטת Purdue (ארה"ב) שהמקרה הגרוע הוא למעשה נדיר, וניתן לבנות parsers פרקטיים עם LL(k). הכלי שפיתחו נקרא כיום ANTLR. כלים אחרים המבוססים על LL(k): JavaCC (משמש לבניית מהדרים ב-Java, כולל מהדר javac עצמו), SableCC (גם הוא ב-Java), ואחרים.
26
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
27
LL(k) or not LL(k) ? דקדוק ב-LL(k+1) שאיננו ב-(LL(k: left-factoring) יניב דקדוק שקול ב-LL(1).) דקדוק/שפה ב-LL(k+1) שאיננה ב-LL(k): רק כשרואים b או c יודעים שצריך להפעיל את המעבר של S ל- ε. S → akb | akc S → aSA | ε A → akbS | c
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.