650 likes | 923 Views
תורת הקומפילציה 236360 הרצאה 9 שפות ביניים Intermediate Languages/Representations. Aho, Sethi and Ullman – Chapter 6 Cooper and Torczon – Chapter 5. יצירת קוד ביניים. syntax analysis. syntax tree. semantic analysis. decorated syntax tree. intermediate code generator. intermediate code.
E N D
תורת הקומפילציה 236360הרצאה9 שפות ביניים Intermediate Languages/Representations Aho, Sethi and Ullman – Chapter 6 Cooper and Torczon – Chapter 5
יצירת קוד ביניים syntax analysis syntax tree semantic analysis decorated syntax tree intermediate code generator intermediate code machine independent optimizations intermediate code code generator
חשיבות קוד הביניים • שימוש בשיטות אופטימיזציה שאינן תלויות במכונה מסויימת • אפשרות לייצר קוד עבור מכונות שונות באמצעות אותו front end • שימוש באותו back end עבור שפות שונות – מספר front ends • אם כתבנו mfront-ends ו-nback ends, אז ניתן לשלב אותם ולקבל n*m קומפיילרים. C Java Pascal C# Intermediate Language Cray PowerPC Intel
ייצוג ביניים – intermediate representation • ייצוגים אפשריים • syntax tree • postfix notation • three address code – זו הצורה שאנו נבחר לעבוד איתה. • שני אופרנדים ותוצאה אחת. • בד"כ עובדים בשני שלבים: • תרגום מונחה דקדוק יוצר עץ ניתוח תחבירי + סימונים בצמתים • את העץ מתרגמים ל- three address code • נדבר ראשית על ייצוג העץ, לפעמים נשתמש ב- DAG(Directed Acyclic Graph) במקום בעץ.
שלושה ייצוגים אפשריים:decorated syntax trees, DAGs, ו-postfix • נתון: a := b * –c + b * –c • ייצוג כ- DAG ייצוג כעץ ייצוג ב- postfix a b c uminus * b c uminus * + assign
דוגמה:תרגום מונחה דקדוק ליצירת עץ מעוטר • פונקציות עזר • mkleaf – יצירת עלה • mkunode – יצירת צומת חדש עבור אופרטור אונרי • mknode – יצירת צומת חדש עבור אופרטור בינארי • id.place – מצביע לטבלת הסמלים • הערה – אפשר להחליף את mkleaf, mkunode, ו- mknode בפונקציות המחזירות מצביע לצמתים קיימים על מנת ליצור DAG
assign * * id a id id b b + uminus uminus id id c c ייצוג בזיכרון של עץ מעוטר a := b * –c + b * –c
אופרטור ↑ ↑ ↑ 3 הכתובות three address code • אחרי שבונים את העץ, צריך לתרגם לשפת הביניים שבחרנו. • אנו נעבוד עם three-address-code. • הצורה הכללית של פקודה: x := y op z • x, y, ו-z הם 3 שמות, קבועים, או משתנים זמניים שנוצרו ע"י הקומפיילר. • op הוא אופרטור כלשהו. • האופרטורים שנשתמש בהם יהיו פשוטים, כך שיהיה קל לעבור מהם לשפת מכונה.
assign assign a + a + * * * b unimus b unimus b unimus c c c t1 := – c t1 := – c t2 := b * t1 t2 := b * t1 t3 := – c t3 := t2 + t2 t4 := b * t3 a := t3 t5 := t2 +t4 a := t5 three address code
קוד ביניים – סוגי המשפטים relop = relational op (==, >=, etc.) n = actual number of parameters קריאה לפרוצדורה: param x1 … param xn call p,n
איך בוחרים אופרטורים? • הבחירה של אוסף פקודות מתאים היא חשובה. • אוסף מצומצם: • קל ליצור קוד מכונה, • הקוד יהיה פחות יעיל, ומעמסה גדולה יותר תיפול על ה- optimizer • הקוד יהיה ארוך והטיפול בו יהיה מסורבל • אי ניצול יכולות של מכונות חכמות • אופרטורים רבים: • קוד יותר יעיל אך קשה יותר לייצרו וממנו קשה לייצר קוד עבור מכונות פשוטות.
יצירת קוד ביניים בעל 3 כתובות על ידי תרגום מונחה דקדוק ככלל, נניח bottom-up parsing כך שדברים מחושבים לפני שמשתמשים בתוצאת החישוב. השיטה – שימוש במשתנים זמניים • S.code (או E.code)– תכונה המכילה את הקוד הנוצר עבור S (או E). • E.var – שם של משתנה שעתיד להכיל את הערך של E • newtemp – פונקציה המחזירה שם של משתנה חדש
יצירת קוד ביניים בעל 3 כתובות על ידי תרגום מונחה דקדוק
production semantic rule S →while E do S1 S.begin := newlabel ; S.after := newlabel ; S.code := gen ( S.begin ' : ' ) || E.code || gen ( ' if ' E.var ' = '' 0 '' goto ' S.after ) || S1.code || gen ( ' goto ' S.begin ) || gen (S.after ' : ' ) פסוק while:דוגמא לשימוש בתוויות (labels) S →while E do S1 • נוסיף תכונות למשתנים, ותוויות. • newlabel– פונקציה היוצרת תווית חדשה • S.begin – תווית המסמנת את תחילת הקוד • S.after – תווית המסמנת את סוף הקוד • 0 – מייצג את false
↑ ↑ ↑ מצביעים לטבלת הסמלים op arg 1 arg 2 result (0) uminus c t1 (1) * b t1 t2 (2) uminus c t3 (3) * b t3 t4 (4) + t2 t4 t5 (5) =: t5 a מבנה נתונים לייצוג של 3-address code: • ייצוג סטנדרטי הוא ע"י רביעיות כך שכל שורה נכתבת לתוך משתנה זמני. op, arg1, arg2, result • יתרון:פשוט + אין בעיה להעתיק ולהזיז קטעי קוד (וזה חשוב לאופטימיזציות). • עלות – מחייב לשמור את ה- temporaries בטבלת הסמלים t1 = - c t2 = b * t1 t3 = - c t4 = b * t3 t5 = t2 * t4 a = t5
↑ ↑ op arg 1 arg 2 מצביעים לטבלת הסמלים או למספר הסידורי של השורה המחשבת את הערך (0) uminus c (1) * b (0) (2) uminus c (3) * b (2) (4) + (1) (3) (5) assign a (4) op op arg 1 arg 1 arg 2 arg 2 (0) (0) [ ] = = [ ] x y i i (1) (1) assign assign x (0) (0) y x [ i ] := y x := y [ i ] ייצוג נוסף של 3-address code • שלשות : op, arg1, arg2(התוצאה מובנת כמספר השורה) • אין צורך ב- result • אבל: אי אפשר להזיז קוד +פעולה טרנרית כמו x [ i ] := y דורשת שתי שורות
op arg 1 arg 2 uminus c 0 * b (0) 1 uminus c 2 * b (2) 3 + (1) (3) 4 assign a (4) 5 ייצוג שלישי של 3-address code • indirect triples – השלשות מופרדות מהסדר ביניהן • עתה ניתן לשנות סדר ביצוע, להזיז קטעי קוד, ולחסוך במקום אם קיימות שלשות זהות • (לא פותר את הפעולה הכפולה עבור פעולות טרנריות.) רשימת פקודות לפי סדר הביצוע הרצוי שלהן Execution Order 10 11 0 1 15 12
Types והקצאות זיכרון למשתנים • ניתוח ה-types חשוב מאד לבדיקת שגיאות, • אבל חשוב גם על-מנת לאפשר הקצאת מקום בגודל נכון למשתנים במחסנית (או באובייקט) וחישוב offset לכל אחד מהם, ואף לחשב כתובות בתוך מערכים. ... משתנים קודמים ל-employee Offset for variable employee רשומת הפעלה למתודה employee מקום למשתנה employee ...
הכרזות והקצאת זכרון • דוגמא:תרגום מונחה דקדוק עם פעולות סמנטיות לחישוב ה- offset. • נשמור משתנה גלובלי offset עם גודל השטח שהוקצה עד עתה. • לכל משתנה בפרוצדורה – נכניס לטבלת הסמלים ונקבע לו offset.
enter(money, real, 4) offset = offset + 4 הכרזות enter(count, int, 0) offset = offset + 4 P D4 D1 D5 D2 id T1 id T2 T3 id int count real money ] balances num [ T4 T1.type = int T1.width = 4 T2.type = real T2.width = 4 int 98 id.name = count id.name = money
הכרזות והקצאת זיכרון • האיפוס של offset בהתחלה עובד מצוין לניתוח top-down שבו נפעיל את P → Dבתור הכלל הראשון. אך מה עושים עם ניתוח bottom-up? • טריק סטנדרטי:נוסיף marker וכלל שתמיד נראה ראשון, גם ב-LR parsing
הכרזות והקצאת זיכרון • השיטה עובדת מצוין לניתוח top-down שבו נפעיל את P → Dבתור הכלל הראשון ונאפס את offset כנדרש. אך מה עושים עם ניתוח bottom-up? • טריק סטנדרטי:נוסיף marker וכלל שתמיד נראה ראשון, גם ב-LR parsing _____________ P M D Є
לסיכום – ייצוג של קוד ביניים • קוד ביניים סטנדרטי הוא חשוב, ניתן להפריד בין ה-front-end שתלוי בשפת המקור, לבין ה-back-end שתלוי במכונת היעד, ולשלב כל front-end עם כל back-end. • השלבים הקודמים בונים עץ מעוטר (עם attributes) • נתרגם אותו אל three-address-code שהיא שפת ביניים סטנדרטית. • אפשר לעשות זאת ע"י פעולות סמנטיות בתרגום מונחה דקדוק. • אוספים את הקוד לתוויות של משתני הדקדוק. • Three-address-code ניתן לייצוג ע"י רביעיות או שלשות (ישירות או עקיפות). • ניתוח ה-types חיוני עבור קביעת מקום למשתנים בזיכרון, וגם את זה ניתן לעשות באמצעות פעולות סמנטיות בתרגום מונחה דקדוק...
יצירת קוד • אפשרות א' –צבירת קוד ב-attributes של משתנים בעץ הגזירה (למשל, בתכונות מסוג code). כך עשינו עד עתה. • אפשרות ב' –יצירת קובץ המכיל את הקוד תוך כדי תהליך הקומפילציה • אפשרות זו מעשית (לגזירת bottom-up) אם לכל חוק דקדוק תכונת ה- code של אגף שמאל של החוק מתקבלת משרשור תכונות ה- code של המשתנים באגף ימין של החוק על פי סדר הופעתן (אולי בצירוף מחרוזות נוספות) • חסרון:לא מאפשר מניפולציות על הקוד. • במספר שקפים הקרובים נדגים את אפשרות ב'. כמובן שניתן בקלות לחזור לצבירת קוד בתכונות של משתני הדקדוק שבגזירה.
ביטויים ומשפטי השמה • דקדוק המסגרת:התוכנית מכילה הגדרות של משתנים (כמו קודם) ופרוצדורות. • ביטויים ומשפטי השמה: • Lookup מחזיר את הכתובת של המשתנה בזיכרון. • Emit פולט שורת קוד מתאימה בפורמט three-address-code לתוך הקובץ. • הטיפול כאן (ובד"כ בהמשך)הוא לפי bottom-up parsing ולכן מקצים למשתנה מקום בפעם הראשונה שפוגשים אותו = כמשתנה השמאלי בכלל הדקדוק.
ביטויים ומשפטי השמה • דקדוק המסגרת:התוכנית מכילה הגדרות של משתנים (כמו קודם) ופרוצדורות. • ביטויים ומשפטי השמה: • ניתן לייעל במקום הדרוש למשתנים זמניים: כשיוצאים מתת-עץ אין יותר שימוש במשתנים הפנימיים שלו. ניתן לנהל את המשתנים במחסנית.
ביטויים בוליאניים נייצג את false כ-0 ואת true כ-1. • חשוב לשים לב – כתובת המטרה ניתנת לחישוב תוך כדי יצירת הקוד
חישוב ביטויים בוליאניים ע"י קפיצה נייצג את false כ-0 ואת true כ-1. • למה זה מועיל?לחישוב מקוצר... אבל נראה קודם דוגמא.
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
E E or E a < b E and E c < d e < f ביטויים בוליאניים בייצוג מספרי – דוגמא
ביטויים בוליאניים – חישוב מקוצר • בניגוד לביטויים אריתמטיים, בביטויים בוליאניים ניתן לחסוך בחישוב כי לעיתים ניתן לדעת מה התוצאה כבר באמצע החישוב. • למשל, בביטוי E1or E2, אם E1 הוא true הרי שלא חשוב לנו מה ערכו של E2. • חישוב כזה נקרא lazy evaluation או short circuit boolean evaluation.
100: if a < b goto 103 101: T1 := 0 102: goto 104 103: T1 := 1 104: if c < d goto 107 105: T2 := 0 106: goto 108 107: T2 := 1 108: if e < f goto 111 109: T3 := 0 110: goto 112 111: T3 := 1 112: T4 := T2 andT3 113: T5 := T1 andT4 100: if a < b goto 105 101: if !(c < d) goto 103 102: if e < f goto 105 103: T := 0 104: goto 106 105: T := 1 106: דוגמא: a < b or (c < d and e < f) ניזכר בביטוי של קודם: חישוב מקוצר:
תכונות של חישוב מקוצר • האם החישוב מקוצר שקול לחישוב רגיל? • מתי אסור להשתמש בחישוב מקוצר? • מתי חייבים להשתמש בחישוב מקוצר? תשובות: לא – יתכנו side-effects לחישוב ביטוי בוליאני. דוגמא קלאסית: if ( (i > 0) and (i++ < 10) ) A[i]=i else B[i]=i; כאשר הגדרת השפה לא מרשה זאת. כאשר הגדרת השפה מחייבת קיצור, והמתכנת עלול להתבסס על כך.דוגמא קלאסית: if ( (file=open(“c:\grades”) or (die) ) printfile(file);
טיפול בהפניות בקרה:if, else, while. • נחזור לאגור את הקוד בתכונה (attribute) בשם קוד. ההבדל בין emit ל- gen: genמחזירה את הפקודה שנוצרה; emit מדפיסה אותה ל- buffer. • נתבונן בקפיצות מותנות: • אפשרות אחת היא לעבוד כמו קודם, לייצר קוד ל-B לייצר קוד ל-S, ואז לייצר קפיצה לתחילת S או סוף S כתלות בערך של B. • אבל באופן יעיל יותר, אפשר פשוט לייצר קוד שבזמן החישוב של B יקפוץ למקום הנכון ברגע שיתגלה מה ערכו של B.
טיפול בהפניות בקרה:if, else, while. • מסתבר שיש כאן בעיה עם ההחלטה לאן לקפוץ בזמן הניתוח... • כאשר מנתחים את העץ שנפרש מ-B עבור "if B then S” לא יודעים למה S יתפתח ואיפה מתחיל ונגמר הקוד של S, אבל צריך לייצר קפיצות למקומות אלו. • השיטה – לכל ביטוי B נצמיד שתי תוויות: B.true, ו-B.false שהן התוויות אליהן החישוב צריך לעבור אם B הוא true (או false בהתאמה). • לכל פסוק S נחזיק תווית next שאומרת מה הכתובת של הקוד שאחריו. • משוואה סמנטיות מתאימה:B.false = S.next • לגבי B.true, נייצר label בין הקוד של B לקוד של S ונייחס לו את B.true. S if B then S
התכונה next • בגזירה של פסוק S, נייצר את הקוד עם התווית שאחריו: • התכונה S.next היא נורשת: הילד מקבל אותה כשהוא נגזר מאביו. • תכונת ה-code היא נוצרת: האבא מקבל אותה בעת גזירת ילדיו. • ה-labelS.next היא סימבולית. הכתובת המתאימה לה תיוודע רק אחרי שנגזור את כל הביטוי של S.
→ to B.true קוד לחישוב B עם קפיצות החוצה → to B.false B.true: קוד לחישוב S B.false: . . . If B then S • B.false ו- S1.next הן תכונות נורשות • S.code היא תכונה נוצרת
If B then S1 else S2 →to B.true →to B.false B.True ו-B.false לא נקבעים ע"י ההורים ולא ע"י הילדים. אבל הם נקבעים בזמן גזירה שבה B הוא ילד ולכן נחשבים נורשים. נורש נוצר
חישוב ביטויים בוליאניים על ידי הפנית בקרה • נייצר קוד שקופץ ל-B.true אם הערך של Btrue ול-B.false אם הוא false. • איזו צורת חישוב מוצגת כאן? מקוצרת או מלאה?
חישוב ביטויים בוליאניים על ידי הפנית בקרה • נייצר קוד שקופץ ל-B.true אם הערך של Btrueול-B.false אם הוא false. • נתבונן לדוגמא ב-labelB1.false. • הכתובת של ה-label ניתנת לחישוב רק אחרי שנדע את כל הקוד של B1 וכל הקוד שלפני B1. • למעשה, אנו נייצר את כל הקוד עם labels סימבוליים, ואחרי כן נבצע מעבר נוסף על העץ כדי לקבוע כתובת לכל label סימבולי, ולעדכן את כתובות הקפיצה בפקודות המתאימות.
Backpatching – תיקון לאחור • מטרתנו להסתפק במעבר אחד על העץ בזמן היצירה שלו, ללא המעבר הנוסף. • השיטה:נשמור לכל label את אוסף הכתובות של פקודות שמדלגות אליו. • ברגע שנדע את הכתובת של ה-label, נלך על רשימת הכתובות ונכניס בפקודות הקפיצה המתאימות את הכתובת האמיתית של ה-label. • יתרון:מעבר DFS יחיד יספיק (חישבו על אלפי שורות קוד). • חסרון:נצטרך להקצות מקום לרשימות של הכתובות. • נדגיש שפתרונות שהזכרנו בעבר לא יעבדו. • הגזירה אינה S-attributed (יש גם תכונות נורשות, למשל next). • היא לא L-attributed (התכונות הנורשות אינן בהכרח נורשות-משמאל) • לכן לא נוכל לחשב את התכונות תוך כדי הניתוח.
דוגמא להבהרת הקושי S B then else if S1 S2 • חישבו על פסוק if-then-else. • על-מנת לחשב את S1.next צריך כבר לדעת את הקוד של כל הבלוקים B, S1, ו-S2 (כדי לדעת מהי הכתובת שאחריהם). • מצד שני, כדי לחשב את הקוד של S1 צריך להעביר לו את S1.next, או S.next, אבל ערך זה לא ידוע לפני החישוב של S1. • כאמור, לא נוכל לחשב את הקוד של S1 עם כל כתובות הקפיצה, אבל נוכל לחשב אותו עד כדי "השארת מקום" להכנסה מאוחרת יותר של S1.next. • בשיטת ה-backpatching נבנה את הקוד ונשאיר לעצמנו רשימה עבור ה-label הסימבולי S1.next של כל שורות הקוד שבהן יש קפיצה אליו. • כשנדע את ערכו של S1.next, נעבור על הרשימה ונעדכן.
פונקציות ליצירה וטיפול בהתחייבויות • makelist ( addr ) – יצירת רשימת התחייבויות חדשה המכילה את הכתובת addr. התוצאה – מצביע לרשימה של כתובות של פקודות. • addr הוא מספר שורה ברשימת הרביעיות שלנו • המשמעות:יש לתקן את הפקודה שבשורה addr כשיתקבל מידע רלוונטי • merge ( p1, p2 ) – איחוד הרשימות אליהם מצביעים p1ו- p2. מחזיר מצביע לאיחוד הרשימות. • כלומר, שתי הרשימות מכילות פקודות שצריכות לקפוץ לאותו מקום. • backpatch ( p, addr ) – קביעת הכתובת addr ככתובת הקפיצה בכל אחת מהפקודות (רביעיות) שברשימה אליה מצביע p
אגירת הקוד • נניח (כהרגלנו)ניתוח bottom-up כך שהקוד נוצר בסדר הנכון (שמאל לימין, מלמטה למעלה). • הניתוח הסמנטי יתבצע במהלך הניתוח התחבירי והקוד ייפלט לתוך buffer עם פקודת emit (פשוט כדי שיהיה נוח לחשוב על כתובות של פקודות). • אפשר גם לאסוף את הקוד בתוך תכונה, כל עוד יש דרך לשמור מצביע על שורת קוד (שעליה יתבצע backpatch). • כזכור, לכל ביטוי B הצמדנו שתי תוויות: B.true, ו-B.false שהן התוויות אליהן החישוב צריך לעבור אם B הוא true (או false בהתאמה). • עתה תהיינה לנו גם זוג רשימות : B.truelist, ו-B.falselist שאומרות באילו פקודות צריך לחזור ולעדכן את הכתובות של : B.true, ו-B.false כשמגלים את ערכיהם. • בנוסף, לכל פסוק S שעבורו החזקנו label סימבולי S.next, נחזיק עתה גם רשימה S.nextlist.
אגירת הקוד - המשך • B.truelist, ו-B.falselist הן תכונות נוצרות: הצאצאים מספרים לאב איפה יש קוד שצריך לתקן. • כאשר עולים לאב של B עצמו, נדע מה הכתובת הרלוונטית ונוכל לבצע backpatch ולהכניס אותה לכל הפקודות שנרשמו ברשימה. • באופן דומה, ל-S.nextlist תכונות דומות. • נשתמש בפונקציה nextinstr שתחזיר את הכתובת של הפקודה הבאה.