Kami menulis penerjemah sederhana dalam Lisp - III

Artikel sebelumnya

Kesalahan, Kesalahan, Kesalahan ...


Program yang baik harus dilindungi dari kesalahan pengguna. Ini sangat pasti. Kesalahan perlu ditangani, dan bahkan lebih baik diperingatkan (pencegahan selalu lebih baik daripada mengobati!). Aerobatik - jadi buat dialog dengan pengguna sehingga yang terakhir tidak bisa membuat kesalahan.

Misalnya, jika pengguna perlu memasukkan bilangan bulat positif di bidang input, maka, tentu saja, Anda dapat menganalisis jawabannya, dan jika Anda menemukan karakter non-numerik, beri peringatan dan minta pengguna untuk mengulangi input. Tetapi jauh lebih baik untuk hanya melarang karakter non-numerik!

Sayangnya, teknik seperti itu tidak selalu bisa diterapkan. Secara khusus, variasi desain yang masuk ke input penerjemah terlalu besar untuk hanya “memotong yang salah” dengan mengatur masker input.

Seseorang memiliki hak istimewa untuk membuat kesalahan, dan penerjemah harus, dalam hal memasukkan konstruksi bahasa yang salah, memberikan diagnosis yang jelas dan, jika mungkin, terus menganalisis teks sumber untuk mengidentifikasi semua kesalahan. Pengguna mungkin tidak akan benar-benar menyukainya jika penerjemah menangkap kesalahan "satu per satu". Dan benar-benar tidak dapat diterima untuk mengenali situasi di mana program "macet" dengan pesan kesalahan sistem.
Pada artikel ini, kita akan membahas kode yang sebelumnya dikembangkan secara kritis, dan mencoba untuk mencegah (memproses) kemungkinan kesalahan.

Mari kita mulai dengan fungsi start pertama. Apa yang dia lakukan Dia mengambil nama file input, membukanya, dan memproses baris demi baris. Untuk program semacam itu, skenario interaksi pengguna telah "tenang" - ini dapat dianggap kanonik:

  • Jika nama file tidak ditentukan, panggil dialog "Open" standar;
  • Jika pengguna mengklik tombol "menolak" di dialog "Buka" - matikan;
  • Periksa apakah file dengan nama yang diberikan / dimasukkan ada. Jika tidak ada, keluarkan pesan dan keluar;
  • Jika file yang ditentukan ada, proseskan.

Versi kami dari prosedur mulai tidak memenuhi skenario ini. Bahkan, lihat kode di bawah ini:

(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …) ;;      (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (let ((fi (gensym 'fi))) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (return t)) (when (filEOF fi) (return t)) (eval curr-proc))) (filClose fi)) (when *flagerr* (printsline "****   "))) 

Respons negatif pengguna tidak dianalisis, jadi jika tombol "tolak" ditekan, program akan "macet". Keberadaan file juga tidak dianalisis. Sayangnya, cacat ini tidak terbatas pada kekurangan. Jelas, jika prosedur mini-basic adalah yang terakhir dalam file input, maka analisis akhir file akan menyebabkan siklus untuk istirahat sebelum fungsi yang dihasilkan dimuat ke lingkungan Lisp.

Perbaiki kekurangan ini:

 (defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* … ) (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (if (and fname (filExistp fname)) (let ((fi (gensym 'fi))) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (return t)) (when curr-proc (eval curr-proc)) (when (filEOF fi) (return t)))) (filClose fi) (when *flagerr* (printsline "****   "))) (printsline (if fname (strCat "****  " fname "  ") "****   "))) (unset '*numline*) (unset '*flagerr*) (unset '*oplist*)) 

Jika nama file ditentukan dan file ada, pemrosesan dilakukan. Jika tidak, salah satu pesan dicetak: "File tidak ada" atau "Nama file dihilangkan".
Tindakan berikut dilakukan secara berurutan di tubuh loop utama:

  • Fungsi action-proc terpenuhi. Hasil kerjanya disimpan dalam variabel lokal proc-proc;
  • Jika flag * flagerr * dinaikkan, loop akan putus;
  • Jika fungsi action-proc mengembalikan hasil yang tidak kosong, fungsi yang dihasilkan dimuat ke lingkungan Lisp;
  • Jika akhir file tercapai, loop juga terputus.

Kode tampaknya lebih baik ... Tapi satu kesalahan serius lainnya tetap tidak terselesaikan - setelah proses prosedur yang mengandung satu atau lebih kesalahan selesai, loop utama akan rusak dan program akan berakhir tanpa melihat bagian tahun asli yang terletak di belakang prosedur dengan kesalahan. Ini buruk - saya ingin penerjemah menghasilkan semua kesalahan yang dapat dideteksi pada setiap awal.

Untuk memperbaiki cacat ini, mari kita perkenalkan variabel global "penghitung kesalahan", saat memproses prosedur dengan kesalahan, kami akan menambah penghitung ini. Dan flag kesalahan akan diatur ulang setelah memproses setiap prosedur:

 (defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *errcount* 0) (setq *oplist* …) (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (if (and fname (filExistp fname)) (let ((fi (gensym 'fi))) (filCloseAll) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (setq *errcount* (add1 *errcount*))) (when (and curr-proc (not *flagerr*)) (eval curr-proc)) (setq *flagerr* nil) (when (filEOF fi) (return t)))) (filClose fi) (when (> *errcount* 0) (printsline "****   "))) (printsline (if fname (strCat "****  " fname "  ") "****   "))) (unset '*numline*) (unset '*flagerr*) (unset '*oplist*) (unset '*errcount*)) 

Sekarang, fungsi start akan bekerja dengan baik. Mari kita pastikan ini. Buat file sumber berikut:

 * *    * proc test1(x) local y y=x^2 bla-bla end_proc * *    * proc test2() local x,y input x y=test1(x) print y end_proc * *    * proc test3(x) bla-bla-bla print x end_proc 

Dan "biarkan saja" melalui penerjemah kami. Kami mendapatkan:

 0001 * 0002 *    0003 * 0004 proc test1(x) 0005 local y 0006 y=x^2 0007 bla-bla ****  (BLA - BLA)   0008 end_proc 0009 * 0010 *    0011 * 0012 proc test2() 0013 local x,y 0014 input x 0015 y=test1(x) 0016 print y 0017 end_proc 0018 * 0019 *    0020 * 0021 proc test3(x) 0022 bla-bla-bla ****  (BLA - BLA - BLA)   0023 print x 0024 end_proc 0025 ****    

Kami berasumsi bahwa kami berhasil dengan fungsi start. Tapi "kerjakan bug" baru saja dimulai. Mari kita lihat sintaks bagian bahasa yang telah kita terapkan.

Mungkin kesalahan sintaksis paling umum yang paling sering dilakukan orang adalah struktur braket yang salah (tidak seimbang atau dalam tanda kurung urutan yang salah). Ingat apa yang terjadi pada satu baris kode sumber untuk program mini-basic setelah dibaca. String diurai (dipecah menjadi token), dan kemudian daftar token diterjemahkan ke dalam bentuk daftar internal. Dalam daftar token, tanda kurung adalah token yang terpisah dan kami tidak memeriksa saldo mereka. Ini bisa dilakukan sebagai fungsi terpisah, tetapi daftar token ditransmisikan ke input dari fungsi input, yang menerjemahkan daftar garis ke dalam daftar Lisp. Jika ekspresi string yang salah diteruskan ke input fungsi input, fungsi tersebut akan mengembalikan kesalahan.

Mari kita atasi kesalahan ini.

Di HomeLisp, konstruk digunakan untuk menangani kesalahan (coba Ekspresi-1 kecuali Ekspresi-1). Ia bekerja sebagai berikut:

  • Upaya dilakukan untuk menghitung Ekspresi-1. Jika upaya ini berhasil, hasil perhitungan dikembalikan sebagai hasil dari seluruh formulir coba;
  • Jika kesalahan terjadi, maka Ekspresi-2 dihitung. Pada saat yang sama, fungsi sistem tanpa parameter (errormessage) tersedia, yang mengembalikan teks pesan kesalahan.

Berdasarkan hal tersebut di atas, transfer ke formulir daftar dapat dikeluarkan sebagai berikut:

 (defun mk-intf (txt) (let ((lex (parser txt " ," "()+-*/\^=<>%")) (intf "")) (iter (for a in lex) (setq intf (strCat intf a " "))) (try (input (strCat "(" intf ")")) except (progn (printsline (strCat "**** " (errormessage))) `(,txt) )))) 

Jika terjadi kesalahan konversi, pesan sistem akan dikeluarkan, dan sebagai hasilnya, daftar satu elemen akan dikembalikan - baris kode asli. Selanjutnya, daftar ini akan jatuh (sebagai pernyataan berikutnya) ke dalam prosedur tindakan-proc. Dan, tentu saja, itu tidak akan dikenali. Ini akan menghasilkan pesan kesalahan lain, dan kompiler akan terus bekerja. Kami akan menyiapkan kode sumber berikut, dan mencoba menerjemahkannya:

 * *    * proc test1(x) local y y=(x^2)) end_proc * *    * proc test2() local x,y input x y=test1(x) print y end_proc * *    * proc test3(x) x=3+)x^2 print x end_proc 

Kami mendapatkan hasil yang diharapkan:

 0001 * 0002 *    0003 * 0004 proc test1(x) 0005 local y 0006 y=(x^2)) ****        ****  ("y=(x^2))")   0007 end_proc 0008 * 0009 *    0010 * 0011 proc test2() 0012 local x,y 0013 input x 0014 y=test1(x) 0015 print y 0016 end_proc 0017 * 0018 *    0019 * 0020 proc test3(x) 0021 x=3+)x^2 ****        ****  ("x=3+)x^2")   0022 print x 0023 end_proc ****    

Sekarang mari kita lihat kode yang mengubah ekspresi aritmatika menjadi notasi awalan. Kode ini tidak mengandung cara untuk memperbaiki kesalahan pengguna. Sayangnya, kesalahan ini bisa sangat banyak. Mari kita perbaiki kesalahan ini. Untuk memulai, mari kita coba menerjemahkan kode yang sama sekali tidak bersalah (dalam tampilan):

 proc test() local x,y x=6 y=-x print y end_proc 

Siaran akan berakhir dengan "kejatuhan" penerjemah! Penurunan akan menyebabkan operator y = -x. Ada apa? Dalam minus unary! Mengubah rumus dari bentuk infiks ke awalan, kami entah bagaimana tidak berpikir minus “bermuka dua” - ada minus biner (tanda operasi), dan ada minus unary (tanda angka). Pengurai kami tidak mengetahui perbedaan ini - menganggap semua kontra sebagai biner ... Apa yang harus dilakukan sekarang? Agar tidak menghancurkan kode yang sudah berfungsi, mari kita ubah semua kontra unary menjadi kode biner. Bagaimana? Tapi sangat sederhana. Sangat jelas bahwa unary minus "hidup" hanya dalam konstruksi seperti:

"(-Sesuatu"
“> -Sesuatu”
"<-Beberapa"
“= Sesuatu”
Nah, di awal formula, dia juga bisa bertemu. Karena itu, jika, sebelum membobol token, kami melakukan penggantian berikut:

“(-Sesuatu” => “(0-sesuatu”
“> -Sesuatu” => “> 0-sesuatu”
“<-Sesuatu” => “<0-something”
“= Sesuatu” => “= 0 sesuatu”

dan jika rumus dimulai dengan minus, kami menetapkan nol pada awal rumus, maka semua minus akan menjadi biner dan kesalahan akan dihilangkan secara radikal. Mari kita panggil fungsi yang akan melakukan konversi di atas nama prepro. Begini tampilannya:

 (defun prepro (s) (let* ((s0 (if (eq "-" (strLeft s 1)) (strCat "0" s) s)) (s1 (strRep s0 "(-" "(0-")) (s2 (strRep s1 "=-" "=0-")) (s3 (strRep s2 ">-" ">0-")) (s4 (strRep s3 "<-" "<0-"))) s4)) 

Tidak diperlukan komentar khusus di sini. Tetapi pengurai sederhana kami memiliki masalah lain yang tidak begitu jelas pada pandangan pertama - dua tanda operasi. Ketika bekerja dengan rumus, tanda-tanda ">" dan "=" berdiri berdampingan berarti satu operasi "> =" (dan harus merupakan salah satu token!). Parser tidak ingin tahu ini - itu akan membuat masing-masing tanda sebagai token terpisah. Anda dapat mengatasi masalah ini dengan melihat daftar token yang diterima, dan jika karakter yang sesuai bersebelahan, dengan menggabungkan. Kami memberi nama fungsi yang akan melakukan penyatuan dengan nama "postpro". Berikut adalah kode untuk implementasi yang mungkin:

 (defun postpro (lex-list) (cond ((null (cdr lex-list)) lex-list) (t (let ((c1 (car lex-list)) (c2 (cadr lex-list))) (cond ((and (eq c1 ">") (eq c2 "=")) (cons ">=" (postpro (cddr lex-list)))) ((and (eq c1 "<") (eq c2 "=")) (cons "<=" (postpro (cddr lex-list)))) ((and (eq c1 "=") (eq c2 "=")) (cons "==" (postpro (cddr lex-list)))) ((and (eq c1 "<") (eq c2 ">")) (cons "<>" (postpro (cddr lex-list)))) ((and (eq c1 ">") (eq c2 "<")) (cons "<>" (postpro (cddr lex-list)))) ((and (eq c1 "!") (eq c2 "=")) (cons "/=" (postpro (cddr lex-list)))) ((and (eq c1 "/") (eq c2 "=")) (cons "/=" (postpro (cddr lex-list)))) (t (cons c1 (postpro (cdr lex-list))))))))) 

Juga, seperti yang kita lihat, tidak ada yang istimewa. Tetapi sekarang fungsi final menerjemahkan operator ke bentuk daftar internal akan terlihat seperti ini:

 (defun mk-intf (txt) (let ((lex (postpro (parser (prepro txt) " ," "()+-*/\^=<>%"))) (intf "")) (iter (for a in lex) (setq intf (strCat intf a " "))) (try (input (strCat "(" intf ")")) except (progn (printsline (strCat "**** " (errormessage))) `(,txt) )))) 

Sekarang mari kita lihat fungsi inf2ipn. Apa kesalahan pengguna yang bisa "menyalahkan" itu? Kami telah memotong ketidakseimbangan kurung di atas. Apa yang bisa lebih? Dua tanda operasi atau dua operan, berdiri berjajar. Orang dapat menganalisis ini dalam kode inf2ipn (dan mereka yang ingin dapat melakukan ini sendiri). Tapi kami “menangkap” kesalahan-kesalahan ini pada tahap mengubah rumus dari SCR ke awalan. Dan mari (untuk berjaga-jaga) kita akan menangkap semua kesalahan yang mungkin timbul dalam proses mengubah rumus dari infix ke awalan. Tempat terbaik untuk ini adalah fungsi pembungkus i2p. Sekarang mungkin terlihat seperti ini:

 (defun i2p (f) (try (ipn2pref (inf2ipn f)) except (progn (printsline "****    ") (printsline (strCat "**** " (errormessage))) (setq *flagerr* t) nil))) 

Dan sekarang kita akan mencegah tampilan dalam formula dua tanda operasi atau dua operan berturut-turut. Artikel sebelumnya menjelaskan algoritma untuk menerjemahkan formula dari SCR ke bentuk awalan. Tanda penyelesaian yang benar dari algoritma ini adalah bahwa pada langkah terakhir tumpukan harus mengandung nilai tunggal. Jika tidak demikian, maka kesalahan telah dibuat. Dan situasi salah lainnya muncul dalam kasus ketika fungsi dipanggil dengan jumlah parameter yang salah (kurang lebih). Situasi-situasi ini harus "ditangkap":

 (defun ipn2pref (f &optional (s nil)) (cond ((null f) (if (null (cdr s)) (car s) (progn (printsline "****    ") (setq *flagerr* t) nil))) ((numberp (car f)) (ipn2pref (cdr f) (cons (car f) s))) ((is-op (car f)) (let ((ar (arity (car f)))) (if (< (length s) ar) (progn (setq *flagerr* t) (printsline "****    ") nil) (ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar)))))) ((atom (car f)) (ipn2pref (cdr f) (cons (car f) s))) (t (ipn2pref (cdr f) (cons (list (car f) (car s)) (cdr s)))))) 

Sekarang mari kita "melihat kritis" pada penangan pernyataan proc. Kami jelas kehilangan dua poin. Hal pertama yang harus dilakukan adalah jangan lupa saat memproses prosedur untuk menghitung aritynya (jumlah argumen) dan memodifikasi variabel global * oplist * yang sesuai. Dan yang kedua adalah bahwa fungsi yang kita hasilkan tidak mengembalikan nilai yang benar! Lebih tepatnya, sebagai hasil dari fungsi yang dihasilkan oleh penerjemah kami, nilai formulir terakhir yang dihitung sebelum kembali akan dikembalikan. Untuk menjamin pengembalian nilai yang diinginkan, saya mengusulkan untuk mentransfer variabel hasil dari Pascal. Sekarang, jika perlu, kembalikan nilai yang diinginkan, cukup bagi pengguna untuk memberikan nilai yang diinginkan ke variabel ini sebelum keluar dari fungsi, dan ketika menghasilkan isi fungsi, kita perlu memasukkan hasil nama di badan fungsi dengan ekspresi terakhir. Semua ini membawa fungsi action-proc ke:

 (defun action-proc (fi) (let ((stmt nil) (proc-name nil) (proc-parm nil) (loc-var nil) (lv '((result 0))) (body nil)) (loop (setq stmt (mk-intf (getLine fi))) (when (null stmt) (return t)) (cond ((eq (car stmt) 'proc) (setq proc-name (nth 1 stmt)) (setq proc-parm (nth 2 stmt)) (setq *oplist* (cons (list proc-name (length proc-parm)) *oplist*))) ((eq (car stmt) 'end_proc) (return t)) ((eq (car stmt) 'print) (setq body (append body (list (cons 'printline (cdr stmt)))))) ((eq (car stmt) 'input) (setq body (append body (list (list 'setq (cadr stmt) (list 'read) ))))) ((eq (car stmt) 'local) (setq loc-var (append loc-var (cdr stmt)))) ((eq (cadr stmt) '=) (setq body (append body (list (action-set stmt))))) (t (printsline (strCat "****  " (output stmt) "  ")) (setq *flagerr* t)))) (iter (for a in (setof loc-var)) (collecting (list a 0) into lv)) (if proc-name `(defun ,proc-name ,proc-parm (let ,lv ,@body result)) nil))) 

Kami akan berhenti di sini untuk saat ini (meskipun kami masih akan menghadapi masalah, dan kode harus diselesaikan; tetapi begitu banyak programmer ...) Dan sekarang kami akan mempertimbangkan dua peningkatan pada bahasa kami yang sesuai untuk dibuat sekarang.

Perbaikan kecil ...


Dalam artikel sebelumnya, saya menulis bahwa itu tidak nyaman untuk seorang programmer jika dalam bahasa satu operator menempati tepat satu baris. Hal ini diperlukan untuk memberikan kemampuan untuk menulis pernyataan besar pada banyak baris. Mari kita terapkan ini. Ini sama sekali tidak sulit untuk dilakukan. Dalam prosedur getLine, kita akan membuat variabel lokal di mana kita akan mengakumulasi teks baca (asalkan ini bukan komentar dan diakhiri dengan beberapa karakter "_". Segera setelah garis signifikan dengan akhiran yang berbeda diperbaiki, kami mengembalikan nilai terakumulasi sebagai nilai. Berikut adalah kode:

 (defun getLine (fil) (let ((stri "") (res "")) (loop (when (filEof fil) (return "")) (setq *numline* (add1 *numline*)) (setq stri (filGetline fil)) (printsline (strCat (format *numline* "0000") " " (strRTrim stri))) (unless (or (eq "" stri) (eq "*" (strLeft stri 1))) (setq stri (strATrim stri)) (if (eq " _"(strRight stri 2)) (setq res (strCat res (strLeft stri (- (strLen stri) 2)))) (setq res (strCat res stri))) (unless (eq " _"(strRight stri 2)) (return res)))))) 

Dan peningkatan terakhir. Dalam banyak bahasa pemrograman, Anda dapat menggunakan operan logis dalam ekspresi aritmatika (yang dalam hal ini dihitung nol atau satu). Ini memberi bahasa tambahan ekspresi dan, omong-omong, cukup konsisten dengan semangat dasar. Dalam mini-BASIC kami, upaya untuk menghitung ungkapan ini, misalnya, adalah:

 z=(x>y)*5+(x<=y)*10 

akan menyebabkan kesalahan runtime. Dan ini bisa dimengerti: dalam Lisp, ekspresi (> xy) dihitung untuk Nil atau T. Tapi Nil / T tidak dapat dikalikan dengan 5 ... Namun, masalah ini mudah untuk membantu. Mari kita menulis beberapa makro sederhana yang menggantikan hasil ekspresi perbandingan dengan 0/1 (bukan Nil / T):

 (defmacro $= (xy) `(if (= ,x ,y) 1 0)) (defmacro $== (xy) `(if (= ,x ,y) 1 0)) (defmacro $> (xy) `(if (> ,x ,y) 1 0)) (defmacro $< (xy) `(if (< ,x ,y) 1 0)) (defmacro $/= (xy) `(if (/= ,x ,y) 1 0)) (defmacro $<> (xy) `(if (/= ,x ,y) 1 0)) (defmacro $<= (xy) `(if (<= ,x ,y) 1 0)) (defmacro $>= (xy) `(if (>= ,x ,y) 1 0)) 

Sekarang, lihat garis di fungsi ipn2pref yang melakukan pemrosesan operasi. Inilah barisnya:

 (ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar))) 

Di sini (mobil f) adalah nama operasi. Mari kita menulis fungsi kecil untuk mengganti kode perbandingan:

 (defun chng-comp (op) (if (member op '(= == /= <> > < >= <=)) (implode (cons '$ (explode op))) op)) 

Fungsi memeriksa apakah argumennya adalah operasi perbandingan, dan, jika perlu, menambahkan karakter "$" ke awal. Sekarang panggil di tempat yang tepat dari fungsi ipn2pref:

 (ipn2pref (cdr f) (cons (cons (chng-comp (car f)) (reverse (subseq s 0 ar))) (subseq s ar))) 

Apa hasilnya? Operasi perbandingan akan diganti oleh panggilan ke makro yang sesuai, dan semua operasi lainnya tidak akan berubah. Jika Anda menerjemahkan fungsi ini:

 proc test() local x,y x=1 y=2 result=(x>y)*5+(x<=y)*10 end_proc 

dan kemudian menyebutnya, kami mendapatkan hasil yang diharapkan.

Itu saja untuk hari ini.

Kode untuk artikel ini ada di sini.
Untuk dilanjutkan.

Source: https://habr.com/ru/post/id423663/


All Articles