Hayat hidup ialah ciri yang menarik bagi Rust dan pengalaman manusia. Ini adalah blog teknikal, jadi mari kita fokus pada yang pertama. Saya diakui adalah pengguna yang lambat untuk memanfaatkan jangka hayat untuk meminjam data dengan selamat dalam Rust. Dalam pelaksanaan treewalk Memphis, penterjemah Python saya yang ditulis dalam Rust, saya hampir tidak memanfaatkan jangka hayat (dengan mengklon tanpa henti) dan saya berulang kali mengelak pemeriksa pinjaman (dengan menggunakan kebolehubah dalaman, juga tanpa henti) apabila boleh.
Rakyat Rustacean saya, saya di sini hari ini untuk memberitahu anda bahawa ini berakhir sekarang. Baca bibir saya…tiada jalan pintas lagi.
Okey okey, biar betul. Apa itu jalan pintas berbanding cara yang betul adalah soal keutamaan dan perspektif. Kita semua telah melakukan kesilapan, dan saya di sini untuk bertanggungjawab ke atas diri saya.
Saya mula menulis jurubahasa enam minggu selepas saya mula-mula memasang rustc kerana saya tidak mempunyai kesejukan. Dengan sikap yang meleret-leret dan postur itu, mari kita mulakan kuliah hari ini tentang cara kita boleh menggunakan jangka hayat sebagai talian hayat kita untuk menambah baik pangkalan kod jurubahasa saya yang kembung.
Satu Hayat karat ialah mekanisme yang menyediakan jaminan masa kompilasi bahawa sebarang rujukan tidak melebihi umur objek yang dirujuknya. Ia membolehkan kita mengelakkan masalah "penunjuk berjuntai" daripada C dan C .
Ini mengandaikan anda memanfaatkannya sama sekali! Pengklonan ialah penyelesaian yang mudah apabila anda ingin mengelakkan kerumitan yang berkaitan dengan pengurusan jangka hayat, walaupun kelemahannya ialah peningkatan penggunaan memori dan sedikit kelewatan yang berkaitan dengan setiap kali data disalin.
Menggunakan seumur hidup juga memaksa anda untuk berfikir lebih idiomatik tentang pemilik dan meminjam dalam Rust, yang saya ingin lakukan.
Saya memilih calon pertama saya sebagai token daripada fail input Python. Pelaksanaan asal saya, yang sangat bergantung pada panduan ChatGPT semasa saya duduk di Amtrak, menggunakan aliran ini:
Aspek yang mudah untuk mengklon aliran token ialah Lexer bebas untuk digugurkan selepas langkah 3. Dengan mengemas kini seni bina saya supaya Lexer memiliki token dan Parser hanya meminjamnya, Lexer kini perlu kekal hidup lebih lama lagi. Jangka hayat karat akan menjamin ini untuk kami: selagi Parser wujud memegang rujukan kepada token yang dipinjam, pengkompil akan menjamin bahawa Lexer yang memiliki token tersebut masih wujud, memastikan rujukan yang sah.
Seperti semua kod selalu, ini akhirnya menjadi perubahan yang lebih besar daripada yang saya jangkakan. Mari lihat sebabnya!
Sebelum mengemas kini Parser untuk meminjam token daripada Lexer, ia kelihatan seperti ini. Dua bidang yang diminati untuk perbincangan hari ini ialah token dan current_token. Kami tidak tahu berapa besar Vec
pub struct Parser { state: Container<State>, tokens: Vec<Token>, current_token: Token, position: usize, line_number: usize, delimiter_depth: usize, } impl Parser { pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self { let current_token = tokens.first().cloned().unwrap_or(Token::Eof); Parser { state, tokens, current_token, position: 0, line_number: 1, delimiter_depth: 0, } } }
Selepas meminjam token daripada Lexer, ia kelihatan agak serupa, tetapi kini kita melihat SEUMUR HIDUP! Dengan menyambungkan token kepada 'a seumur hidup, pengkompil Rust tidak akan membenarkan pemilik token (iaitu Lexer kami) dan token itu sendiri digugurkan semasa Parser kami masih merujuknya. Ini rasa selamat dan mewah!
static EOF: Token = Token::Eof; /// A recursive-descent parser which attempts to encode the full Python grammar. pub struct Parser<'a> { state: Container<State>, tokens: &'a [Token], current_token: &'a Token, position: usize, line_number: usize, delimiter_depth: usize, } impl<'a> Parser<'a> { pub fn new(tokens: &'a [Token], state: Container<State>) -> Self { let current_token = tokens.first().unwrap_or(&EOF); Parser { state, tokens, current_token, position: 0, line_number: 1, delimiter_depth: 0, } } }
Satu lagi perbezaan kecil yang anda mungkin perasan ialah baris ini:
static EOF: Token = Token::Eof;
Ini adalah pengoptimuman kecil yang saya mula pertimbangkan sebaik sahaja Penghurai saya bergerak ke arah "cekap ingatan". Daripada membuat instantiat Token baharu::Eof setiap kali Parser perlu menyemak sama ada ia berada di penghujung strim teks, model baharu membenarkan saya membuat instantiat hanya satu token dan rujukan &EOF berulang kali.
Sekali lagi, ini adalah pengoptimuman kecil, tetapi ia merujuk kepada pemikiran yang lebih besar bagi setiap data yang wujud hanya sekali dalam ingatan dan setiap pengguna hanya merujuknya apabila diperlukan, yang mana Rust menggalakkan anda untuk melakukannya dan memegang tangan anda dengan kemas caranya.
Bercakap tentang pengoptimuman, saya sepatutnya menanda aras penggunaan memori sebelum dan selepas. Memandangkan saya tidak melakukannya, saya tidak mempunyai apa-apa lagi untuk diperkatakan mengenai perkara itu.
Seperti yang saya nyatakan sebelum ini, mengikat seumur hidup Lexer dan Parser saya bersama-sama memberi impak yang besar pada corak Builder saya. Mari lihat rupanya!
Dalam aliran yang saya terangkan di atas, ingat bagaimana saya menyebut bahawa Lexer boleh digugurkan sebaik sahaja Parser mencipta salinan tokennya sendiri? Ini secara tidak sengaja telah mempengaruhi reka bentuk Builder saya, yang bertujuan untuk menjadi komponen yang menyokong mendalangi interaksi Lexer, Parser dan Interpreter, sama ada anda bermula dengan strim teks Python atau laluan ke fail Python.
Seperti yang anda lihat di bawah, terdapat beberapa aspek lain yang tidak sesuai untuk reka bentuk ini:
fn downcast<T: InterpreterEntrypoint + 'static>(input: T) -> Interpreter { let any_ref: &dyn Any = &input as &dyn Any; any_ref.downcast_ref::<Interpreter>().unwrap().clone() } fn init(text: &str) -> (Parser, Interpreter) { let (parser, interpreter) = Builder::new().text(text).build(); (parser, downcast(interpreter)) } #[test] fn function_definition() { let input = r#" def add(x, y): return x + y a = add(2, 3) "#; let (mut parser, mut interpreter) = init(input); match interpreter.run(&mut parser) { Err(e) => panic!("Interpreter error: {:?}", e), Ok(_) => { assert_eq!( interpreter.state.read("a"), Some(ExprResult::Integer(5.store())) ); } } }
Di bawah ialah antara muka MemphisContext baharu. Mekanisme ini menguruskan seumur hidup Lexer secara dalaman (untuk memastikan rujukan kami kekal cukup lama untuk memastikan Parser kami gembira!) dan hanya mendedahkan perkara yang diperlukan untuk menjalankan ujian ini.
pub struct Parser { state: Container<State>, tokens: Vec<Token>, current_token: Token, position: usize, line_number: usize, delimiter_depth: usize, } impl Parser { pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self { let current_token = tokens.first().cloned().unwrap_or(Token::Eof); Parser { state, tokens, current_token, position: 0, line_number: 1, delimiter_depth: 0, } } }
context.run_and_return_interpreter() masih agak kikuk dan bercakap kepada masalah reka bentuk lain yang mungkin saya atasi: apabila anda menjalankan penterjemah, adakah anda mahu memulangkan hanya nilai pulangan akhir atau sesuatu yang membolehkan anda mengakses nilai sewenang-wenangnya daripada jadual simbol? Kaedah ini memilih pendekatan yang terakhir. Saya sebenarnya berpendapat ada kes untuk melakukan kedua-duanya, dan akan terus mengubahsuai API saya untuk membenarkan perkara ini semasa kita pergi.
Secara kebetulan, perubahan ini meningkatkan keupayaan saya untuk menilai sekeping kod Python yang sewenang-wenangnya. Jika anda ingat dari saga WebAssembly saya, saya terpaksa bergantung pada semak silang TreewalkAdapter saya untuk melakukannya pada masa itu. Kini, antara muka Wasm kami jauh lebih bersih.
static EOF: Token = Token::Eof; /// A recursive-descent parser which attempts to encode the full Python grammar. pub struct Parser<'a> { state: Container<State>, tokens: &'a [Token], current_token: &'a Token, position: usize, line_number: usize, delimiter_depth: usize, } impl<'a> Parser<'a> { pub fn new(tokens: &'a [Token], state: Container<State>) -> Self { let current_token = tokens.first().unwrap_or(&EOF); Parser { state, tokens, current_token, position: 0, line_number: 1, delimiter_depth: 0, } } }
Konteks antara muka.evaluate_oneshot() mengembalikan hasil ungkapan dan bukannya jadual simbol penuh. Saya tertanya-tanya sama ada terdapat cara yang lebih baik untuk memastikan mana-mana kaedah "oneshot" hanya boleh beroperasi pada konteks sekali, memastikan tiada pengguna menggunakannya dalam konteks stateful. Saya akan terus merenungnya!
Memphis ialah latihan pembelajaran yang pertama sekali, jadi ini sangat berbaloi!
Selain berkongsi token antara Lexer dan Parser, saya mencipta antara muka untuk menilai kod Python dengan boilerplate yang kurang ketara. Walaupun perkongsian data memperkenalkan kerumitan tambahan, perubahan ini membawa faedah yang jelas: penggunaan memori yang dikurangkan, jaminan keselamatan yang dipertingkatkan melalui pengurusan seumur hidup yang lebih ketat dan API diperkemas yang lebih mudah untuk diselenggara dan dilanjutkan.
Saya memilih untuk mempercayai ini adalah pendekatan yang betul, kebanyakannya untuk mengekalkan harga diri saya. Akhirnya, saya berhasrat untuk menulis kod yang menggambarkan dengan jelas prinsip perisian dan kejuruteraan komputer. Kami kini boleh membuka sumber Memphis, menunjuk kepada pemilik tunggal token dan tidur dengan nyenyak pada waktu malam!
Jika anda ingin mendapatkan lebih banyak siaran seperti ini terus ke peti masuk anda, anda boleh melanggan di sini!
Selain membimbing jurutera perisian, saya juga menulis tentang pengalaman saya mengemudi bekerja sendiri dan autisme yang didiagnosis lewat. Kurang kod dan bilangan jenaka yang sama.
Atas ialah kandungan terperinci Meningkatkan kecekapan ingatan dalam penterjemah yang berfungsi. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!