لینک کوتاه مطلب : https://hsgar.com/?p=3712

وقتی Rust بیکن شما را ذخیره می کند چه حسی دارد

احتمالاً شنیده‌اید که Rust type checker می‌تواند یک “کمک خلبان” عالی باشد، به شما کمک می‌کند تا از اشکالات ظریفی که می‌توانستند در !@#!$ باشند، اجتناب کنید! برای رفع اشکال این واقعاً عالی است! اما چیزی که ممکن است متوجه نباشید این است که چه احساسی دارد در حال حاضر وقتی این اتفاق می افتد پاسخ به طور معمول این است: واقعا، واقعا خسته کننده! معمولاً سعی می کنید کدی برای کامپایل دریافت کنید و متوجه می شوید که نمی توانید آن را انجام دهید.

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

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

کد موجود در این مخزن

تمام کدهای این پست وبلاگ در a موجود است مخزن github.

تنظیم صحنه: پایین آوردن AST

در کامپایلر، ابتدا برنامه های Rust را با استفاده از یک نشان می دهیم درخت نحوی چکیده (AST). من یک را آماده کرده ام مثال مستقل که تقریباً نشان می دهد که کد امروز چگونه به نظر می رسد (البته چیز واقعی بسیار پیچیده تر است). AST به ویژه در یافت می شود ماژول ast شامل ساختارهای داده مختلف است که از نزدیک به نحو Rust نگاشت می شوند. بنابراین به عنوان مثال ما یک Ty نوعی که نشان دهنده انواع Rust است:

pub enum Ty {
    ImplTrait(TraitRef),
    NamedType(String, Vec<Ty>),
    // ...
}

pub struct Lifetime {
    // ...
}

در impl Trait ارجاعات علامت گذاری الف TraitRef، که ذخیره می کند Trait بخشی از چیزها:

pub struct TraitRef {
    pub trait_name: String,
    pub parameters: Parameters,
}

pub enum Parameters {
    AngleBracket(Vec<Parameter>),
    Parenthesized(Vec<Ty>),
}

pub enum Parameter {
    Ty(Ty),
    Lifetime(Lifetime),
}

توجه داشته باشید که پارامترهای این صفت در دو نوع هستند، براکت زاویه (به عنوان مثال، impl PartialEq<T> یا impl MyTrait<'a, U>) و پرانتز شده (به عنوان مثال، impl FnOnce(String, u32)). این دو کمی متفاوت هستند – برای مثال پارامترهای پرانتز شده فقط انواع را می پذیرند، در حالی که براکت زاویه انواع یا طول عمر را می پذیرد.

پس از تجزیه، این AST به چیزی به نام ترجمه می شود نمایندگی سطح بالا متوسط ​​(HIR) از طریق فرآیندی به نام پایین آوردن. قطعه شامل HIR نمی شود، اما شامل تعدادی روش مانند lower_ty که یک نوع AST را ورودی می گیرند و نوع HIR را تولید می کنند:

impl Context {
    fn lower_ty(&mut self, ty: &ast::Ty) -> hir::Ty {
        match ty {
            // ... lots of stuff here
            // A type like `impl Trait`
            ast::Ty::ImplTrait(trait_ref) => {
                do_something_with(trait_ref);
            }

            // A type like `Vec<T>`, where `Vec` is the name and
            // `[T]` are the `parameters`
            ast::Ty::NamedType(name, parameters) => {
                for parameter in parameters {
                    self.lower_ty(parameter);
                }
            }
        }
        // ...
    }
}

هر روش در این مورد تعریف شده است Context نوعی که دارای یک حالت مشترک است و متدها تمایل به فراخوانی یکدیگر دارند. مثلا، lower_signature فراخوانی میکند lower_ty در تمام انواع ورودی (آرگمون) و در نوع خروجی (بازگشت):

impl Context {
    fn lower_signature(&mut self, sig: &ast::Signature) -> hir::Signature {
        for input in &sig.inputs {
            self.lower_ty(input);
        }

        self.lower_ty(&sig.output);

        ...
    }
}

داستان ما شروع می شود

سانتیاگو پاستورینو در حال کار بر روی یک refactoring برای تسهیل پشتیبانی از بازگشت است impl Trait مقادیر از توابع صفت به عنوان بخشی از آن، او باید همه را جمع آوری کند impl Trait انواعی که در آرگومان های تابع ظاهر می شوند. چالش این است که این انواع می توانند در هر جایی ظاهر شوند، و نه فقط در سطح بالا. به عبارت دیگر، ممکن است داشته باشید fn foo(x: impl Debug)، اما ممکن است داشته باشید fn foo(x: Box<(impl Debug, impl Debug)>). بنابراین، ما تصمیم گرفتیم که منطقی باشد که یک بردار به آن اضافه کنیم Context و دارد lower_ty جمع آوری کنید impl Trait در آن تایپ می کند. به این ترتیب، ما می توانیم مجموعه کامل را پیدا کنیم.

برای انجام این کار، با اضافه کردن بردار به این شروع کردیم Context. ما ذخیره می کنیم TraitRef از هر کدام impl Trait نوع:

struct Context<'ast> {
    saved_impl_trait_types: Vec<&'ast ast::TraitRef>,
    // ...
}

برای انجام این کار، ما مجبور شدیم یک پارامتر طول عمر جدید اضافه کنیم، 'ast، که به معنای نشان دادن طول عمر خود ساختار AST است. به عبارت دیگر، saved_impl_trait_types منابع را در AST ذخیره می کند. البته یک بار که این کار را انجام دادیم، کامپایلر ناراحت شد و ما مجبور شدیم که آن را اصلاح کنیم impl آن مراجع را مسدود کنید Context:

impl<'ast> Context<'ast> {
    ...
}

اکنون می توانیم آن را اصلاح کنیم lower_ty برای فشار دادن صفت ref به بردار:

impl<'ast> Context<'ast> {
    fn lower_ty(&mut self, ty: &ast::Ty) {
        match ty {
            ...
            
            ast::Ty::ImplTrait(...) => {
                // 👇 push the types into the vector 👇
                self.saved_impl_trait_types.push(ty);
                do_something();
            }

            ast::Ty::NamedType(name, parameters) => {
                ... // just like before
            }
            
            ...
        }
    }
}

در این مرحله، کامپایلر به ما یک خطا می دهد:

error[E0621]: explicit lifetime required in the type of `ty`
   --> examples/b.rs:125:42
    |
119 |     fn lower_ty(&mut self, ty: &ast::Ty) -> hir::Ty {
    |                                -------- help: add explicit lifetime `'ast` to the type of `ty`: `&'ast ast::Ty`
...
125 |                 self.impl_trait_tys.push(trait_ref);
    |                                          ^^^^^^^^^ lifetime `'ast` required

خطای بسیار خوبی، در واقع! اشاره به این است که ما در حال فشار دادن به این بردار هستیم که نیاز به ارجاع به “AST” دارد، اما در امضای خود اعلام نکرده ایم که ast::Ty در واقع باید از “AST”. خوب، بیایید این را اصلاح کنیم:

impl<'ast> Context<'ast> {
    fn lower_ty(&mut self, ty: &'ast ast::Ty) {
        // had to add 'ast here 👆, just like the error message said
        ...
    }
}

انتشار طول عمر در همه جا

البته، اکنون شروع به دریافت خطا در توابعی می کنیم که زنگ زدن lower_ty. مثلا، lower_signature می گوید:

error[E0621]: explicit lifetime required in the type of `sig`
  --> examples/b.rs:71:18
   |
65 |     fn lower_signature(&mut self, sig: &ast::Signature) -> hir::Signature {
   |                                        --------------- help: add explicit lifetime `'ast` to the type of `sig`: `&'ast ast::Signature`
...
71 |             self.lower_ty(input);
   |                  ^^^^^^^^ lifetime `'ast` required

رفعش هم همینه ما به کامپایلر می گوییم که ast::Signature بخشی از “AST” است، و این نشان می دهد که ast::Ty ارزش های متعلق به ast::Signature همچنین بخشی از “AST” هستند:

impl<'ast> Context<'ast> {
    fn lower_signature(&mut self, sig: &'ast ast::Signature) -> hir::Signature {
        //        had to add 'ast here 👆, just like the error message said
        ...
    }
}

عالی. این یک مقدار ادامه دارد. اما بعد… این خطا را می‌زنیم:

error[E0597]: `parameters` does not live long enough
  --> examples/b.rs:92:53
   |
58 | impl<'ast> Context<'ast> {
   |      ---- lifetime `'ast` defined here
...
92 |                 self.lower_angle_bracket_parameters(&parameters);
   |                 ------------------------------------^^^^^^^^^^^-
   |                 |                                   |
   |                 |                                   borrowed value does not live long enough
   |                 argument requires that `parameters` is borrowed for `'ast`
93 |             }
   |             - `parameters` dropped here while still borrowed

این در مورد چیست؟

اوه اوه…

با پرش به آن خط، این تابع را مشاهده می کنیم lower_trait_ref:

impl Context<'ast> {
    // ...
    fn lower_trait_ref(&mut self, trait_ref: &'ast ast::TraitRef) -> hir::TraitRef {
        match &trait_ref.parameters {
            ast::Parameters::AngleBracket(parameters) => {
                self.lower_angle_bracket_parameters(&parameters);
            }
            ast::Parameters::Parenthesized(types) => {
                let parameters: Vec<_> = types.iter().cloned().map(ast::Parameter::Ty).collect();
                self.lower_angle_bracket_parameters(&parameters); // 👈 error is on this line
                
            }
        }

        hir::TraitRef
    }
    // ...
}

پس این در مورد چیست؟ خوب، هدف این کد کمی هوشمندانه است. همانطور که قبلا دیدیم، Rust دارای دو نحو برای trait-ref است، می توانید از پرانتزهایی مانند FnOnce(u32)، در این صورت شما فقط انواع دارید یا می توانید از براکت های زاویه مانند استفاده کنید Foo<'a, u32>، در این صورت می توانید هر دو عمر داشته باشید یا انواع بنابراین این کد به نماد براکت زاویه، که کلی تر است، عادی می شود، و سپس از همان تابع کمکی کاهش دهنده استفاده می کند.

صبر کن! همان جا! آن لحظه بود!

چی؟

آن لحظه ای بود که رست شما را یک دنیا درد نجات داد!

بود؟ این فقط یک جورهایی آزاردهنده به نظر می رسید، و من می گویم، نوعی خطای کامپایل گیج کننده. چه اتفاقی می افتد؟ مشکل اینجاست که parameters یک متغیر محلی است. قراره به زودی آزاد بشه lower_trait_ref برمی گرداند. اما ممکن است این اتفاق بیفتد lower_trait_ref تماس می گیرد lower_ty که یک ارجاع به نوع می گیرد و در آن ذخیره می کند saved_impl_trait_types بردار سپس، بعداً، برخی از کدها سعی می کنند از آن مرجع استفاده کنند و به حافظه آزاد شده دسترسی پیدا کنند. این گاهی اوقات جواب می‌دهد، اما اغلب نه – و اگر فراموش کردید با صفات پرانتزی تست کنید، کد برای همیشه خوب کار می‌کند، بنابراین هرگز متوجه نمی‌شوید.

چطوری میشه اینو تعمیر کرد

شاید از خود بپرسید: عالی است، Rust من را یک دنیا درد نجات داد، اما چگونه آن را برطرف کنم؟ آیا من فقط باید کپی کنم lower_angle_bracket_parameters و دو نسخه دارید؟ زیرا این یک نوع تاسف است.

خوب، راه های مختلفی برای شما وجود دارد ممکن درستش کن یکی از آنها استفاده از an عرصه، مانند typed-arena جعبه عرصه یک استخر حافظه است. به جای ذخیره موقت Vec<Parameter> بردار روی پشته، آن را در یک عرصه قرار می دهیم، و به این ترتیب برای تمام مدتی که چیزها را پایین می آوریم، زنده خواهد ماند. مثال ج در مخزن این رویکرد را اتخاذ می کند. با اضافه کردن شروع می شود arena میدان به Context:

struct Context<'ast> {
    impl_trait_tys: Vec<&'ast ast::TraitRef>,

    // Holds temporary AST nodes that we create during lowering;
    // this can be dropped once lowering is complete.
    arena: &'ast typed_arena::Arena<Vec<ast::Parameter>>,
}

این در واقع یک تغییر ظریف در معنی ایجاد می کند 'ast. استفاده می شود که تنها چیزهایی که با 'ast طول عمر خود «AST» بود، بنابراین داشتن آن طول عمر به معنای بخشی از AST بود. اما اکنون از همان عمر برای برچسب زدن عرصه نیز استفاده می‌شود، بنابراین اگر ما چنین کنیم &'ast Foo این بدان معنی است که داده ها متعلق به آن است یا عرصه یا خود AST.

یادداشت جانبی: علیرغم نام‌هایی که در حال حاضر متأسفم، بیشتر و بیشتر به آن فکر می‌کنم طول عمر پسندیدن 'ast از نظر “چه کسی مالک داده ها است” که می توانید در توضیحات من در پاراگراف قبلی مشاهده کنید. شما می توانید به جای آن فکر کنید 'ast به عنوان یک بازه زمانی (یک “طول عمر”)، در این صورت به زمانی اشاره دارد که Context نوع معتبر است، واقعاً، که باید زیرمجموعه زمانی باشد که عرصه معتبر است و زمانی که خود AST معتبر است، زیرا Context ارجاع به داده های متعلق به هر دو را ذخیره می کند.

حالا می توانیم بازنویسی کنیم lower_trait_ref تماس گرفتن self.arena.alloc():

impl Context<'ast> {
    fn lower_trait_ref(&mut self, trait_ref: &'ast ast::TraitRef) -> hir::TraitRef {
        match &trait_ref.parameters {
            // ...
            ast::Parameters::Parenthesized(types) => {
                let parameters: Vec<_> = types.iter().cloned().map(ast::Parameter::Ty).collect();
                let parameters = self.arena.alloc(parameters); // 👈 added this line!
                self.lower_angle_bracket_parameters(parameters);
            }
        }
        // ...
    }
}

در حال حاضر parameters متغیر در پشته ذخیره نمی شود بلکه در عرصه تخصیص داده می شود. عرصه دارد 'ast مادام العمر، پس خوب است، و همه چیز کار می کند!

فراخوانی کد کاهش دهنده و ایجاد زمینه

اکنون که اضافه کردیم، عرصه، ایجاد زمینه کمی متفاوت به نظر می رسد. چیزی شبیه به این خواهد بود:

let arena = TypedArena::new();
let context = Context::new(&arena);
let hir_signature = context.lower_signature(&signature);

نکته خوب در مورد این این است که، هنگامی که ما با کاهش تمام شد، context حذف خواهد شد و تمام آن گره های موقت آزاد خواهند شد.

راه دیگری برای رفع آن

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

struct Context {
    impl_trait_tys: Vec<ast::TraitRef>, // just clone it!
}

اگر آن کلون خیلی گران است (ممکن است)، از آن استفاده کنید Rc<ast::TraitRef> یا Arc<ast::TraitRef> (این به تغییرات عمیق در AST نیاز دارد تا همه چیز را در آن قرار دهد Rc یا Arc که ممکن است نیاز به ارجاع جداگانه داشته باشد). در این مرحله شما بسیار شبیه به جمع آوری زباله هستید (اگر ارگونومیک کمتری داشته باشید).

باز هم راه دیگری

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

نتیجه

خطاهای کامپایل بسیار خسته کننده هستند، اما ممکن است نشانه ای از این باشد که کامپایلر از ما در برابر خودمان محافظت می کند. در این مورد، زمانی که ما شروع به بازسازی مجدد کردیم، کاملا مطمئن بودم که به خوبی کار می کند، زیرا متوجه نشدم که ما هرگز گره های “موقت AST” را ایجاد کرده ایم، بنابراین فرض کردم که تمام داده ها متعلق به AST اصلی است. . در زبانی مانند C یا C++، این چنین بود خیلی داشتن یک اشکال در اینجا آسان است، و پیدا کردن آن درد وحشتناکی خواهد بود. با Rust، این مشکلی نیست.

البته همه چیز عالی نیست. برای من، انجام این نوع دگرگونی‌ها در طول زندگی یک کلاه قدیمی است. اما برای بسیاری از افراد کاملاً واضح نیست که چگونه وقتی کامپایلر به شما پیام خطا می دهد شروع کنید. وقتی مردم برای کمک به من مراجعه می‌کنند، اولین کاری که سعی می‌کنم انجام دهم این است که بگویم: روابط مالکیت چیست، و ما انتظار داریم که این ارجاعات از کجا بیایند؟ همچنین اکتشافی های مختلفی وجود دارد که من از آنها برای تصمیم گیری استفاده می کنم: آیا به یک پارامتر طول عمر جدید نیاز داریم؟ آیا می‌توانیم یک مورد موجود را دوباره استفاده کنیم؟ من سعی خواهم کرد داستان های بیشتری از این قبیل بنویسم تا آن طرف قضیه را روشن کنم. راستش را بخواهید، نکته اصلی من در اینجا این بود که من بسیار سپاسگزارم که Rust مانع از صرف ساعت ها و ساعت ها برای رفع اشکال یک خرابی ظریف شد!

با اندکی نگاه کردن، پتانسیل زیادی برای بهبود موارد در مورد نشانه گذاری و اصطلاحاتمان می بینم. من فکر می‌کنم که ما باید بتوانیم مواردی مانند این را بسیار ظریف‌تر کنیم، امیدوارم بدون نیاز به پارامترهای طول عمر نام‌گذاری شده و غیره، یا ویرایش‌های زیاد. اما اعتراف می کنم که هنوز نمی دانم چگونه این کار را انجام دهم! 🙂 برنامه من در حال حاضر این است که مراقب ترفندهایی که استفاده می کنم و انواع تحلیل هایی که در ذهنم انجام می دهم باشم و پست های وبلاگی مانند این را بنویسم تا آن روایت ها را به تصویر بکشم. من کسانی از شما که Rust را خوب می شناسند (یا نمی شناسند!) تشویق می کنم که همین کار را انجام دهند.

ضمیمه: چرا نداشته باشیم Context خود را را TypedArena?

ممکن است متوجه شده باشید که استفاده از عرصه یک نوع پیامد آزاردهنده داشت: افرادی که تماس گرفتند Context::new اکنون باید یک منطقه ایجاد و تامین شود:

let arena = TypedArena::new();
let context = Context::new(&arena);
let hir_signature = context.lower_signature(&signature);

این بخاطر این است که Context<'ast> فروشگاه های الف &'ast TypedArena<_>، و بنابراین تماس گیرنده باید عرصه را ایجاد کند. اگر اصلاح کردیم Context به خود را عرصه، پس API می تواند بهتر باشد. پس چرا من این کار را نکردم؟ برای دیدن دلیل، بررسی کنید مثال D (که نمی سازد). در آن مثال، Context به نظر می رسد …

struct Context<'ast> {
    impl_trait_tys: Vec<&'ast ast::TraitRef>,

    // Holds temporary AST nodes that we create during lowering;
    // this can be dropped once lowering is complete.
    arena: typed_arena::Arena<Vec<ast::Parameter>>,
}

سپس باید امضاهای هر تابع را برای گرفتن یک تغییر دهید &'ast mut self:

impl Context<'ast> {
    fn lower_signature(&'ast mut self, sig: &'ast ast::Signature) -> hir::Signature {...}
}

این می گوید: 'ast پارامتر ممکن است به داده های متعلق به خود، یا شاید توسط علامت اشاره داشته باشد. معقول به نظر می رسد، اما اگر سعی کنید بسازید مثال Dبا این حال، شما خطاهای زیادی دریافت می کنید. این یکی از جالب ترین ها برای من است:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> examples/d.rs:98:17
   |
62 | impl<'ast> Context<'ast> {
   |      ---- lifetime `'ast` defined here
...
97 |                 let parameters = self.arena.alloc(parameters);
   |                                  ----------------------------
   |                                  |
   |                                  immutable borrow occurs here
   |                                  argument requires that `self.arena` is borrowed for `'ast`
98 |                 self.lower_angle_bracket_parameters(parameters);
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

این ها برای چیست؟ این در واقع بسیار ظریف است! این داره میگه parameters از اختصاص داده شد self.arena. یعنی همین parameters معتبر خواهد بود تا زمانیکه self.arena معتبر است.

ولی self هست یک &mut Context، به این معنی که می تواند هر یک از فیلدهای را تغییر دهد Context. وقتی زنگ میزنیم self.lower_angle_bracket_parameters()، کاملاً ممکن است که lower_angle_bracket_parameters می تواند عرصه را تغییر دهد:

fn lower_angle_bracket_parameters(&'ast mut self, parameters: &'ast [ast::Parameter]) {
    self.arena = TypedArena::new(); // what if we did this?
    // ...
}

البته کد الان این کار را نمی کند، اما اگر انجام می داد چه؟ پاسخ این است که پارامترها آزاد می شوند، زیرا عرصه ای که آنها را در اختیار دارد آزاد می شود، و بنابراین ما کد مرده خواهیم داشت. اوه!

با در نظر گرفتن همه چیز، من می خواهم آن را ممکن کنم Context مالکیت عرصه، اما در حال حاضر بسیار چالش برانگیز است. این نمونه خوبی از الگوهای کد است که می‌توانیم فعال کنیم، اما به پسوندهای زبانی نیاز دارد.

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.