Download presentation
Presentation is loading. Please wait.
1
ניתוח תחבירי (Parsing)
תורת הקומפילציה הרצאה 3 ניתוח תחבירי (Parsing) Wilhelm, and Maurer – Chapter 8 Aho, Sethi, and Ullman – Chapter 4 Cooper and Torczon – Chapter 3
2
front-end שלב הניתוח תוכנית מקור token string Back end parse tree
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
ניתוח סינטקטי המטרה: להבין את המבנה של התוכנית.
למשל: תוכנית C בנויה מפונקציות, כל פונקציה בנויה מהצהרות ופקודות, כל פקודה בנויה מביטויים וכו'. צורה פשוטה אך מדויקת לניסוח מבנה של תוכנית (בשפת תוכנית ספציפית) היא ע"י דקדוק חסר הקשר. אנו נתעניין במחלקות של דקדוקים ח"ה שניתן לנתח ביעילות. פרטים בהמשך... ה-parser יקרא את רצף ה-tokens, יוודא שהם מקיימים את הדקדוק (או יתריע על שגיאות), ויבנה את עץ הגזירה של התוכנית.
5
דקדוק חסר הקשר G=(V, T, P, S) V – nonterminals, משתנים
T – terminals, טרמינלים, tokens P – חוקי גזירה P = V (V U T)* S – משתנה תחילי S V
6
דוגמא: דקדוק ח"ה עבור ביטוי אריתמטי
סִימוּן מָלֵא: סימון מקוצר: {E, A} V = { ( , ) , - , id , +, - , , / , ^} T = A +, , { E E A E P = A ‒, E ( E ) A , E - E A ^} A /, E id E S = E A E | ( E ) | - E | id E + | ‒ | | / | ^ A
7
שפה חסרת הקשר גזירה – סדרה של החלפות של אותיות לא טרמינליות תוך שימוש בחוקי הגזירה שפה – אוסף ביטויים הנגזרים מהמצב התחילי והמכילים טרמינלים בלבד E A E E id A E id + E id + id סימון הגזירה
8
שפה חסרת הקשר גזירה – סדרה של החלפות של אותיות לא טרמינליות תוך שימוש בחוקי הגזירה שפה – אוסף ביטויים הנגזרים מהמצב התחילי והמכילים טרמינלים בלבד תבנית פסוקית (sentential form) – תוצאת סדרת גזירות בה נותרו (אולי) לא-טרמינלים E * id + E גזירה שמאלית – גזירה בה מוחלף בכל שלב הסימן השמאלי ביותר (באופן דומה – גזירה ימנית) ניתן לגזירה (רב שלבית) E E A E A id E A id + E A id +
9
דוגמא לגזירה bottom-up
נתון דקדוק S → a A c B e A → A b | b B → d קלט – a b b c d e a b b c d e נבחר בשמאלית a A b c d e a A c d e a A c B e A A 3 אלטרנטיבות B A A 3 אלטרנטיבות B B S
10
דוגמא לגזירה bottom-up
נתון דקדוק S → a A c B e A → A b | b B → d קלט – a b b c d e a b b c d e נבחר בשמאלית a A b c d e a A c d e a A c B e A A 3 אלטרנטיבות B A A 3 אלטרנטיבות B B S
11
דוגמא לגזירה bottom-up
נתון דקדוק S → a A c B e A → A b | b B → d קלט – a b b c d e a b b c d e נבחר בשמאלית a A b c d e a A c d e a A c B e A A 3 אלטרנטיבות B A A 3 אלטרנטיבות B B S
12
דוגמא לגזירה bottom-up
נתון דקדוק S → a A c B e A → A b | b B → d קלט – a b b c d e a b b c d e נבחר בשמאלית a A b c d e a A c d e a A c B e A A 3 אלטרנטיבות B A A 3 אלטרנטיבות B B S
13
דוגמא לגזירה bottom-up
נתון דקדוק S → a A c B e A → A b | b B → d קלט – a b b c d e a b b c d e נבחר בשמאלית a A b c d e a A c d e a A c B e A A 3 אלטרנטיבות B A A 3 אלטרנטיבות B B S האם קיבלנו גזירה ימנית או שמאלית?
14
קיבלנו גזירה ימנית S a A c B e a A c d e a A b c d e a b b c d e מלמטה תמיד בחרנו בכלל השמאלי ביותר, ולכן הגזירה המתקבלת (מלמעלה) בוחרת קודם בכלל הימני ביותר, כלומר ימנית. נראה גם בעתיד ניתוחי תחביריים שהולכים על הקלט משמאל לימין ויוצרים גזירה ימנית. ניתוחים אלה מסומנים כ-LR והם עובדים בשיטת ה-bottom-up. ניתוחים שהולכים על הקלט משמאל לימין ומייצרים גזירה שמאלית מסומנים ב-LL והם עובדים לפי top-down.
15
ביטוי רגולרי מול דקדוק חסר הקשר
כל מבנה שניתן לבטא ע"י ביטוי רגולרי יכול להיות מתואר ע"י דקדוק ההיפך לא נכון (למשל?) מדוע לא לעשות הכל בדקדוק? כרגיל: הפרדה, מודולריות, פישוט. אין טעם להפעיל כלים חזקים (ופחות יעילים) על ביטויים רגולריים שקל לנתח. ביטוים רגולרים שימושיים עבור תאור מבנים לקסיקלים כ- identifiers, קבועים, מילות מפתח וכו' דקדוקים שימושיים עבור מבנים מקוננים כסוגרים מאוזנים, התאמת begin-end, וכו'
16
החלק שטרם נקרא חלק הקלט שקראנו
סוגי הניתוח התחבירי כל דקדוק חסר-הקשר שקול לאוטומט מחסנית (אי-דטרמיניסטי). אז למה שלא נבנה אוטומט מחסנית (דטרמיניסטי) ונשתמש בו בתור מנתח תחבירי? באופן כללי: Cocke-Younger-Kasami מתאים לכל דקדוק אבל בסיבוכיות O(n3) ולכן האלגוריתם אינו מעשי. המטרה פענוח ובנית עץ הגזירה תוך מעבר בודד על הקלט, משמאל לימין בכל שלב, הקלט w מתחלק ל x y החלק שטרם נקרא חלק הקלט שקראנו
17
סוגי הניתוח התחבירי (המשך)
top-down – מהשורש לעלים (נקרא גם – "ניתוח תחזית" – predictive) bottom-up – מהעלים לשורש – מעבירים למחסנית, או מחליפים צד ימין בסימן מהצד השמאלי של חוק הדקדוק (נקרא גם shift reduce). s x y s x y
18
נתחיל בניתוח top-down נתונים לנו דקדוק ומילה.
רוצים להחליט ע"י מעבר על המילה, כיצד המשתנה התחילי S נגזר, ואח"כ תוך-כדי סריקת המילה, כיצד ממשיכים לגזור כל משתנה עד שמגיעים לטרמינלים. בדקדוק כללי תיתכן יותר מאפשרות אחת לגזור את המשתנה השמאלי ביותר לפי האות הבאה בקלט. אנו נשתדל לעבוד עם דקדוקים שבהם תהיה רק אפשרות אחת, אולי נצטרך lookahead קטן כדי להחליט מהו כלל הגזירה המתאים הבא.
19
דקדוק שקל לנתח התבוננו בדקדוק:
E → LIT | ( E OP E ) | not E LIT → true | false OP → and | or | xor בכל שלב של הגזירה, אם מסתכלים על משתנה שצריך לגזור והאות הבאה בקלט, ברור מה כלל הגזירה שצריך להפעיל ! E למשל איך נגזרת המילה not ( not true or false ) ? not E E => not E => not ( E OP E ) => not ( not E OP E ) => not ( not LIT OP E ) => not ( not true OP E ) => not ( not true or E ) => not ( not true or LIT ) => not ( not true or false ) ( E OP E ) not LIT LIT or false true
20
ניתוח top-down תוך כדי הפעלת פונקציות: Recursive Descent
מטרה: להתחיל במשתנה התחילי ולמצוא גזירה. עבור כל משתנה בדקדוק (nonterminal) מגדירים פונקציה. המנתח מתחיל לפעול מהפונקציה המתאימה ל-nonterminal התחילי. כל פונקציה מחקה את החוק שעבורו הוגדרה, באופן הבא: terminal מתורגם לקריאת האסימון המתאים מהקלט. nonterminal מתורגם להפעלת הפונקציה המתאימה לו. אם ישנם כמה חוקי גזירה עבור אותו nonterminal, בוחרים ביניהם בעזרת lookahead.
21
פונקציית עזר: match void match(token t) { if (lookahead == t)
lookahead = next_token(); else error; } פונקצייה זו משמשת כדי לקרוא טרמינלים. ה-lookahead הוא של אסימון אחד בלבד.
22
כתיבת פונקציות בהתאם לדקדוק
למשל, עבור הדקדוק: E → LIT | ( E OP E ) | not E LIT → true | false OP → and | or | xor נגדיר שלוש פונקציות: E, LIT ו-OP
23
כתיבת פונקציות בהתאם לדקדוק
void E() { if (lookahead {TRUE, FALSE}) // E → LIT LIT(); else if (lookahead = LPAREN) // E → ( E OP E ) match(LPARENT); E(); OP(); E(); match(RPAREN); else if (lookahead = NOT) // E → not E match(NOT); E(); else error; }
24
כתיבת פונקציות בהתאם לדקדוק
void LIT() { if (lookahead = TRUE) match(TRUE); else if (lookahead = FALSE) match(FALSE); else error; }
25
כתיבת פונקציות בהתאם לדקדוק
void OP() { if (lookahead = AND) match(AND); else if (lookahead = OR) match(OR); else if (lookahead = XOR) match(XOR); else error; } הפונקציות האלו נראות כאילו הן לא עושות כלום (חוץ מלזהות שגיאות), אבל ניתן כמובן להוסיף לכל אחת קוד נוסף, למשל שבונה את העץ כך שיוחזר העץ המלא של הגזירה ביציאה מן הרקורסיה.
26
הוספת פעולות במהלך הגזירה
בכל פעם שנקראת אחת הפונקציות (למשל, E(), LIT() ו-OP() בדוגמא שלנו), פירוש הדבר ש"איתרנו" צעד בגזירה. בכל צעד כזה ניתן לבצע פעולות שונות. בפרט, ניתן לבנות את עץ הגזירה. כל פונקציה תחזיר רשומה מסוג Node (צומת בעץ). כל רשומה כזו מכילה רשימה של בנים. בכל קריאה לפונקציה אחרת (או ל-match), מוסיפים את תוצאת הקריאה ל-Node שנבנה כעת.
27
הוספת פעולות במהלך הגזירה
Node E() { result = new Node(); result.name = “E”; 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(match(RPAREN)); else if (lookahead = NOT) // E → not E result.addChild(match(NOT)); else error; return result; }
28
הוספת פעולות במהלך הגזירה
ואז, למשל: input = “(not true and false)”; Node treeRoot = E(); E ( E OP E ) not LIT and LIT true false
29
Recursive Descent גנרי
נחזור לרגע לצורה הבסיסית. לכל משתנה דקדוק A יש פרוצדורה הנראית כך: Void A() { Choose an A-production, A -> X1X2…Xk; for (i=1; i≤ k; i++) { if (Xi is a nonterminal) call procedure Xi(); elseif (Xi == lookahead) advance input; else report error; } איך מבצעים את הבחירה של כלל הגזירה? ניתן פשוט לנסות אותם אחד אחר השני ולהמשיך באופן רקורסיבי עם backtracking. אבל זה יכול להיות מאד יקר.
30
איך מחליטים בין הכללים? בדוגמה שראינו הספיק lookahead של אסימון יחיד.
פורמלית: עבור כלל A→α, נגדיר: FIRST(α) – רשימת האסימונים שהם ראשונים באחת או יותר גזירות אפשריות הנובעות מכלל זה. בדוגמה שלנו, עבור הכללים ל-E: FIRST(LIT) = { true, false } FIRST( ( E OP E ) ) = { ( } FIRST(not E) = { not } אין שום חיתוך בין הקבוצות השונות ולכן ניתן מיד לדעת מה לגזור עם lookahead של אסימון יחיד.
31
איך מחליטים בין הכללים? אם יש חיתוך בין ה-FIRSTs עבור כללים שונים ל-nonterminal נתון, צריך לתקן את הדקדוק או להשתמש ב-lookahead עמוק יותר. מחלקת הדקדוקים שעבורם ניתן לקבוע את כלל הגזירה הבא בכל פעם ע"י lookahead של k אסימונים נקראת LL(k).
32
דוגמא לבעיה הניתנת לפיתרון
רקורסיה שמאלית יוצרת בעיית זיהוי עבור ניתוח top-down שלא ניתן לפתור בעזרת lookahead חסום. A -> AaB | aC לא ניתן לדעת איזה מהכללים להפעיל. (ויותר: backtracking עשוי להיתקע בלולאה אינסופית! ) משנים את הדקדוק. לכל דקדוק עם רקורסיה שמאלית יש דקדוק מקביל בלעדיה. A -> aCA’ A’ -> aBA’ | ε נלמד את אלגוריתם האלימינציה של רקורסיה שמאלית. בשיעורים הבאים נלמד על המחלקה LL(1) של דקדוקים הנוחים לגזירה מהירה, ומתאימים לשפות תכנות רבות.
33
מראה גזירה (לדוגמא) של תוכנית כללית
program Main function More Functions { Decls Stmts } Function More Functions { Decls Stmts } Function { Decls Stmts } Decl Decls Stmt Stmts Decl Decls Stmt Type Id ; Decl id = expr ;
34
טיפול בשגיאות – נושא חשוב
סוגי שגיאות שגיאות לקסיקליות (טעויות "דפוס"). שגיאות תחביריות (סוגרים לא מאוזנים). שגיאות סמנטיות (אופרנדים מטיפוס לא מתאים). שגיאות לוגיות (לולאה אינסופית, אבל גם שימוש ב-"=" במקום "=="). דרישות דיווח על שגיאות בצורה ברורה. היחלצות מהירה משגיאות כך שאפשר יהיה לגלות שגיאות המופיעות בהמשך. שימור יעילות ה-parser.
35
גישות לטיפול בשגיאות ראשית, מודיעים איפה הניתוח נתקע. בד"כ זה די קרוב לשגיאה. הבעיה העיקרית היא ההיחלצות מהשגיאה לצורך המשך ניתוח. panic mode – השמטת קטעים מהקלט עד מציאת סימן סנכרון ברור (למשל ";" או סוגר סוגריים). פשוט, אבל מאבד חלק (לפעמים משמעותי) מהקוד. phrase-level recovery – נסיונות תיקון מקומיים. למשל החלפת "," ב ";", הורדה או הוספה של ";", וכיו"ב. לא יעבוד אם הטעות קרתה לפני הזיהוי. error production - טיפול בשגיאות צפויות ע"י תיקון אוטומטי במסגרת הדקדוק. global correction – מציאת השינוי המינימאלי בתוכנית שיהפוך אותה לגזירה. יקר, לא פרקטי.
36
parsing: מפורמליזם לתוכנה
grammar parse generator parsing tables parser driver parsing tables tokens parser syntax tree העיקרון דומה לזה של הניתוח הלקסיקאלי. הפורמליזם והשיטות – שונים הפורמליזם – דקדוק חסר הקשר האמצעי – הרצת ה-parsing table ע"י recursive descent. לכל משתנה ו-lookahead מתאים נאמר בטבלה מה להפעיל.
37
לסיכום לאחר שמקלפים את הקליפה הלקסיקלית, מפעילים ניתוח תחבירי parser על ה-tokens להבנת התחביר - עץ התוכנית. מבנה תוכנית חוקית מתואר בפשטות ובדייקנות ע"י דקדוק חסר-הקשר. המנתח התחבירי (parser) עובד בשיטת bottom-up או top-downומגלה אם התוכנית נגזרת מהדקדוק ואיך. שיטת ה-recursive descent משתמשת בפונקציה המתאימה לכל משתנה המחליטה על גזירת המשתנה עפ"י look-ahead. בעת הפעלת הפונקציות תוך כדי הניתוח ניתן כמובן לעשות דברים נוספים, כגון לשמור מידע על עץ הגזירה לשימוש עתידי וכיו"ב. ניתוח דקדוק כללי הוא קשה. ראינו דוגמא לדקדוק קל מאד לניתוח. בשיעורים הבאים נדבר על מחלקות של דקדוקים ריאליסטיים שניתן לנתח ביעילות.
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.