رفتن به محتوا

هوش کد

Control Center چهار راه در اختیار عامل می‌گذارد تا کد را فراتر از خواندنش بفهمد. این راه‌ها به پرسش‌هایی واقعاً متفاوت پاسخ می‌دهند، و دلیل اینکه چهار راه هست و نه یکی این است که هیچ سازوکار واحدی به همهٔ آن‌ها پاسخ نمی‌دهد.

سازوکار پاسخ می‌دهد به هزینه
گراف کد چه چیزی اینجاست و چه چیزی به آن وابسته است یک گذر نمایه‌سازی؛ پرس‌وجو از آن ارزان است
سرور زبان آیا این واقعاً کامپایل می‌شود و این چه نوعی است یک فرایند سرور به‌ازای هر پروژه
جستجوی ساختاری این شکل کجا ظاهر می‌شود یک تجزیه به‌ازای هر فایل
اشکال‌زدا در این لحظه و در این فریم چه چیزی برقرار بود یک فرایند متوقف‌شده

گراف کد از tree-sitter ساخته می‌شود: نمادها، یال‌های فراخوانی، یک شعاع تأثیر. سریع است، ارزان است و هیچ چیزی دربارهٔ نوع‌ها نمی‌داند. نمی‌تواند به شما بگوید آیا فراخوانی‌ای که همین حالا نوشتید کامپایل می‌شود، آیا آن فیلد روی شیء وجود دارد، یا اینکه تغییر نام‌تان یک صادرات مجدد را از قلم انداخته است.

بدون سرور زبان، عامل هنگام اجرای تست می‌فهمد — یا هرگز. چیزی به‌ظاهر درست می‌نویسد، جلو می‌رود و خطا سه نوبت بعد، چسبیده به تغییری دیگر، سر می‌زند.

پس سود واقعی خودِ ابزار lsp نیست. سود این است که write، edit و apply_patch پیچیده شده‌اند: پس از هر نوشتن، فایل از سرور زبان می‌گذرد و پیام‌های تشخیصی تازه‌معرفی‌شده در نتیجهٔ ابزار گنجانده می‌شوند. عامل همان لحظه‌ای که بیلد را شکسته است می‌فهمد که شکسته، چه به فکر پرسیدنش افتاده باشد و چه نه.

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

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

تشخیص یک اشتراک است: هم باید نشانگر ریشه‌ای در چک‌اوت وجود داشته باشد و هم باینری باید قابل پیدا شدن باشد. نشانگر به‌تنهایی می‌گوید «این پروژه Python است» و هیچ چیزی دربارهٔ نصب‌بودن سرور نمی‌گوید؛ باینریِ قابل پیدا شدن به‌تنهایی می‌گوید میزبان یک زنجیرهٔ ابزار دارد و هیچ چیزی دربارهٔ این چک‌اوت نمی‌گوید. تنها هر دو با هم یعنی یک پرس‌وجو می‌تواند موفق شود، و ابزاری که عرضه می‌شود و همیشه شکست می‌خورد از ابزاری که وجود ندارد بدتر است.

چرا جستجوی ساختاری، وقتی grep وجود دارد

بخش با عنوان “چرا جستجوی ساختاری، وقتی grep وجود دارد”

grep با کاراکترها مطابقت می‌کند. آن ابزار درست برای یک نام است و ابزار نادرست برای یک شکل.

یک الگوی ساختاری با همان گرامری تجزیه می‌شود که فایل با آن تجزیه می‌شود، بنابراین dispose($X) فراخوانی را پیدا می‌کند، هرگونه که فاصله‌گذاری شده، شکسته یا چندخطی شده باشد، و هرگز همان کاراکترها را درون یک رشتهٔ متنی یا یک کامنت مطابقت نمی‌دهد.

ویژگی‌ای که آن را بیش از یک regex با چند مرحلهٔ اضافه می‌کند این است که فرامتغیری که در یک الگو تکرار می‌شود باید همان متن را بگیرد:

if ($X != null) $X.dispose()

با گاردی مطابقت می‌کند که همان چیزی را که آزموده آزاد می‌کند و با گاردی که چیز دیگری را آزاد می‌کند مطابقت نمی‌کند. این دسته‌ای از باگ است که اصلاً نمی‌توان آن را به‌صورت متنی بیان کرد.

روی همان tree-sitter که پیش‌تر پیوند شده اجرا می‌شود

بخش با عنوان “روی همان tree-sitter که پیش‌تر پیوند شده اجرا می‌شود”

موتور دومی وجود ندارد. cc_natives همین حالا tree-sitter و گرامرهایش را برای گراف کد بارگذاری می‌کند، همراه با یک کش پرس‌وجوی کامپایل‌شده و یک جدول زبان. تطبیق‌دهنده، Dart خالص روی درخت‌هایی است که در هر حال تجزیه می‌شدند. موتور ساختاری دوم یعنی تجزیه‌گر دوم، مجموعهٔ گرامر دومی که باید روی هر پلتفرم ساخته و آماده شود، و دو پاسخ به این پرسش که «فراخوانی‌بیان چیست».

یک قطعه به جایی نیاز دارد که در آن بنشیند

بخش با عنوان “یک قطعه به جایی نیاز دارد که در آن بنشیند”

گرامر فقط تعریف می‌کند که یک فایل کامل چیست. dispose(x) وقتی خالی در سطح بالای Dart تجزیه شود، یک امضای تابع کاملاً معتبر است — اعلانی به نام dispose که پارامتری به نام x می‌گیرد — و هیچ خطایی در هیچ جا نیست که آن را لو بدهد. تطبیق‌دهنده‌ای که روی آن تجزیه ساخته شود، دنبال اعلان‌ها می‌گردد، هیچ فراخوانی‌ای پیدا نمی‌کند و برای الگویی که هرگز آن‌گونه که به نظر می‌رسد خوانده نمی‌شد، «هیچ موردی» را گزارش می‌کند.

بنابراین یک الگو در نردبانی از داربست‌ها آزموده می‌شود — دستور، عبارت، عضو کلاس، سطح بالا — و نخستینی که آن را می‌پذیرد برنده است. قطعه‌ای که هیچ زمینه‌ای نمی‌پذیرد، به‌عنوان تجزیه‌ناپذیر گزارش می‌شود، نه اینکه جست‌وجو شود و بی‌صدا پیدا نشود.

چرا بازنویسی به‌جای نوشتن، در مرحلهٔ آماده‌سازی می‌ماند

بخش با عنوان “چرا بازنویسی به‌جای نوشتن، در مرحلهٔ آماده‌سازی می‌ماند”

بازنویسی ساختاری گسترده‌ترین ویرایشی است که یک عامل می‌تواند انجام دهد: یک الگو، چهل فایل، و هیچ‌کس نتیجه را خط‌به‌خط نمی‌خواند — نه مدل و نه کسی که آن را خواسته است. نوشتنش و گزارش دادن «انجام شد» یعنی نخستین نگاه صادقانه به تغییر، بعدها یک git diff است.

بنابراین ast_edit گزارش می‌دهد چه کاری انجام می‌داد — تعداد، فایل‌ها، جاهای مطابقت — و تغییر تنها زمانی اعمال می‌شود که resolve آن را کامیت کند. همین هم باعث می‌شود یک diff و گزینهٔ پذیرش/دور انداختن، پیش از رسیدن هر چیزی به دیسک، جلوی چشم یک انسان قرار بگیرد.

کامیت همه‌یاهیچ است و هر فایل با محتوایی بررسی می‌شود که در زمان آماده‌سازی ثبت شده است. میان آماده‌سازی و کامیت، ممکن است عامل یکی از آن فایل‌ها را دستی ویرایش کرده باشد، یک گذر تشخیصی آن را بازنویسی کرده باشد، یا یک ابزار قالب‌بندی به آن دست زده باشد — کامیت کردن به‌رغم آن، آن کار را دور می‌ریزد و diff طوری به نظر می‌رسد که گویی عمدی بوده است. هر ناهمخوانی کل تغییر را رد می‌کند، چون بازنویسی ساختاری نیمه‌اعمال‌شده درختی بر جا می‌گذارد که زیر هیچ‌یک از آن دو شکل کامپایل نمی‌شود.

آنچه اشکال‌زدا پاسخ می‌دهد و یک دستور print نمی‌تواند

بخش با عنوان “آنچه اشکال‌زدا پاسخ می‌دهد و یک دستور print نمی‌تواند”

حلقهٔ دستور print این است: یک print اضافه کن، کل ماجرا را دوباره اجرا کن، خروجی را بخوان، print را پاک کن. هزینه‌اش یک اجرای کامل تست به‌ازای هر پرسش است و تنها به پرسشی پاسخ می‌دهد که پیش از اجرا به ذهن‌تان رسیده است.

یک فریم متوقف‌شده هم‌زمان به همهٔ پرسش‌ها دربارهٔ آن لحظه پاسخ می‌دهد — هر متغیر محلی، هر فراخوان، و عبارتی که در حوزهٔ خود فریم ارزیابی می‌شود. برای یک فرایند قفل‌شده یا خطایی که یک بار در ده اجرا بازتولید می‌شود، این تفاوت یک بعدازظهر و یک دقیقه است.

کران‌دار مثل یک محفظه، و به همان دلیل

بخش با عنوان “کران‌دار مثل یک محفظه، و به همان دلیل”

یک آداپتور اشکال‌زدا مالک یک فرایند متوقف‌شده است که هر چه آن فرایند نگه داشته را نگه می‌دارد — یک پورت، یک قفل، یک اتصال پایگاه داده. نشستی که کسی آن را هدایت نمی‌کند، نشتی است که از نوبتی که آن را آغاز کرده عمر بیشتری می‌کند. پس یک TTL سخت وجود دارد، دقیقاً یک نشست به‌ازای هر گفتگو، و launch دوم رد می‌شود، نه اینکه بی‌صدا جای اولی را بگیرد: عاملی که دومی را بدون پایان دادن به اولی شروع کند، تقریباً قطعاً اولی را از دست داده است.

Debug Adapter Protocol همان قالب‌بندی Content-Length مربوط به Language Server Protocol را دارد و هیچ چیز بالاتر از آن را — کلیدهای همبستگی متفاوت، شکل خطای متفاوت، و یک نوع پیام سوم (رویدادها) بدون همتای LSP. قالب‌بندی در یک جا زندگی می‌کند؛ پروتکل‌ها جداگانه روی آن می‌نشینند.

نوشتن دوبارهٔ قالب‌بندی یعنی دو پاسخ به «یک پیام دو تکه را در بر می‌گیرد»، «یک تکه سه پیام را نگه می‌دارد» و «سرور پیش از نخستین فریمش یک بنر به stdout نوشته است»، و فقط یکی از آن‌ها اصلاحیه را می‌گیرد.