זהו הפקודה perlinterp שניתן להפעיל בספק האירוח החינמי של OnWorks באמצעות אחת מתחנות העבודה המקוונות המרובות שלנו, כגון Ubuntu Online, Fedora Online, אמולטור מקוון של Windows או אמולטור מקוון של MAC OS
תָכְנִית:
שֵׁם
perlinterp - סקירה כללית של מתורגמן Perl
תיאור
מסמך זה מספק סקירה כללית של אופן פעולת המתורגמן של Perl ברמה של C
קוד, יחד עם מצביעים לקובצי קוד המקור C הרלוונטיים.
אלמנטים OF LA מְתוּרגְמָן
לעבודת המתורגמן שני שלבים עיקריים: הידור הקוד לתוך הפנימי
ייצוג, או bytecode, ולאחר מכן ביצועו. "קוד מורכב" ב-perlguts מסביר
בדיוק איך מתרחש שלב הקומפילציה.
להלן פירוט קצר של הפעולה של perl:
אתחול
הפעולה מתחילה ב perlmain.c. (אוֹ miniperlmain.c עבור miniperl) זוהי רמה גבוהה מאוד
קוד, מספיק כדי להתאים על מסך בודד, והוא דומה לקוד שנמצא ב-perlembed; רוב
של הפעולה האמיתית מתרחשת ב perl.c
perlmain.c נוצר על ידי "ExtUtils::Miniperl" מ miniperlmain.c בזמן לעשות זמן, אז אתה
צריך לעשות Perl כדי לעקוב אחרי זה.
ראשית, perlmain.c מקצה קצת זיכרון ובונה מתורגמן של Perl, לאורך כל אלה
שורות:
1 PERL_SYS_INIT3(&argc,&argv,&env);
2
3 if (!PL_do_undump) {
4 my_perl = perl_alloc();
5 if (!my_perl)
6 יציאה(1);
7 perl_construct(my_perl);
8 PL_perl_destruct_level = 0;
9}
שורה 1 היא מאקרו, וההגדרה שלה תלויה במערכת ההפעלה שלך. שורה 3
הפניות "PL_do_undump", משתנה גלובלי - כל המשתנים הגלובליים ב- Perl מתחילים ב
"PL_". זה אומר לך אם התוכנית הפועלת הנוכחית נוצרה עם הדגל "-u".
לפרל ואז בטל dump, מה שאומר שזה הולך להיות שקר בכל הקשר שפוי.
שורה 4 מתקשרת לפונקציה perl.c להקצות זיכרון למתורגמן של פרל. זה די
פונקציה פשוטה, והקרביים של זה נראה כך:
my_perl = (PerlInterpreter*)PerlMem_malloc(sizeof(PerlInterpreter));
כאן אתה רואה דוגמה להפשטת המערכת של פרל, אותה נראה בהמשך:
"PerlMem_malloc" הוא ה-"malloc" של המערכת שלך, או ה-"malloc" של Perl עצמו כפי שהוגדר ב-
malloc.c אם בחרת באפשרות זו בזמן ההגדרה.
לאחר מכן, בשורה 7, אנו בונים את המתורגמן באמצעות perl_construct, גם ב perl.c; זה
מגדיר את כל המשתנים המיוחדים ש-Perl צריך, הערימות וכו'.
כעת אנו מעבירים לפרל את אפשרויות שורת הפקודה, ואומרים לה ללכת:
exitstatus = perl_parse(my_perl, xs_init, argc, argv, (char **)NULL);
if (!exitstatus)
perl_run(my_perl);
מצב יציאה = perl_destruct(my_perl);
perl_free(my_perl);
"perl_parse" הוא למעשה עטיפה סביב "S_parse_body", כפי שמוגדר ב perl.c, אשר
מעבד את אפשרויות שורת הפקודה, מגדיר כל מודולי XS מקושרים סטטית, פותח את
תוכנית וקוראת "yyparse" כדי לנתח אותה.
ניתוח
מטרת השלב הזה היא לקחת את מקור הפרל ולהפוך אותו לעץ אופ. נראה
איך אחד מאלה נראה מאוחר יותר. למען האמת, יש כאן שלושה דברים שקורים.
"yyparse", המנתח, מתגורר בו perly.c, למרות שעדיף לך לקרוא את המקור
כניסת YACC פנימה perly.y. (כן, וירג'יניה, שם is דקדוק YACC לפרל!) תפקידו של ה
מנתח הוא לקחת את הקוד שלך ו"להבין" אותו, לפצל אותו למשפטים, להחליט
אילו אופרנדים הולכים עם אילו אופרטורים וכן הלאה.
המנתח נעזר באצילות על ידי lexer, שמחלק את הקלט שלך לאסימונים, ו
מחליט איזה סוג של דבר הוא כל אסימון: שם משתנה, אופרטור, מילה חשופה, א
תת שגרה, פונקציית ליבה וכן הלאה. נקודת הכניסה העיקרית ללקסר היא "yylex",
ואת זה ואת השגרות הקשורות אליו ניתן למצוא toke.c. פרל לא מאוד דומה לאחרים
שפות מחשב; זה מאוד רגיש להקשר לפעמים, זה יכול להיות מסובך להתאמן
איזה סוג של אסימון משהו, או היכן מסתיים אסימון. ככזה, יש הרבה
משחק גומלין בין האסימון למנתח, שיכול להיות די מפחיד אם אתה
לא רגיל לזה.
כפי שהמנתח מבין תוכנית Perl, הוא בונה עץ פעולות עבור
מתורגמן לבצע במהלך הביצוע. השגרות שבונות ומחברות יחד
ניתן למצוא את הפעולות השונות אופ.ג, וייבדק בהמשך.
אופטימיזציה
כעת הושלם שלב הניתוח, והעץ המוגמר מייצג את הפעולות ש
המתורגמן של Perl צריך לבצע כדי להפעיל את התוכנית שלנו. לאחר מכן, פרל עושה ריצה יבשה
מעל העץ מחפש אופטימיזציות: ביטויים קבועים כגון "3 + 4" יהיו
מחושב כעת, והאופטימיזר יראה גם אם ניתן להחליף מספר פעולות
עם אחד בודד. למשל, להביא את המשתנה $foo, במקום לתפוס את הגלוב
*foo ומסתכל על הרכיב הסקלרי, כלי האופטימיזציה מתעסק בעץ האופ כדי להשתמש ב-
פונקציה שמחפשת ישירות את הסקלר המדובר. האופטימיזציה העיקרית היא "הציץ" פנימה
אופ.ג, ולאופציות רבות יש פונקציות אופטימיזציה משלהן.
ריצה
עכשיו אנחנו סוף סוף מוכנים לצאת לדרך: הרכבנו קוד בתים של Perl, וכל מה שנותר לעשות
הוא להפעיל אותו. הביצוע בפועל נעשה על ידי הפונקציה "runops_standard" ב run.c; יותר
באופן ספציפי, זה נעשה על ידי שלושת השורות התמימות למראה:
while ((PL_op = PL_op->op_ppaddr(aTHX))) {
PERL_ASYNC_CHECK();
}
ייתכן שתרגיש יותר נוח עם גרסת Perl של זה:
PERL_ASYNC_CHECK() בעוד $Perl::op = &{$Perl::op->{function}};
טוב, אולי לא. בכל מקרה, כל אופציה מכילה מצביע פונקציה, שקובע את
פונקציה שתבצע בפועל את הפעולה. פונקציה זו תחזיר את הבא
op ברצף - זה מאפשר דברים כמו "אם" שבוחרים את הפעולה הבאה באופן דינמי
בזמן ריצה. ה-"PERL_ASYNC_CHECK" מוודא שדברים כמו אותות מפריעים
ביצוע במידת הצורך.
הפונקציות שנקראות בפועל ידועות כקוד PP, והן מפוזרות בין ארבעה קבצים:
pp_hot.c מכיל את הקוד "החם", שנמצא לרוב בשימוש ובעל אופטימיזציה גבוהה, pp_sys.c
מכיל את כל הפונקציות הספציפיות למערכת, pp_ctl.c מכיל את הפונקציות אשר
ליישם מבני בקרה ("אם", "תוך כדי" וכדומה) ו pp.c מכיל הכל
אַחֵר. אלו הם, אם תרצו, קוד C עבור הפונקציות והאופרטורים המובנים של Perl.
שימו לב שכל פונקציה "pp_" צפויה להחזיר מצביע לפעולה הבאה. שיחות ל
תת-פרל (ובלוקי eval) מטופלים בתוך אותה לולאת runops, ואינם צורכים
מקום נוסף בערימת C. לדוגמה, "pp_entersub" ו-"pp_entertry" פשוט לחץ על a
מבנה בלוק "CxSUB" או "CxEVAL" על מחסנית ההקשר המכילה את הכתובת של
פעולה בעקבות שיחת המשנה או eval. לאחר מכן הם מחזירים את האופ הראשון של המשנה או ההשוואה
בלוק, וכך הביצוע של המשנה או הבלוק הזה ממשיך. מאוחר יותר, "pp_leavesub" או
האופציה "pp_leavetry" מקפיצה את ה-"CxSUB" או "CxEVAL", מאחזרת ממנו את ה-retour, ו
מחזיר אותו.
חריגה מסירת
מסירת החריגה של פרל (כלומר "למות" וכו') בנויה על גבי הרמה הנמוכה
"setjmp()"/"longjmp()" פונקציות של ספריית C. אלה בעצם מספקים דרך ללכוד את
אוגרי PC ו-SP נוכחיים ושחזר אותם מאוחר יותר; כלומר, "longjmp()" ממשיך ב-
נקודה בקוד שבה נעשה "setjmp()" קודם, עם כל דבר יותר למעלה ב-C
מחסנית הולכת לאיבוד. זו הסיבה שקוד צריך תמיד לשמור ערכים באמצעות "SAVE_FOO" במקום
במשתנים אוטומטיים.
ליבת perl עוטפת את "setjmp()" וכו' בפקודות המאקרו "JMPENV_PUSH" ו-"JMPENV_JUMP". ה
הכלל הבסיסי של חריגים ב-perl הוא ש"יציאה" ו"למות" (בהיעדר "eval") מבצעים
a JMPENV_JUMP(2), בעוד "למות" בתוך "eval" עושה א JMPENV_JUMP(3).
בנקודות כניסה ל-perl, כגון "perl_parse()", "perl_run()" ו-"call_sv(cv, G_EVAL)"
כל אחד עושה "JMPENV_PUSH", ואז נכנס ללולאת runops או כל דבר אחר, ומטפל אפשרי
חריג חוזר. עבור 2 החזרות, מתבצע ניקוי סופי, כגון פתיחת ערימות ו
קורא לחסימות "CHECK" או "END". בין היתר, כך עדיין ניקוי היקף
מתרחשת במהלך "יציאה".
אם "מת" יכול למצוא בלוק "CxEVAL" בערימת ההקשר, אז הערימה מופצת ל
רמה זו והחזרה אופ בבלוק זה מוקצים ל-"PL_restartop"; ואז א
JMPENV_JUMP(3) מתבצע. זה בדרך כלל מעביר את השליטה בחזרה לשומר. במקרה
של "perl_run" ו-"call_sv", "PL_restartop" שאינו ריק מפעיל כניסה מחדש ל-runops
לוּלָאָה. זוהי הדרך הנורמלית שבה מטפלים ב"מות" או ב"קרקור" בתוך "השוואה".
לפעמים מבצעים מבצעים בתוך לולאת רינופס פנימית, כגון תיקו, מיון או עומס יתר
קוד. במקרה הזה, משהו כמו
sub FETCH { eval { die } }
יגרום ל-longjmp ישר חזרה לשומר ב-"perl_run", שיקפיץ את שתי לולאות ה-runops,
דבר שאינו נכון בעליל. אחת הדרכים להימנע מכך היא שקוד העניבה יעשה א
"JMPENV_PUSH" לפני ביצוע "FETCH" בלולאת ה-runops הפנימית, אך לצורך יעילות
סיבות, perl למעשה רק מגדיר דגל, באמצעות "CATCH_SET(TRUE)". ה-"pp_require",
אופציות "pp_entereval" ו-"pp_entertry" בודקים את הדגל הזה, ואם זה נכון, הם קוראים "docatch",
שעושה "JMPENV_PUSH" ומתחיל רמת runops חדשה כדי להפעיל את הקוד, במקום
עושה את זה בלולאה הנוכחית.
כאופטימיזציה נוספת, ביציאה מבלוק eval ב-"FETCH", ביצוע של
הקוד שאחרי הבלוק עדיין ממשיך בלולאה הפנימית. כאשר חריג הוא
שהועלה, "docatch" משווה את רמת "JMPENV" של "CxEVAL" עם "PL_top_env" ואם
הם שונים, רק זורקים מחדש את החריג. בדרך זו כל לולאות פנימיות מתפוצצות.
הנה דוגמה.
1: eval { tie @a, 'A' };
2: sub A::TIEARRAY {
3: eval { למות};
4: למות;
5: }
כדי להפעיל את הקוד הזה, "perl_run" נקרא, אשר עושה "JMPENV_PUSH" ואז נכנס ל-runops
לוּלָאָה. לולאה זו מבצעת את ה-eval and tie ops בשורה 1, כשה-eval דוחפת "CxEVAL"
על מחסנית ההקשר.
ה-"pp_tie" עושה "CATCH_SET(TRUE)", ואז מתחיל לולאת runops שניה כדי לבצע את
הגוף של "TIEARRAY". כאשר הוא מבצע את ה-entry op בשורה 3, "CATCH_GET" נכון, אז
"pp_entertry" קורא ל-"docatch" שעושה "JMPENV_PUSH" ומתחיל לולאה שלישית,
אשר לאחר מכן מבצע את ה-die op. בשלב זה ערימת השיחות C נראית כך:
Perl_pp_die
Perl_runops # לולאה שלישית
S_docatch_body
S_docatch
Perl_pp_entertry
Perl_runops # לולאה שנייה
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops # לולאה ראשונה
S_run_body
perl_run
ראשי
וערימות ההקשר והנתונים, כפי שמוצג על ידי "-Dstv", נראות כך:
מחסנית 0: עיקרית
CX 0: BLOCK =>
CX 1: EVAL => AV() PV("A"\0)
retop=עזוב
מחסנית 1: קסם
CX 0: SUB =>
retop=(null)
CX 1: EVAL => *
retop=nextstate
הקוביה מקפיצה את ה-"CxEVAL" הראשון מחסנית ההקשר, מגדירה ממנה את "PL_restartop", עושה
JMPENV_JUMP(3), והבקרה חוזרת ל"docatch" העליון. ואז זה מתחיל עוד שליש-
level runops level, אשר מבצע את Nextstate, pushmark ו-die ops בשורה 4. ב-
נקודה שה-"pp_die" השני נקרא, מחסנית ה-C call נראית בדיוק כמו למעלה,
אף על פי שאיננו נמצאים עוד בתוך שוויון פנימי; זה בגלל האופטימיזציה
הוזכר קודם לכן. עם זאת, ערימת ההקשר נראית כעת כך, כלומר עם ה-CxEVAL העליון
קפץ:
מחסנית 0: עיקרית
CX 0: BLOCK =>
CX 1: EVAL => AV() PV("A"\0)
retop=עזוב
מחסנית 1: קסם
CX 0: SUB =>
retop=(null)
הקוביה בשורה 4 מקפיצה את ערימת ההקשר בחזרה אל ה-CxEVAL, ומשאירה אותה כ:
מחסנית 0: עיקרית
CX 0: BLOCK =>
כרגיל, "PL_restartop" מוחלץ מה-"CxEVAL", ו-a JMPENV_JUMP(3) נעשה, אשר
מקפיץ את ערימת C בחזרה ל-docatch:
S_docatch
Perl_pp_entertry
Perl_runops # לולאה שנייה
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops # לולאה ראשונה
S_run_body
perl_run
ראשי
במקרה זה, מכיוון שרמת ה-"JMPENV" שנרשמה ב-"CxEVAL" שונה מה-
הנוכחי, "docatch" פשוט עושה א JMPENV_JUMP(3) וערימת C מתפרקת ל:
perl_run
ראשי
מכיוון ש-"PL_restartop" אינו ריק, "run_body" מתחיל לולאת runops חדשה וביצוע
ממשיכה.
פְּנִימִי מִשְׁתַנֶה סוגי
עד עכשיו היית צריך להסתכל על perlguts, שמספר לך על הפנים של Perl
סוגי משתנים: SVs, HVs, AVs וכל השאר. אם לא, עשה זאת כעת.
משתנים אלה משמשים לא רק לייצוג משתני Perl-space, אלא גם כל משתנים
קבועים בקוד, כמו גם כמה מבנים פנימיים לחלוטין לפרל. הסמל
table, למשל, הוא חשיש רגיל של Perl. הקוד שלך מיוצג על ידי SV כפי שהוא
קרא לתוך המנתח; כל קבצי התוכנה שאתה מתקשר אליהם נפתחים באמצעות ידיות קבצים רגילות של Perl,
וכן הלאה.
מודול הליבה Devel::Peek מאפשר לנו לבחון SVs מתוכנית Perl. בוא נראה, עבור
למשל, איך פרל מתייחסת ל"שלום" הקבוע.
% perl -MDevel::Peek -e 'Dump("שלום")'
1 SV = PV(0xa041450) ב-0xa04ecbc
2 REFCNT = 1
3 דגלים = (POK,readonly,pPOK)
4 PV = 0xa0484e0 "שלום"\0
5 CUR = 5
6 LEN = 6
קריאת פלט "Devel::Peek" דורשת מעט תרגול, אז בואו נעבור על זה שורה אחר שורה.
שורה 1 אומרת לנו שאנחנו מסתכלים על SV שחי בזיכרון 0xa04ecbc. SVs עצמם
הם מבנים פשוטים מאוד, אבל הם מכילים מצביע למבנה מורכב יותר. ב
במקרה זה, זה PV, מבנה שמכיל ערך מחרוזת, במיקום 0xa041450. קַו
2 הוא ספירת ההתייחסות; אין אזכורים אחרים לנתונים האלה, אז זה 1.
שורה 3 הם הדגלים של ה-SV הזה - זה בסדר להשתמש בו בתור PV, זה SV לקריאה בלבד (מכיוון
זה קבוע) והנתונים הם PV פנימי. הבא יש לנו את התוכן של
מחרוזת, החל ממיקום 0xa0484e0.
שורה 5 נותנת לנו את האורך הנוכחי של המחרוזת - שימו לב שזה כן לֹא כולל
terminator null. שורה 6 היא לא אורך המיתר, אלא אורך הנוכחי
חוצץ מוקצה; ככל שהמחרוזת גדלה, Perl מרחיבה אוטומטית את האחסון הזמין
באמצעות שגרה בשם "SvGROW".
אתה יכול לקבל בכל אחת מהכמויות הללו מ-C בקלות רבה; פשוט הוסף "Sv" לשם של
השדה המוצג בקטע, ויש לך מאקרו שיחזיר את הערך:
"SvCUR(sv)" מחזיר את האורך הנוכחי של המחרוזת, "SvREFCOUNT(sv)" מחזיר את
ספירת התייחסות, "SvPV(sv, len)" מחזירה את המחרוזת עצמה עם אורכה, וכן הלאה.
ניתן למצוא פקודות מאקרו נוספות לתמרן מאפיינים אלה ב-perlguts.
ניקח דוגמה של מניפולציה של PV, מתוך "sv_catpvn", ב sv.c
1 בטל
2 Perl_sv_catpvn(pTHX_ SV *sv, const char *ptr, STRLEN len)
3 {
4 STRLEN tlen;
5 char *זבל;
6 זבל = SvPV_force(sv, tlen);
7 SvGROW(sv, tlen + len + 1);
8 if (ptr == זבל)
9 ptr = SvPVX(sv);
10 Move(ptr,SvPVX(sv)+tlen,len,char);
11 SvCUR(sv) += len;
12 *SvEND(sv) = '\0';
13 (void)SvPOK_only_UTF8(sv); /* לאמת מצביע */
14 SvTAINT(sv);
15}
זוהי פונקציה שמוסיפה מחרוזת, "ptr", באורך "len" על קצה ה-PV
מאוחסן ב-"sv". הדבר הראשון שאנו עושים בשורה 6 הוא לוודא שה-SV יש ל PV חוקי,
על ידי קריאה למאקרו "SvPV_force" כדי לאלץ PV. כתופעת לוואי, "tlen" מקבל את ההגדרה
הערך הנוכחי של ה-PV, וה-PV עצמו מוחזר ל"זבל".
בשורה 7, אנו מוודאים של-SV יהיה מספיק מקום כדי להכיל את המיתר הישן,
המחרוזת החדשה והמחסל האפס. אם "LEN" אינו גדול מספיק, "SvGROW" יעשה זאת
להקצות לנו מקום מחדש.
כעת, אם "זבל" זהה למחרוזת שאנו מנסים להוסיף, נוכל לתפוס את המחרוזת
ישירות מה-SV; "SvPVX" היא הכתובת של ה-PV ב-SV.
שורה 10 מבצעת את הקטנטציה בפועל: המאקרו "הזז" מזיז נתח זיכרון סביב:
הזז את המחרוזת "ptr" לסוף ה-PV - זו ההתחלה של ה-PV פלוס הזרם שלו
אורך. אנו מזיזים בתים "len" מסוג "char". לאחר שנעשה זאת, עלינו לספר לפרל
הרחבנו את המחרוזת, על ידי שינוי "CUR" כדי לשקף את האורך החדש. "SvEND" הוא מאקרו
מה שנותן לנו את סוף המחרוזת, אז זה צריך להיות "\0".
קו 13 עושה מניפולציות על הדגלים; מכיוון ששינינו את ה-PV, כל ערכי IV או NV לא יהיו
עוד יהיה תקף: אם יש לנו "$a=10; $a.="6";" אנחנו לא רוצים להשתמש ב-IV הישן של 10.
"SvPOK_only_utf8" היא גרסה מיוחדת המודעת ל-UTF-8 של "SvPOK_only", מאקרו שהופך
מכבה את דגלי IOK ו-NOK ומפעיל את POK. ה-"SvTAINT" הסופי הוא מאקרו שמכבס
נתונים נגועים אם מצב הכתמה מופעל.
AVs ו-HVs הם יותר מסובכים, אבל SVs הם ללא ספק סוג המשתנים הנפוץ ביותר
נזרק מסביב. לאחר שראינו משהו מהאופן שבו אנו מתמרנים אותם, בואו נמשיך ונסתכל
כיצד בנוי עץ הפעולה.
OP עצים
ראשית, מהו בכלל עץ הניתוח? עץ האופ הוא הייצוג המנתח שלך
תוכנית, כפי שראינו בחלק שלנו על ניתוח, ורצף הפעולות הוא זה
פרל עוברת כדי להפעיל את התוכנית שלך, כפי שראינו ב"ריצה".
ניתוח הוא פעולה בסיסית ש-Perl יכול לבצע: כל הפונקציות המובנות ו
אופרטורים הם מבצעים, ויש סדרה של פעולות העוסקות במושגים של המתורגמן
צרכים פנימיים - כניסה ויציאה מבלוק, סיום משפט, שליפת משתנה,
וכן הלאה.
עץ האופ קשור בשתי דרכים: אתה יכול לדמיין שיש שני "מסלולים" דרך
זה, שני סדרים שבהם אתה יכול לחצות את העץ. ראשית, סדר הניתוח משקף כיצד
מנתח הבין את הקוד, ושנית, סדר ביצוע אומר ל-perl איזו הזמנה לבצע
הפעולות ב.
הדרך הקלה ביותר לבחון את עץ הפעולה היא לעצור את פרל לאחר שהוא סיים לנתח, ו
לגרום לו לזרוק את העץ. זה בדיוק מה שהמהדר מגן על B::Terse,
B::תמציתית ו-B::Debug לעשות.
בואו נסתכל כיצד פרל רואה את "$a = $b + $c":
% perl -MO=Terse -e '$a=$b+$c'
1 LISTOP (0x8179888) עוזב
2 OP (0x81798b0) הזן
3 COP (0x8179850) nextstate
4 BINOP (0x8179828) הקצה
5 BINOP (0x8179800) הוסף [1]
6 UNOP (0x81796e0) null [15]
7 SVOP (0x80fafe0) gvsv GV (0x80fa4cc) *b
8 UNOP (0x81797e0) null [15]
9 SVOP (0x8179700) gvsv GV (0x80efeb0) *c
10 UNOP (0x816b4f0) null [15]
11 SVOP (0x816dcf0) gvsv GV (0x80fa460) *a
נתחיל באמצע, בשורה 4. זהו BINOP, אופרטור בינארי, שנמצא ב-
מיקום 0x8179828. האופרטור הספציפי המדובר הוא "sassign" - הקצאה סקלרית -
ואתה יכול למצוא את הקוד שמיישם אותו בפונקציה "pp_sassign" ב pp_hot.c. כמו
אופרטור בינארי, יש לו שני ילדים: האופרטור add, המספק את התוצאה של "$b+$c",
הוא העליון ביותר בקו 5, והצד השמאלי נמצא על קו 10.
שורה 10 היא ה-null op: זה לא עושה כלום. מה זה עושה שם? אם אתה רואה
ה-null op, זה סימן שמשהו עבר אופטימיזציה לאחר הניתוח. כפי שאנו
המוזכר ב"אופטימיזציה", שלב האופטימיזציה ממיר לפעמים שתי פעולות ל
אחד, למשל בעת שליפת משתנה סקלרי. כשזה קורה, במקום לכתוב מחדש
את עץ הפעולה ולנקות את המצביעים המשתלשלים, קל יותר פשוט להחליף את
פעולה מיותרת עם ה-null op. במקור, העץ היה נראה כך:
10 SVOP (0x816b4f0) rv2sv [15]
11 SVOP (0x816dcf0) gv GV (0x80fa460) *a
כלומר, קחו את הערך "a" מטבלת הסמלים הראשית, ואז הסתכלו על הסקלר
רכיב של זה: "gvsv" ("pp_gvsv" לתוך pp_hot.c) במקרה עושה את שני הדברים האלה.
צד ימין, החל משורה 5 דומה למה שראינו זה עתה: יש לנו את
"add" op ("pp_add" גם ב pp_hot.c) הוסיפו שני "gvsv"ים.
עכשיו, על מה זה?
1 LISTOP (0x8179888) עוזב
2 OP (0x81798b0) הזן
3 COP (0x8179850) nextstate
"כניסה" ו"עזוב" הם פעולות scoping, ותפקידם לבצע כל ניקיון בית בכל
בזמן שאתה נכנס ויוצא מגוש: משתנים לקסיקליים מסודרים, משתנים ללא הפניה
נהרסים וכן הלאה. לכל תוכנית יהיו שלוש השורות הראשונות: "עזוב" הוא א
רשימה, והילדים שלה הם כל ההצהרות בבלוק. הצהרות מוגבלות על ידי
"nextstate", אז בלוק הוא אוסף של פעולות "nextstate", עם הפעולות שיש לבצע
כל הצהרה היא הילדים של "המדינה הבאה". "כניסה" היא פעולה אחת אשר
מתפקד כסמן.
כך פרל פרל את התוכנית, מלמעלה למטה:
תָכְנִית
|
הצהרה
|
=
/
/
$a +
/
$b $c
עם זאת, זה בלתי אפשרי לבצע הפעולות בסדר הזה: עליך למצוא את
ערכים של $b ו-$c לפני שתחבר אותם יחד, למשל. אז, השרשור השני זה
עובר דרך עץ ה-op הוא צו הביצוע: לכל מבצע יש שדה "op_next" אשר
מצביע על המבצע הבא שיופעל, אז מעקב אחר מצביעים אלה אומר לנו כיצד perl מבצע
הקוד. אנו יכולים לחצות את העץ בסדר הזה באמצעות האפשרות "exec" ל-"B::Terse":
% perl -MO=Terse,exec -e '$a=$b+$c'
1 OP (0x8179928) הזן
2 COP (0x81798c8) nextstate
3 SVOP (0x81796c8) gvsv GV (0x80fa4d4) *b
4 SVOP (0x8179798) gvsv GV (0x80efeb0) *c
5 BINOP (0x8179878) הוסף [1]
6 SVOP (0x816dd38) gvsv GV (0x80fa468) *a
7 BINOP (0x81798a0) הקצה
8 LISTOP (0x8179900) עוזב
זה כנראה הגיוני יותר עבור אדם: הזן בלוק, התחל הצהרה. להשיג את
ערכים של $b ו-$c, ולחבר אותם יחד. מצא $a והקצה אחד לשני. לאחר מכן
לעזוב.
ניתן לפענח את הדרך שבה פרל בונה את עצי ההפעלה הללו בתהליך הניתוח
בחינה perly.y, דקדוק YACC. בואו ניקח את היצירה שאנחנו צריכים כדי לבנות את העץ
עבור "$a = $b + $c"
מונח 1: מונח ASSIGNOP מונח
2 { $$ = newASSIGNOP(OPf_STACKED, $1, $2, $3); }
3 | מונח ADDOP מונח
4 { $$ = newBINOP($2, 0, scalar($1), scalar($3)); }
אם אתה לא רגיל לקרוא דקדוקי BNF, ככה זה עובד: אתה ניזון בוודאות
דברים של האסימון, שבסופו של דבר מגיעים באותיות גדולות. כאן מסופק "ADDOP".
כאשר האסימון רואה "+" בקוד שלך. "ASSIGNOP" מסופק כאשר "=" משמש עבור
הקצאה. אלה הם "סמלים מסוף", כי אתה לא יכול להיות יותר פשוט מהם.
הדקדוק, שורות XNUMX ו-XNUMX של הקטע שלמעלה, אומר לך איך לבנות עוד
צורות מורכבות. צורות מורכבות אלה, "סמלים שאינם סופניים" ממוקמים בדרך כלל בתחתית
מקרה. "מונח" כאן הוא סמל לא סופני, המייצג ביטוי בודד.
הדקדוק נותן לך את הכלל הבא: אתה יכול לעשות את הדבר בצד שמאל של המעי הגס
אם אתה רואה את כל הדברים בצד ימין ברצף. זה נקרא "הפחתה", וה
מטרת הניתוח היא לצמצם לחלוטין את הקלט. ישנן מספר דרכים שונות שאתה יכול
בצע הפחתה, מופרדים על ידי פסים אנכיים: אז, "מונח" ואחריו "=" ואחריו
"מונח" עושה "מונח", ו-"מונח" ואחריו "+" ואחריו "מונח" יכולים גם ליצור
"טווח".
לכן, אם אתה רואה שני מונחים עם "=" או "+", ביניהם, אתה יכול להפוך אותם ליחיד
ביטוי. כאשר אתה עושה זאת, אתה מבצע את הקוד בבלוק בשורה הבאה: אם אתה
ראה "=", תעשה את הקוד בשורה 2. אם תראה "+", תעשה את הקוד בשורה 4. זה
הקוד הזה שתורם לעץ האופ.
| מונח ADDOP מונח
{ $$ = newBINOP($2, 0, scalar($1), scalar($3)); }
מה שזה עושה הוא יוצר אופציה בינארית חדשה ומזין אותו במספר משתנים. ה
משתנים מתייחסים לאסימונים: $1 הוא האסימון הראשון בקלט, $2 השני, וכך
על - חשבו על הפניות לאחור של ביטוי רגולרי. $$ הוא האופ המוחזר מההפחתה הזו.
אז, אנו קוראים "newBINOP" כדי ליצור אופרטור בינארי חדש. הפרמטר הראשון ל-"newBINOP",
פונקציה ב אופ.ג, הוא סוג האופ. זה אופרטור תוספת, אז אנחנו רוצים שהסוג יהיה
"ADDOP". אנחנו יכולים לציין את זה ישירות, אבל זה ממש שם בתור האסימון השני ב-
קלט, אז אנחנו משתמשים ב-$2. הפרמטר השני הוא הדגלים של המבצע: 0 פירושו "שום דבר מיוחד".
ואז הדברים שצריך להוסיף: הצד השמאלי והימני של הביטוי שלנו, בהקשר סקלרי.
ערימות
כאשר perl מבצע משהו כמו "addop", איך הוא מעביר את התוצאות שלו למבצע הבא?
התשובה היא, באמצעות שימוש בערימות. לפרל יש מספר ערימות לאחסון דברים שהם
כרגע עובדים על, ונסתכל על שלושת החשובים ביותר כאן.
טיעון לערום
ארגומנטים מועברים לקוד PP ומוחזרים מקוד PP באמצעות ערימת הארגומנטים, "ST".
הדרך האופיינית לטפל בטיעונים היא להוציא אותם מהערימה, להתמודד איתם איך שאתה
רוצה, ולאחר מכן דחוף את התוצאה חזרה לערימה. כך, למשל, הקוסינוס
המפעיל עובד:
ערך NV;
ערך = POPn;
value = Perl_cos(value);
XPUSHn(value);
נראה דוגמה מסובכת יותר לכך כאשר נשקול את פקודות המאקרו של Perl להלן. "POPn" נותן
אתה ה-NV (ערך נקודה צפה) של ה-SV העליון בערימה: ה-$x ב-"cos($x)". אז אנחנו
חשב את הקוסינוס, ודחוף את התוצאה אחורה כ-NV. ה-"X" ב-"XPUSHn" אומר שה-
יש להרחיב את המחסנית במידת הצורך - זה לא יכול להיות נחוץ כאן, כי אנחנו יודעים
יש מקום לעוד פריט אחד בערימה, כי זה עתה הסרנו אחד! ה-"XPUSH*"
פקודות מאקרו לפחות מבטיחות בטיחות.
לחלופין, אתה יכול להתעסק עם הערימה ישירות: "SP" נותן לך את האלמנט הראשון פנימה
החלק שלך בערימה, ו-"TOP*" נותן לך את ה-SV/IV/NV/וכו' העליון. על הערימה. כך,
למשל, לבצע שלילה חד-משמעית של מספר שלם:
SETi(-TOPi);
פשוט הגדר את הערך השלם של כניסת המחסנית העליונה לשלילה שלו.
מניפולציה של ערימת טיעונים בליבה זהה בדיוק לזה שהיא ב-XSUBs - ראה
perlxstut, perlxs ו-perlguts לתיאור ארוך יותר של פקודות המאקרו המשמשות בערימה
מניפולציה.
סמן לערום
אני אומר "החלק שלך בערימה" למעלה כי קוד PP לא בהכרח מקבל את השלם
מחסנית לעצמה: אם הפונקציה שלך קוראת לפונקציה אחרת, תרצה רק לחשוף את
ארגומנטים מכוונים לפונקציה הנקראת, ולא (בהכרח) נותנים לה להגיע לשלכם
נתונים. הדרך בה אנו עושים זאת היא לקבל תחתית מחסנית "וירטואלית", חשופה לכל אחד
פוּנקצִיָה. ערימת הסימנים שומרת סימניות למיקומים בערימת הארגומנטים הניתנות לשימוש על ידי כל אחד
פוּנקצִיָה. לדוגמה, כאשר עוסקים במשתנה קשור, (בפנים, משהו עם "P"
קסם) על פרל לקרוא לשיטות עבור גישה למשתנים הקשורים. עם זאת, אנחנו צריכים
להפריד בין הטיעונים שנחשפו לשיטה לטיעון החשוף למקור
פונקציה - החנות או האחזור או מה שזה לא יהיה. הנה בערך איך ה"דחיפה" קשורה
מיושם; ראה "av_push" ב av.c:
1 PUSHMARK(SP);
2 EXTEND(SP,2);
3 PUSHs(SvTIED_obj((SV*)av, mg));
4 PUSHs(val);
5 PUTBACK;
6 ENTER;
7 call_method("PUSH", G_SCALAR|G_DISCARD);
8 עזוב;
בואו נבחן את כל היישום, לצורך תרגול:
1 PUSHMARK(SP);
דחוף את המצב הנוכחי של מצביע הערימה על ערימת הסימון. זה כדי מתי
סיימנו להוסיף פריטים לערימת הארגומנטים, פרל יודע כמה דברים הוספנו
לאחרונה.
2 EXTEND(SP,2);
3 PUSHs(SvTIED_obj((SV*)av, mg));
4 PUSHs(val);
אנו הולכים להוסיף שני פריטים נוספים לערימת הארגומנטים: כאשר יש לך מערך מקושר, ה-
תת שגרת "PUSH" מקבלת את האובייקט ואת הערך שיש לדחוף, וזה בדיוק מה
יש לנו כאן - האובייקט הקשור, מאוחזר עם "SvTIED_obj", והערך, ה-SV "val".
5 PUTBACK;
לאחר מכן אנו אומרים לפרל לעדכן את מצביע המחסנית הגלובלי מהמשתנה הפנימי שלנו: "dSP"
רק נתן לנו עותק מקומי, לא התייחסות לגלובלי.
6 ENTER;
7 call_method("PUSH", G_SCALAR|G_DISCARD);
8 עזוב;
"ENTER" ו-"LEAVE" ממקמים גוש קוד - הם מוודאים שכל המשתנים כן
מסודר, כל מה שעבר לוקליזציה מקבל את הערך הקודם שלו מוחזר, וכן הלאה.
חשבו עליהם כעל "{" ו-"}" של בלוק Perl.
כדי לבצע את הקריאה של שיטת הקסם בפועל, עלינו לקרוא לתת-שגרה במרחב פרל:
"call_method" מטפל בזה, וזה מתואר ב-perlcall. אנחנו קוראים ל"דחיפה"
שיטה בהקשר סקלרי, ואנחנו הולכים להשליך את ערך ההחזר שלה. ה call_method()
הפונקציה מסירה את הרכיב העליון של מחסנית הסימנים, כך שאין מה למתקשר
לנקות.
שמור לערום
ל-C אין מושג של היקף מקומי, אז perl מספק אחד. ראינו ש"ENTER" ו
"LEAVE" משמשים כפלטה לטווחים; ערימת השמירה מיישמת את המקבילה C של, for
דוּגמָה:
{
$foo מקומי = 42;
...
}
ראה "לוקליזציה של שינויים" ב-perlguts כיצד להשתמש בערימת השמירה.
מיליונים OF מקרו
דבר אחד שתבחין במקור Perl הוא שהוא מלא בפקודות מאקרו. לחלק יש
כינו את השימוש המקיף בפקודות מאקרו הדבר הקשה ביותר להבנה, אחרים מוצאים שזה מוסיף
בְּהִירוּת. ניקח דוגמה, הקוד שמיישם את אופרטור ההוספה:
1 PP(pp_add)
2 {
3 dSP; dATARGET; tryAMAGICbin(add,opASSIGN);
4 {
5 dPOPTOPnnrl_ul;
6 SETn( שמאל + ימין);
7 החזרה;
8}
9}
כל שורה כאן (חוץ מהסוגרים, כמובן) מכילה מאקרו. השורה הראשונה קובעת
להעלות את הצהרת הפונקציה כפי ש-Perl מצפה עבור קוד PP; שורה 3 מגדירה משתנה
הצהרות עבור מחסנית הארגומנטים והיעד, ערך ההחזרה של הפעולה.
לבסוף, הוא מנסה לראות אם פעולת ההוספה עמוסה מדי; אם כן, המתאים
תתי שגרה נקראת.
שורה 5 היא הצהרת משתנה נוספת - כל הצהרות המשתנים מתחילות ב-"d" - אשר
קופץ מהחלק העליון של ערימת הארגומנט שני NVs (ולכן "nn") ומכניס אותם לתוך
משתנים "ימין" ו"שמאל", ומכאן ה"rl". אלו שני האופרנדים לתוספת
מַפעִיל. לאחר מכן, אנו קוראים "SETn" כדי להגדיר את ה-NV של ערך ההחזרה לתוצאה של הוספה
שני הערכים. זה נעשה, אנחנו חוזרים - המאקרו "RETURN" מוודא שערך ההחזר שלנו
מטופל כראוי, ואנחנו עוברים את המפעיל הבא שיירוץ בחזרה ללולאת הריצה הראשית.
רוב פקודות המאקרו הללו מוסברות בפרלפי, וחלק מהחשובות יותר
מוסבר גם ב-perlxs. שימו לב במיוחד ל"רקע ו
PERL_IMPLICIT_CONTEXT" ב-perlguts לקבלת מידע על פקודות המאקרו "[pad]THX_?".
נוסף קריאה
למידע נוסף על הפרל הפנימיים, עיין במסמכים המפורטים ב"פנימיים
ו-C Language Interface" ב-perl.
השתמש ב-perlinterp באינטרנט באמצעות שירותי onworks.net