Saya akan mencoba memasukkannya ke dalam istilah awam.
Jika Anda berpikir tentang pohon parse (bukan AST, tetapi kunjungan parser dan perluasan input), rekursi kiri menghasilkan pohon yang tumbuh ke kiri dan ke bawah. Rekursi yang tepat justru sebaliknya.
Sebagai contoh, tata bahasa umum dalam kompiler adalah daftar item. Mari kita ambil daftar string ("merah", "hijau", "biru") dan menguraikannya. Saya bisa menulis tata bahasa beberapa cara. Contoh-contoh berikut adalah rekursif langsung kiri atau kanan, masing-masing:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Pohon-pohon untuk parse ini:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Perhatikan bagaimana ia tumbuh ke arah rekursi.
Ini sebenarnya bukan masalah, tidak apa-apa jika ingin menulis tata bahasa rekursif kiri ... jika alat parser Anda dapat mengatasinya. Parser bottom up menanganinya dengan baik. Begitu juga parser LL yang lebih modern. Masalah dengan tata bahasa rekursif bukanlah rekursi, itu adalah rekursi tanpa memajukan pengurai, atau, berulang tanpa memakan token. Jika kita selalu mengonsumsi setidaknya 1 token saat kita kambuh, kita akhirnya mencapai akhir penguraian. Rekursi kiri didefinisikan sebagai berulang tanpa mengkonsumsi, yang merupakan loop tak terbatas.
Batasan ini adalah murni implementasi detail penerapan tata bahasa dengan parser LL top-down naif (parser keturunan rekursif). Jika Anda ingin tetap menggunakan tata bahasa rekursif kiri, Anda dapat mengatasinya dengan menulis ulang produksi untuk mengkonsumsi setidaknya 1 token sebelum berulang, jadi ini memastikan kami tidak pernah terjebak dalam lingkaran non-produktif. Untuk aturan tata bahasa yang kiri-rekursif, kita dapat menulis ulang dengan menambahkan aturan menengah yang meratakan tata bahasa hanya satu tingkat lookahead, menggunakan token di antara produksi rekursif. (CATATAN: Saya tidak mengatakan ini adalah satu-satunya cara atau cara yang disukai untuk menulis ulang tata bahasa, hanya menunjukkan aturan umum. Dalam contoh sederhana ini, opsi terbaik adalah menggunakan bentuk rekursif yang tepat). Karena pendekatan ini digeneralisasi, generator parser dapat mengimplementasikannya tanpa melibatkan programmer (secara teoritis). Dalam praktiknya, saya percaya ANTLR 4 sekarang melakukan hal itu.
Untuk tata bahasa di atas, implementasi LL menampilkan rekursi kiri akan terlihat seperti ini. Parser akan mulai dengan memprediksi daftar ...
bool match_list()
{
if(lookahead-predicts-something-besides-comma) {
match_STRING();
} else if(lookahead-is-comma) {
match_list(); // left-recursion, infinite loop/stack overflow
match(',');
match_STRING();
} else {
throw new ParseException();
}
}
Pada kenyataannya, apa yang sebenarnya kita hadapi adalah "implementasi naif", yaitu. kami awalnya memprediksikan kalimat yang diberikan, kemudian secara rekursif memanggil fungsi untuk prediksi itu, dan fungsi itu secara naif memanggil prediksi yang sama lagi.
Parser bottom-up tidak memiliki masalah aturan rekursif di kedua arah, karena mereka tidak mem-reparsing awal kalimat, mereka bekerja dengan menyatukan kalimat kembali.
Rekursi dalam tata bahasa hanya masalah jika kita menghasilkan dari atas ke bawah, yaitu. parser kami bekerja dengan "memperluas" prediksi kami saat kami mengkonsumsi token. Jika alih-alih berkembang, kami runtuh (produksi "berkurang"), seperti pada pengurai bottom-up LALR (Yacc / Bison), maka rekursi dari kedua sisi bukanlah masalah.
::=
dariExpression
menjadiTerm
, dan jika Anda melakukan hal yang sama setelah yang pertama||
, itu tidak lagi akan menjadi rekursif kiri? Tetapi jika Anda hanya melakukannya setelah itu::=
, tetapi tidak||
, itu masih akan tetap rekursif?