درس ۱۹ از ۴۹ · برنامه‌های بزرگ‌تر

قواعد مترجم

مترجم سلام خیلی از اشتباه‌ها را پیش از اجرای برنامه می‌گیرد. هر پیام می‌گوید چه چیزی کجا اشتباه است و معمولاً راه درست کردنش را هم می‌گوید. این درس قاعده‌های پشت پیام‌هایی را که بیشتر می‌بینید یکی‌یکی مرور می‌کند.

چرا این‌قدر سخت‌گیر؟

هر خطایی که مترجم بگیرد، خطایی است که هرگز به دست کاربر نمی‌رسد. برای همین سلام چیزهایی را خطا می‌داند که در زبان‌های دیگر فقط هشدارند: متغیری که کسی نمی‌خواندش، ناپایایی که هیچ‌وقت عوض نمی‌شود، شاخه‌ای که هرگز اجرا نمی‌شود. هشدارها روی هم جمع می‌شوند و نادیده گرفته می‌شوند، ولی خطا همان لحظه درست می‌شود؛ پس برنامه‌ی سلامی که ترجمه شود، چنین باقی‌مانده‌هایی ندارد.

خواندن پیام خطا

یک پیام معمولی این شکلی است:

[SEMANTIC][ERROR] E013: نمی‌توان متغیر تغییرناپذیر 'جمع' را دوباره مقداردهی کرد؛ برای این کار آن را 'ناپایا' تعریف کنید، یا به‌جای آن به عناصر یا فیلدهایش مقدار دهید (app.salam:3:5)

  • E013 کد خطاست. این کد در فارسی و انگلیسی یکی است، پس برای جست‌وجو همین را به کار ببرید.
  • پس از آن مشکل می‌آید و بعد از نقطه‌ویرگول، راه معمول درست کردنش. پیام را تا آخر بخوانید؛ اغلب همان راه‌حل کافی است.
  • آخر خط نام پرونده، شماره‌ی خط و ستون است: خط ۳، ستون ۵ از app.salam.

اشتباه در شکل کد، مثل جاافتادن پایان یا واژه‌ای که در سلام نیست، را تجزیه‌گر گزارش می‌دهد. این پیام‌ها خود خط را با یک نشانه زیر جای اشتباه نشان می‌دهند، مثل خطا: '&&' عملگر سلام نیست؛ به جای آن 'و' بنویسید. پرونده‌ی فارسی پیام‌هایش را به فارسی می‌گیرد.

اول نخستین خطا را درست کنید. یک اشتباه می‌تواند خطاهای دیگری هم پایین‌تر بسازد: اگر نامی را غلط بنویسید، مترجم هم نام ناشناخته را گزارش می‌دهد (E001) و هم متغیری را که منظورتان بود و حالا بی‌استفاده مانده (E059).

هیچ چیز بی‌استفاده

متغیر، ورودی، تابع خصوصی یا واردسازی‌ای که هیچ‌جا به کار نرود خطاست: E059، E062، E066 و E082. معمولاً راه درست حذف آن است، یا این‌که بفهمید یادتان رفته به کارش ببرید. شروع کردن نام با _ خطا را ساکت می‌کند، ولی آن را برای تنها حالت واقعی‌اش نگه دارید: امضایی که دست شما نیست. این‌جا هر پاسخگو باید کاربر و متن را بگیرد و فریاد به کاربر نیازی ندارد:

rules-unused.salam
میانجی پاسخگو:
    تابع پاسخ(کاربر: رشته، متن: رشته): رشته
پایان

ساختار پژواک:
    همگانی تابع پاسخ(کاربر: رشته، متن: رشته): رشته:
        برگشت کاربر + " گفت " + متن
    پایان
پایان

ساختار فریاد:
    همگانی تابع پاسخ(_کاربر: رشته، متن: رشته): رشته:
        برگشت متن + "!"
    پایان
پایان

تابع اجرا(پ: پویا پاسخگو):
    سرچاپ پ.پاسخ("سارا"، "سلام")
پایان

تابع ریشه:
    اجرا(پژواک {})
    اجرا(فریاد {})
پایان
خروجیسارا گفت سلام سلام!

بدون _ مترجم می‌گفت E062: پارامتر استفاده‌نشده 'کاربر'، و خود پیامش می‌گوید _ را فقط وقتی به کار ببرید که امضا ثابت است.

ناپایا فقط جایی که چیزی عوض می‌شود

متغیری که با := ساخته شود دوباره مقدار نمی‌گیرد (E013). وقتی باید عوض شود ناپایا اضافه کنید، و فقط همان وقت: ناپایایی که هرگز عوض نشود هم خطاست (E069). افزودن به وکتور یا نگاشت داده‌ای را عوض می‌کند که به آن اشاره دارند، نه خود متغیر را، پس بیفزا بدون ناپایا هم کار می‌کند. مقداری که در تمام برنامه ثابت است پایا است.

rules-mut.salam
پایا سقف := ۳

تابع ریشه:
    نام‌ها := وکتور {} برگردان وکتور<رشته>
    دیرکن نام‌ها.آزادکن()
    نام‌ها.بیفزا("سارا")
    نام‌ها.بیفزا("رضا")
    نام‌ها.بیفزا("مینا")
    نام‌ها.بیفزا("علی")
    ناپایا نشان‌داده := ۰
    هر ن در نام‌ها:
        اگر نشان‌داده == سقف:
            بشکن
        پایان
        سرچاپ ن
        نشان‌داده++
    پایان
    سرچاپ نشان‌داده، "از"، نام‌ها.طول()، "نام نشان داده شد"
پایان
خروجیسارا رضا مینا 3 از 4 نام نشان داده شد

گونه‌ها هیچ‌وقت حدس زده نمی‌شوند

سلام پشت سر شما گونه‌ای را به گونه‌ی دیگر تبدیل نمی‌کند. گذاشتن رشته جای صحیح در مقداردهی E002 و در صدا زدن تابع E012 است، و شرط باید واقعاً منطقی باشد: اگر کالاها: با یک عدد E021 است، پس بنویسید اگر کالاها > ۰:. دو شاخه‌ی ?: هم باید یک گونه داشته باشند. با برگردان صریحاً تبدیل کنید، ولی نه وقتی مقدار همان گونه را دارد: ن برگردان صحیح روی یک صحیح تبدیل بی‌فایده است، E093.

همه‌ی مسیرها بررسی می‌شوند

مترجم هر مسیر درون تابع را دنبال می‌کند. تابعی که مقدار برمی‌گرداند باید در همه‌ی مسیرها برگرداند (E071: تابع 'علامت' ممکن است بدون بازگرداندن مقداری از گونه 'رشته' به پایان بدنه‌ی خود برسد)، همخوان روی جداشمار باید همه‌ی عضوها را پوشش دهد یا وگرنه داشته باشد (E080 عضوهای جاافتاده را نام می‌برد)، و کدی که پس از برگشت بیاید هرگز اجرا نمی‌شود (E070). همین‌طور است شرطی که همیشه نادرست است (E068)، شاخه‌ی خالی اگر (E060) و دستوری که مقدارش دور ریخته می‌شود، مثل ن + ۱ تنها در یک خط (E110).

rules-flow.salam
جداشمار چراغ: قرمز، زرد، سبز پایان

تابع کار(چ: چراغ): رشته:
    برگشت همخوان چ:
        قرمز: "بایست" پایان
        زرد: "آهسته" پایان
        سبز: "برو" پایان
    پایان
پایان

تابع علامت(ن: صحیح): رشته:
    اگر ن > ۰:
        برگشت "مثبت"
    وگرنه ن < ۰:
        برگشت "منفی"
    پایان
    برگشت "صفر"
پایان

تابع ریشه:
    سرچاپ کار(چراغ.زرد)، کار(چراغ.سبز)
    سرچاپ علامت(-۴)، علامت(۰)، علامت(۹)
پایان
خروجیآهسته برو منفی صفر مثبت

اگر بعدها عضو تازه‌ای به چراغ اضافه شود، هر همخوانی که آن را پوشش ندهد دیگر ترجمه نمی‌شود، و این دقیقاً همان جاهایی است که باید سری بزنید.

مقدارها پیش از اجرای برنامه بررسی می‌شوند

مترجم مقدارهایی را هم که در تابع جریان دارند دنبال می‌کند: شمارنده در هر دور حلقه چه مقداری دارد، یک وکتور پس از افزودن‌ها چند عضو دارد، و آیا یک اشاره‌گر هنوز می‌تواند پوچ باشد. وقتی خطی بی‌گمان اشتباه پیش می‌رود، ترجمه با خطایی می‌ایستد که آن مقدار را نام می‌برد: اندیسی بیرون از محدوده (E146: اندیس خارج از محدوده است: 'i' برابر ۳ است، اما 'xs' ۳ عضو دارد (در دور ۴ حلقه‌ی خط ۳))، عدد علامت‌داری که از گونه‌اش سرریز می‌کند (E148)، تقسیم بر مقداری که ۰ است (E150) یا خواندن از اشاره‌گر پوچ (E152). وقتی فقط برای برخی مقدارها اشتباه پیش می‌رود، به جای خطا هشدار می‌گیرید (W147، W149، W151، W153)، و بررسی‌ای مانند اگر i < xs.len(): پیش از آن خط برای خاموش کردنش کافی است. مقدارهایی که مترجم نمی‌بیند، مانند یک پارامتر یا خروجی یک تابع، هرگز حدس زده نمی‌شوند.

نمونه‌ای که به خاطر سپردنش می‌ارزد: تکرار 0 تا n در i: تا خود n می‌شمارد، پس تکرار 0 تا xs.len() در i: یک عضو پس از پایان را می‌خواند. به جای آن با تکرار xs.len() در i: یا هر i, x در xs: بشمارید.

خطا با پرتاب خطا، تلاش و گرفتن خطا، و کد خروج ریشه

تابعی که ممکن است شکست بخورد این را در امضایش می‌گوید: خطادار پس از گونه‌ی برگشتی. شکست را با پرتاب خطا اعلام می‌کند که یک خطا (یا فقط یک پیام) می‌گیرد. هر فراخوانی چنین تابعی نشانه دارد: تلاش جلوی فراخوانی خطا را به صدازننده می‌فرستد که آن هم باید خطادار باشد؛ بلوک تلاش: ... گرفتن خطا خ: ... پایان خطا را رسیدگی می‌کند و فراخوانی‌های درونش نشانه نمی‌خواهند. تلاش ر() گرفتن خطا مقدار به جای آن یک مقدار جایگزین می‌دهد. چیزی پنهان نیست: در هر خط می‌بینید خطا از کجا ممکن است بیاید، و وقتی خطایی رد می‌شود دیرکن باز هم اجرا می‌شود.

rules-exit.salam
تابع سن از(متن: رشته): صحیح خطادار:
    ن := متن.به صحیح()
    اگر ن <= ۰ یا ن > ۱۵۰:
        پرتاب خطا خطا.جدید(نوع خطا.خارج از بازه، "«" + متن + "» سن نیست")
    پایان
    برگشت ن
پایان

تابع بزرگ‌ترین(متن‌ها: وکتور<رشته>): صحیح خطادار:
    ناپایا بیشینه := ۰
    هر متن در متن‌ها:
        سن := تلاش سن از(متن)
        اگر سن > بیشینه:
            بیشینه = سن
        پایان
    پایان
    برگشت بیشینه
پایان

تابع ریشه:
    هر متن در ["42"، "abc"]:
        تلاش:
            سرچاپ متن، "سن درستی است:"، سن از(متن)
        گرفتن خطا خ:
            سرچاپ خ.پیام
        پایان
    پایان
    جایگزین := تلاش سن از("900") گرفتن خطا ۱۸
    سرچاپ "با مقدار جایگزین:"، جایگزین
    ناپایا متن‌ها := وکتور {} برگردان وکتور<رشته>
    متن‌ها.بیفزا("30")
    متن‌ها.بیفزا("45")
    تلاش:
        سرچاپ "بزرگ‌ترین:"، بزرگ‌ترین(متن‌ها)
    گرفتن خطا خ:
        نادرست‌سرچاپ "بزرگ‌ترین سن پیدا نشد:"، خ.پیام
        برگشت ۱
    پایان
پایان
خروجی42 سن درستی است: 42 «abc» سن نیست با مقدار جایگزین: 18 بزرگ‌ترین: 45

جا انداختن نشانه خطای ترجمه‌ی E155 است و تلاش در تابعی که خطادار نیست E156. یک اشکال برنامه‌نویسی، مثل نمایه‌ی بیرون از محدوده، مقدار خطا نیست: برنامه را متوقف می‌کند و گرفتن خطا آن را نمی‌بیند. بسته‌ی خطا نوع‌های خطا و آنچه یک خطا با خود دارد را فهرست می‌کند.

مقداری که ریشه برمی‌گرداند کد خروج برنامه است: برگشت ۱ شکست را به پوسته خبر می‌دهد و نبودن برگشت یعنی ۰. صدا زدن سیستم عامل.خروج درون ریشه خطای E109 است، چون پاکسازی‌هایی را که ریشه با دیرکن گذاشته جا می‌اندازد.

رایج‌ترین خطاها

کدآنچه نوشته‌ایدپیام مترجمراه درست
E001نامی که غلط نوشته شدهشناسه‌ی ناشناخته 'شمر'املای نام را درست کنید
E002عدد = "پنج"نمی‌توان 'رشته' را به 'صحیح' نسبت دادگونه‌ی درست را بگذارید یا با برگردان تبدیل کنید
E012دوبرابر("۴")، جمع(۱)آرگومان ۱: نمی‌توان 'رشته' را به 'صحیح' پاس داد؛ 'جمع' به ۲ آرگومان نیاز دارد، اما ۱ دریافت شدورودی‌ها را با تعریف تابع جور کنید
E013جمع := ۰ سپس جمع = ۵نمی‌توان متغیر تغییرناپذیر 'جمع' را دوباره مقداردهی کردآن را ناپایا تعریف کنید
E017نام.فریاد()، ح.موجودیرشته متدی به نام 'فریاد' ندارد؛ فیلد 'موجودی' در ساختار 'حساب' خصوصی استروشی را صدا بزنید که وجود دارد، یا عضو را همگانی کنید
E021اگر کالاها:شرط 'اگر' باید بولی باشد، اما 'صحیح' دریافت شدمقایسه بنویسید: اگر کالاها > ۰:
E059عدد := ۵ و به‌کارنبردن آنمتغیر استفاده‌نشده 'عدد'به کارش ببرید یا حذفش کنید
E060اگر با بدنه‌ی خالیبدنه‌ی 'اگر' خالی استبدنه را پر کنید، یا شرط را با وارونه برعکس کنید
E062ورودی‌ای که بدنه نمی‌خواندشپارامتر استفاده‌نشده 'بلندی'حذفش کنید، یا برای امضای ثابت از _ استفاده کنید
E066تابع خصوصی‌ای که صدا زده نمی‌شودتابع 'کمکی' استفاده نشده استصدایش بزنید یا حذفش کنید
E068اگر نادرست:شرط 'اگر' همیشه نادرست است: بدنه‌ی آن هرگز اجرا نمی‌شودشاخه‌ی مرده را حذف کنید
E069ناپایا عدد := ۵ بدون تغییرمتغیر 'عدد' با کلیدواژه‌ی 'ناپایا' تعریف شده اما هرگز تغییر نکرده استناپایا را بردارید
E070دستوری پس از برگشتکد دست‌نیافتنیحذفش کنید یا بالاتر ببریدش
E071مسیری بدون برگشتتابع 'علامت' ممکن است بدون بازگرداندن مقداری از گونه 'رشته' به پایان بدنه‌ی خود برسدیک برگشت پایانی بگذارید
E080همخوانی که عضوی را جا انداختههمخوان ناقص روی جداشمار 'چراغ': عضو(های) زرد پوشش داده نشده استعضو را اضافه کنید یا شاخه‌ی وگرنه بگذارید
E082واردسازی ریاضی بی‌استفادهبسته‌ی واردشده‌ی 'ریاضی' استفاده نشده استواردسازی را حذف کنید
E084پایا پس از یک تابعپایا «نرخ» باید پیش از هر تعریف تابع یا گونه اعلام شودآن را به بالای پرونده ببرید
E087ساختار پس از یک تابع«نقطه» باید پیش از هر تعریف تابعی اعلام شودگونه‌ها را بالای تابع‌ها بگذارید
E090متغیری هم‌نام یک تابع'جمع' از قبل نام یک تابع در این فایل استیکی را تغییر نام دهید
E093ن برگردان صحیح روی یک صحیحتبدیل بی‌فایدهتبدیل را حذف کنید
E108واردسازی پس از فراخوانی«واردسازی رشته» باید پیش از همه‌ی فراخوانی‌ها بیایدواردسازی‌ها را اول بنویسید
E109سیستم عامل.خروج(۱) در ریشهدر «ریشه» تابع «سیستم عامل.خروج» را صدا نزنیدبنویسید برگشت ۱
E110ن + ۱ تنها در یک خطاین دستور هیچ اثری نداردنتیجه را در متغیری بگذارید یا خط را حذف کنید
E162ن = ن + ۱، ن += ۱، ن = ن + ۵، ن = ن'ن = ن + ۱' مجاز نیست: بنویسید 'ن++'؛ 'ن = ن + ۵' مجاز نیست: بنویسید 'ن += ۵'؛ 'ن = ن' کاری انجام نمی‌دهد: این دستور را برداریدبرای یکی زیاد کردن ن++، برای هر مقدار دیگر ن += ۵ بنویسید و دستورهایی را که چیزی عوض نمی‌کنند بردارید

چند اشتباه را تجزیه‌گر می‌گیرد و کد ندارند: && و || (بنویسید و و یا)، تعریف با گونه مثل ن: صحیح = ۲ (بنویسید ن := ۲)، و واژه‌ی رزروشده به‌جای نام، مثل و که همیشه به معنای «and» است.

چند راهنمایی

  • اول نخستین پیام را درست کنید و دوباره بسازید؛ خیلی از پیام‌های بعدی همراهش می‌روند.
  • هر پیام را تا آخر بخوانید. بخش پس از نقطه‌ویرگول معمولاً همان راه‌حل است.
  • برای پنهان کردن خطا سراغ _ یا همگانی نروید. مترجم به کدی اشاره می‌کند که مرده، نیمه‌کاره یا سر جای خودش نیست.
  • به‌جای متن پیام، کد آن را جست‌وجو کنید، مثل E071؛ کد در هر زبانی یکی است.
  • پیش از دنبال کردن خطاهای ترتیب، salam format را اجرا کنید؛ خیلی از تعریف‌ها را خودش سر جایشان می‌گذارد.