Mengapa menerapkan lexer sebagai array 2d dan switch raksasa?

24

Saya perlahan-lahan bekerja untuk menyelesaikan gelar saya, dan semester ini adalah Compiler 101. Kami menggunakan Buku Naga . Singkat ke kursus dan kita berbicara tentang analisis leksikal dan bagaimana itu dapat diimplementasikan melalui automata terbatas deterministik (selanjutnya, DFA). Siapkan berbagai status lexer Anda, tentukan transisi di antaranya, dll.

Tetapi baik profesor dan buku mengusulkan menerapkannya melalui tabel transisi yang berjumlah array 2d raksasa (berbagai negara non-terminal sebagai satu dimensi, dan simbol input yang mungkin sebagai yang lain) dan pernyataan saklar untuk menangani semua terminal serta pengiriman ke tabel transisi jika dalam kondisi non-terminal.

Teorinya baik dan bagus, tetapi sebagai seseorang yang sebenarnya menulis kode selama beberapa dekade, implementasinya keji. Itu tidak dapat diuji, tidak dapat dipertahankan, tidak dapat dibaca, dan itu adalah rasa sakit dan setengah untuk debug melalui. Lebih buruk lagi, saya tidak bisa melihat bagaimana praktisnya jika bahasa itu mampu UTF. Memiliki sejuta atau lebih entri tabel transisi per kondisi non-terminal menjadi tidak terburu-buru.

Jadi, apa masalahnya? Mengapa buku definitif tentang subjek mengatakan untuk melakukannya dengan cara ini?

Apakah overhead panggilan fungsi benar-benar sebanyak itu? Apakah ini sesuatu yang berfungsi dengan baik atau diperlukan ketika tata bahasa tidak diketahui sebelumnya (ekspresi reguler?)? Atau mungkin sesuatu yang menangani semua kasus, bahkan jika solusi yang lebih spesifik akan bekerja lebih baik untuk tata bahasa yang lebih spesifik?

( catatan: kemungkinan duplikat " Mengapa menggunakan pendekatan OO alih-alih pernyataan switch raksasa? " sudah dekat, tapi saya tidak peduli dengan OO. Pendekatan fungsional atau bahkan pendekatan imperatif yang lebih waras dengan fungsi mandiri akan baik-baik saja.)

Dan sebagai contoh, pertimbangkan bahasa yang hanya memiliki pengidentifikasi, dan pengidentifikasi itu [a-zA-Z]+. Dalam implementasi DFA, Anda akan mendapatkan sesuatu seperti:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(meskipun sesuatu yang akan menangani akhir file dengan benar)

Dibandingkan dengan apa yang saya harapkan:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

Dengan kode yang di- NextTokenrefactored menjadi fungsinya sendiri setelah Anda memiliki beberapa tujuan dari awal DFA.

Telastyn
sumber
5
sebuah warisan kuno (1977) Prinsip Compiler Desain ? 40 tahun yang lalu, gaya penulisan jauh berbeda
agas
7
Bagaimana Anda menerapkan transisi dari negara-negara DFA? Dan apa ini tentang terminal dan non-terminal, "non-terminal" biasanya mengacu pada aturan produksi dalam tata bahasa, yang akan muncul setelah analisis leksikal.
10
Tabel-tabel itu tidak dimaksudkan untuk dapat dibaca oleh manusia, mereka dimaksudkan untuk dapat digunakan oleh kompiler dan untuk bekerja dengan sangat cepat. Sangat mudah untuk melompat-lompat meja ketika melihat ke depan dalam input (misalnya untuk menangkap rekursi kiri, meskipun dalam praktiknya sebagian besar bahasa dibangun untuk menghindari itu).
5
Jika sebagian dari kekesalan Anda datang dari mengetahui cara melakukan pekerjaan yang lebih baik dan kurang memiliki kemampuan untuk mendapatkan umpan balik atau penghargaan untuk pendekatan yang Anda inginkan - seperti dekade dalam industri tidak melatih kita untuk mengharapkan umpan balik dan kadang-kadang apresiasi - mungkin Anda harus menulis implementasi yang lebih baik dan mempostingnya ke CodeReview.SE untuk mendapatkan sebagian dari itu untuk ketenangan pikiran Anda sendiri.
Jimmy Hoffa
7
Jawaban sederhananya adalah karena lexer biasanya diimplementasikan sebagai mesin keadaan terbatas dan dihasilkan secara otomatis dari tata bahasa - dan tabel keadaan, tidak mengherankan, paling mudah dan kompak direpresentasikan sebagai sebuah tabel. Seperti halnya dengan kode objek, fakta bahwa tidak mudah bagi manusia untuk bekerja dengannya tidak relevan karena manusia tidak bekerja dengannya; mereka mengubah sumber dan menghasilkan contoh baru.
keshlam

Jawaban:

16

Dalam praktiknya, tabel ini dihasilkan dari ekspresi reguler yang menentukan token bahasa:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

Kami memiliki utilitas untuk menghasilkan penganalisa leksikal sejak 1975 ketika lex ditulis.

Anda pada dasarnya menyarankan untuk mengganti ekspresi reguler dengan kode prosedural. Ini memperluas beberapa karakter dalam ekspresi reguler menjadi beberapa baris kode. Kode prosedural tulisan tangan untuk analisis leksikal dari bahasa apa pun yang cukup menarik cenderung tidak efisien dan sulit dipertahankan.

kevin cline
sumber
4
Saya tidak yakin saya menyarankan grosir itu. Ekspresi reguler akan berurusan dengan bahasa yang arbitrer (reguler). Apakah tidak ada pendekatan yang lebih baik ketika bekerja dengan bahasa tertentu? Buku ini menyentuh pendekatan prediksi tetapi kemudian mengabaikannya dalam contoh. Juga, setelah melakukan analisa yang naif untuk C # tahun yang lalu saya tidak merasa sangat sulit untuk mempertahankannya. Tidak efisien? tentu, tetapi tidak terlalu diberikan keterampilan saya pada saat itu.
Telastyn
1
@ Telastyn: hampir mustahil untuk pergi lebih cepat daripada DFA yang digerakkan oleh tabel: dapatkan karakter berikutnya, cari status berikutnya dalam tabel transisi, ubah status. Jika status baru adalah terminal, berikan token. Di C # atau Java pendekatan apa pun yang melibatkan pembuatan string sementara akan lebih lambat.
kevin cline
@ kevincline - tentu saja, tetapi dalam contoh saya tidak ada string sementara. Bahkan di C itu hanya akan menjadi indeks atau pointer melangkah melalui string.
Telastyn
6
@JimmyHoffa: ya, kinerja pasti relevan di kompiler. Kompiler cepat karena mereka telah dioptimalkan ke neraka dan kembali. Bukan optimasi mikro, mereka hanya tidak melakukan pekerjaan yang tidak perlu seperti membuat dan membuang objek sementara yang tidak dibutuhkan. Dalam pengalaman saya sebagian besar kode pemrosesan teks komersial melakukan sepersepuluh karya kompiler modern dan membutuhkan waktu sepuluh kali lebih lama untuk melakukannya. Performa sangat besar ketika Anda memproses satu gigabyte teks.
kevin cline
1
@ Telastyn, "pendekatan yang lebih baik" apa yang ada dalam pikiran Anda, dan dengan cara apa Anda mengharapkannya menjadi "lebih baik"? Mengingat bahwa kami sudah memiliki alat lexing yang telah teruji dengan baik, dan mereka menghasilkan parser yang sangat cepat (seperti yang orang lain katakan, DFA yang digerakkan oleh tabel sangat cepat), masuk akal untuk menggunakannya. Mengapa kita ingin menciptakan pendekatan khusus baru untuk bahasa tertentu, padahal kita hanya bisa menulis tata bahasa lex? Tata bahasa lex lebih dapat dipertahankan, dan parser yang dihasilkan lebih cenderung benar (mengingat seberapa baik lex diuji dan alat serupa).
DW
7

Motivasi untuk algoritma tertentu sebagian besar adalah latihan pembelajaran, jadi ia berusaha untuk tetap dekat dengan ide DFA, dan menjaga status dan transisi sangat eksplisit dalam kode. Sebagai aturan, tidak ada yang benar-benar akan secara manual menulis kode ini - Anda akan menggunakan alat untuk menghasilkan kode dari tata bahasa. Dan alat itu tidak akan peduli tentang keterbacaan kode karena itu bukan kode sumber, ini merupakan output berdasarkan definisi tata bahasa.

Kode Anda lebih bersih untuk seseorang yang mempertahankan DFA yang ditulis tangan, tetapi sedikit lebih jauh dihapus dari konsep yang diajarkan.

psr
sumber
7

Lingkaran dalam:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

memiliki banyak keunggulan kinerja. Tidak ada cabang di itu sama sekali, karena Anda melakukan hal yang persis sama untuk setiap karakter input. Kinerja kompiler dapat terjaga keamanannya oleh lexer (yang harus beroperasi pada skala setiap karakter input). Ini bahkan lebih benar ketika Buku Naga ditulis.

Dalam praktiknya, selain siswa CS yang mempelajari lexer, tidak ada yang harus mengimplementasikan (atau men-debug) loop dalam itu karena itu adalah bagian dari pelat boiler yang dilengkapi dengan alat yang membangun transitiontabel.

Ben Jackson
sumber
5

Dari memori, - sudah lama sejak saya membaca buku, dan saya cukup yakin saya tidak membaca edisi terbaru, saya pasti tidak ingat sesuatu yang tampak seperti Jawa - bagian itu ditulis dengan kode yang dimaksudkan sebagai templat, tabel diisi dengan lex like lexer generator. Masih dari memori, ada bagian pada kompresi tabel (sekali lagi dari memori, itu ditulis sedemikian rupa sehingga juga berlaku untuk parser yang digerakkan oleh tabel, sehingga mungkin lebih jauh dalam buku daripada apa yang Anda lihat). Demikian pula, buku yang saya ingat diasumsikan set karakter 8-bit, saya berharap bagian tentang penanganan set karakter yang lebih besar di edisi kemudian, mungkin sebagai bagian dari kompresi tabel. Saya telah memberikan cara alternatif untuk mengatasinya sebagai jawaban atas pertanyaan SO.

Ada keuntungan kinerja yang pasti dalam memiliki data loop ketat yang didorong dalam arsitektur modern: ini cukup cache friendly (jika Anda telah mengompresi tabel), dan prediksi lompat sesempurna mungkin (satu kehilangan di akhir lexem, mungkin satu ketinggalan untuk saklar pengiriman ke kode yang bergantung pada simbol; itu dengan asumsi bahwa dekompresi tabel Anda dapat dilakukan dengan lompatan yang dapat diprediksi). Memindahkan mesin status itu ke kode murni akan mengurangi kinerja prediksi lompatan dan mungkin meningkatkan tekanan cache.

Pemrogram
sumber
2

Setelah bekerja melalui Buku Naga sebelumnya, alasan utama untuk memiliki tuas dan parser yang digerakkan oleh tabel adalah agar Anda dapat menggunakan ekspresi reguler untuk menghasilkan lexer dan BNF untuk menghasilkan parser. Buku ini juga membahas bagaimana alat seperti lex dan yacc bekerja, dan agar Anda tahu cara alat ini bekerja. Selain itu, penting bagi Anda untuk bekerja melalui beberapa contoh praktis.

Meskipun banyak komentar, itu tidak ada hubungannya dengan gaya kode yang ditulis pada 40-an, 50-an, 60-an ..., itu ada hubungannya dengan mendapatkan pemahaman praktis tentang apa yang dilakukan alat untuk Anda dan apa yang Anda miliki lakukan untuk membuatnya bekerja. Ini semua berkaitan dengan pemahaman mendasar bagaimana kompiler bekerja baik dari sudut pandang teoritis maupun praktis.

Mudah-mudahan, instruktur Anda juga akan membiarkan Anda menggunakan lex dan yacc (kecuali itu adalah kelas tingkat pascasarjana dan Anda bisa menulis lex dan yacc).

Robert Baron
sumber
0

Terlambat ke pesta :-) Token dicocokkan dengan ekspresi reguler. Karena ada banyak dari mereka, Anda memiliki mesin multi regex, yang pada gilirannya adalah DFA raksasa.

"Lebih buruk lagi, aku tidak bisa melihat bagaimana praktisnya jika bahasa itu mampu UTF."

Itu tidak relevan (atau transparan). Selain UTF memiliki properti bagus, entitasnya tidak tumpang tindih bahkan sebagian. Misalnya byte yang mewakili karakter "A" (dari tabel ASCII-7) tidak digunakan lagi untuk karakter UTF lainnya.

Jadi, Anda memiliki DFA tunggal (yang merupakan multi-regex) untuk seluruh lexer. Bagaimana lebih baik untuk menuliskannya daripada array 2d?

Greenoldman
sumber