Pemrogram Lisp membanggakan bahwa Lisp adalah bahasa yang kuat yang dapat dibangun dari sekumpulan kecil operasi primitif . Mari kita mempraktekkan gagasan itu dengan bermain golf juru bahasa untuk dialek yang disebut tinylisp
.
Spesifikasi bahasa
Dalam spesifikasi ini, kondisi apa pun yang hasilnya digambarkan sebagai "tidak terdefinisi" dapat melakukan apa saja pada penerjemah Anda: crash, gagal diam-diam, menghasilkan gobbldegook acak, atau bekerja seperti yang diharapkan. Implementasi referensi di Python 3 tersedia di sini .
Sintaksis
Token dalam tinylisp adalah (
,, )
atau string apa pun dari satu atau lebih karakter ASCII yang dapat dicetak kecuali tanda kurung atau spasi. (Yaitu regex berikut:. [()]|[^() ]+
) Setiap token yang seluruhnya terdiri dari digit adalah bilangan bulat integer. (Memimpin nol baik-baik saja.) Setiap token yang mengandung non-digit adalah simbol, bahkan contoh numerik yang tampak seperti 123abc
, 3.14
, dan -10
. Semua spasi putih (termasuk, setidaknya, karakter ASCII 32 dan 10) diabaikan, kecuali sejauh itu memisahkan token.
Program tinylisp terdiri dari serangkaian ekspresi. Setiap ekspresi adalah bilangan bulat, simbol, atau ekspresi-s (daftar). Daftar terdiri dari nol atau lebih ekspresi yang dibungkus dengan tanda kurung. Tidak ada pemisah yang digunakan di antara item. Berikut adalah contoh ungkapan:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Ekspresi yang tidak terbentuk dengan baik (khususnya, yang memiliki tanda kurung yang tidak cocok) memberikan perilaku yang tidak terdefinisi. (Implementasi referensi menutup secara otomatis parens terbuka dan berhenti mengurai pada parens dekat yang tak tertandingi.)
Tipe data
Tipe data tinylisp adalah bilangan bulat, simbol, dan daftar. Fungsi bawaan dan makro juga dapat dianggap tipe, meskipun format outputnya tidak ditentukan. Daftar dapat berisi sejumlah nilai dari jenis apa pun dan dapat disarangkan secara mendalam. Integer harus didukung setidaknya dari -2 ^ 31 hingga 2 ^ 31-1.
Daftar kosong ()
- juga disebut sebagai nil - dan integer 0
adalah satu-satunya nilai yang dianggap salah secara logis; semua bilangan bulat lainnya, daftar kosong, bawaan, dan semua simbol secara logis benar.
Evaluasi
Ekspresi dalam suatu program dievaluasi secara berurutan dan hasil masing-masing dikirim ke stdout (lebih lanjut tentang pemformatan output nanti).
- Integer literal mengevaluasi dirinya sendiri.
- Daftar kosong
()
mengevaluasi dirinya sendiri. - Daftar satu atau lebih item mengevaluasi item pertama dan memperlakukannya sebagai fungsi atau makro, menyebutnya dengan item yang tersisa sebagai argumen. Jika item tersebut bukan fungsi / makro, perilaku tidak terdefinisi.
- Simbol mengevaluasi sebagai nama, memberikan nilai yang terikat pada nama itu dalam fungsi saat ini. Jika nama tidak didefinisikan dalam fungsi saat ini, ia mengevaluasi nilai yang terikat padanya pada lingkup global. Jika nama tidak didefinisikan pada lingkup saat ini atau global, hasilnya tidak terdefinisi (implementasi referensi memberikan pesan kesalahan dan mengembalikan nihil).
Fungsi dan makro bawaan
Ada tujuh fungsi bawaan di tinylisp. Suatu fungsi mengevaluasi setiap argumennya sebelum menerapkan beberapa operasi padanya dan mengembalikan hasilnya.
c
- kontra [daftar saluran]. Membawa dua argumen, nilai dan daftar, dan mengembalikan daftar baru yang diperoleh dengan menambahkan nilai di bagian depan daftar.h
- head ( mobil , dalam terminologi Lisp). Mengambil daftar dan mengembalikan item pertama di dalamnya, atau nihil jika diberikan nihil.t
- tail ( cdr , dalam terminologi Lisp). Mengambil daftar dan mengembalikan daftar baru yang berisi semua kecuali item pertama, atau nihil jika diberikan nihil.s
- kurangi. Membawa dua bilangan bulat dan mengembalikan yang pertama minus yang kedua.l
- kurang dari. Membawa dua bilangan bulat; mengembalikan 1 jika yang pertama kurang dari yang kedua, 0 sebaliknya.e
- sama. Mengambil dua nilai dari jenis yang sama (bilangan bulat, daftar, atau simbol); mengembalikan 1 jika keduanya sama (atau identik di setiap elemen), 0 sebaliknya. Pengujian bawaan untuk kesetaraan tidak terdefinisi (implementasi referensi berfungsi seperti yang diharapkan).v
- eval. Mengambil satu daftar, integer, atau simbol, mewakili ekspresi, dan mengevaluasinya. Misalnya melakukan(v (q (c a b)))
sama dengan melakukan(c a b)
;(v 1)
memberi1
.
"Nilai" di sini mencakup daftar, bilangan bulat, simbol, atau bawaan apa pun, kecuali ditentukan lain. Jika suatu fungsi didaftarkan sebagai mengambil tipe-tipe spesifik, meneruskannya tipe-tipe yang berbeda adalah perilaku yang tidak terdefinisi, seperti halnya melewati jumlah argumen yang salah (implementasi referensi umumnya macet).
Ada tiga makro bawaan di tinylisp. Makro, tidak seperti fungsi, tidak mengevaluasi argumennya sebelum menerapkan operasi padanya.
q
- kutipan. Mengambil satu ekspresi dan mengembalikannya tidak dievaluasi. Misalnya, mengevaluasi(1 2 3)
memberikan kesalahan karena mencoba memanggil1
sebagai fungsi atau makro, tetapi(q (1 2 3))
mengembalikan daftar(1 2 3)
. Mengevaluasia
memberi nilai yang terikat pada namaa
, tetapi(q a)
memberikan nama itu sendiri.i
- jika. Membawa tiga ekspresi: suatu kondisi, ekspresi iftrue, dan ekspresi iffalse. Mengevaluasi kondisi terlebih dahulu. Jika hasilnya falsy (0
atau nil), evaluasi dan kembalikan ekspresi iffalse. Kalau tidak, evaluasi dan kembalikan ekspresi iftrue. Perhatikan bahwa ekspresi yang tidak dikembalikan tidak pernah dievaluasi.d
- def. Mengambil simbol dan ekspresi. Mengevaluasi ekspresi dan mengikatnya ke simbol yang diberikan diperlakukan sebagai nama di lingkup global , lalu mengembalikan simbol. Mencoba mendefinisikan ulang nama harus gagal (diam-diam, dengan pesan, atau dengan crash; implementasi referensi menampilkan pesan kesalahan). Catatan: tidak perlu mengutip nama sebelum meneruskannyad
, meskipun perlu mengutip kutipan jika daftar atau simbol yang tidak ingin Anda evaluasi: misalnya(d x (q (1 2 3)))
,.
Melewati jumlah argumen yang salah ke makro adalah perilaku yang tidak terdefinisi (implementasi referensi lumpuh). Melewati sesuatu yang bukan simbol sebagai argumen pertama d
adalah perilaku tidak terdefinisi (implementasi referensi tidak memberikan kesalahan, tetapi nilainya tidak dapat direferensikan selanjutnya).
Fungsi dan makro yang ditentukan pengguna
Mulai dari sepuluh built-in ini, bahasa dapat diperluas dengan membangun fungsi dan makro baru. Ini tidak memiliki tipe data khusus; mereka hanya daftar dengan struktur tertentu:
- Fungsi adalah daftar dua item. Yang pertama adalah daftar satu atau lebih nama parameter, atau satu nama yang akan menerima daftar argumen apa pun yang diteruskan ke fungsi (sehingga memungkinkan untuk fungsi variabel-arity). Yang kedua adalah ekspresi yang merupakan fungsi tubuh.
- Makro sama dengan fungsi, kecuali makro berisi nil sebelum nama parameter, sehingga menjadikannya daftar tiga item. (Mencoba memanggil daftar tiga item yang tidak dimulai dengan nihil adalah perilaku yang tidak terdefinisi; implementasi referensi mengabaikan argumen pertama dan memperlakukannya sebagai makro juga.)
Misalnya, ekspresi berikut adalah fungsi yang menambahkan dua bilangan bulat:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
Dan makro yang mengambil sejumlah argumen dan mengevaluasi dan mengembalikan yang pertama:
(q
(
()
args
(v (h args))
)
)
Fungsi dan makro dapat dipanggil langsung, terikat dengan nama menggunakan d
, dan diteruskan ke fungsi atau makro lain.
Karena badan fungsi tidak dieksekusi pada waktu definisi, fungsi rekursif mudah didefinisikan:
(d len
(q (
(list)
(i list If list is nonempty
(s 1 (s 0 (len (t list)))) 1 - (0 - len(tail(list)))
0 else 0
)
))
)
Perhatikan, bagaimanapun, bahwa di atas bukan cara yang baik untuk mendefinisikan fungsi panjang karena tidak menggunakan ...
Rekursi ekor-panggilan
Rekursi ekor-panggilan adalah konsep penting dalam Lisp. Ini mengimplementasikan beberapa jenis rekursi sebagai loop, sehingga menjaga stack panggilan kecil. Penerjemah tinylisp Anda harus menerapkan rekursi ekor-panggilan yang tepat!
- Jika ekspresi balik dari fungsi atau makro yang ditentukan pengguna adalah panggilan ke fungsi atau makro yang ditentukan pengguna lain, juru bahasa Anda tidak boleh menggunakan rekursi untuk mengevaluasi panggilan itu. Sebagai gantinya, ia harus mengganti fungsi dan argumen saat ini dengan fungsi dan argumen baru dan loop sampai rantai panggilan diselesaikan.
- Jika ekspresi balik dari fungsi atau makro yang ditentukan pengguna adalah panggilan untuk
i
, jangan segera mengevaluasi cabang yang dipilih. Alih-alih, periksa apakah itu panggilan ke fungsi lain yang ditentukan pengguna atau makro. Jika demikian, tukar fungsi dan argumen seperti di atas. Ini berlaku untuk kejadian yang sangat bersarang darii
.
Rekursi ekor harus bekerja baik untuk rekursi langsung (fungsi memanggil dirinya sendiri) dan rekursi tidak langsung (fungsi a
panggilan fungsi b
yang memanggil [dll] yang memanggil fungsi a
).
Fungsi panjang rekursif ekor (dengan fungsi pembantu len*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
Implementasi ini berfungsi untuk daftar besar yang sewenang-wenang, hanya dibatasi oleh ukuran integer maks.
Cakupan
Parameter fungsi adalah variabel lokal (sebenarnya konstanta, karena tidak dapat dimodifikasi). Mereka berada dalam ruang lingkup sementara tubuh panggilan fungsi itu dieksekusi, dan di luar ruang selama panggilan yang lebih dalam dan setelah fungsi kembali. Mereka dapat "membayangi" nama yang ditentukan secara global, sehingga membuat nama global tersebut untuk sementara tidak tersedia. Misalnya, kode berikut mengembalikan 5, bukan 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
Namun, kode berikut mengembalikan 41, karena x
pada panggilan tingkat 1 tidak dapat diakses dari panggilan tingkat 2:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
Satu-satunya nama dalam lingkup pada waktu tertentu adalah 1) nama lokal dari fungsi yang sedang dijalankan, jika ada, dan 2) nama global.
Persyaratan pengiriman
Masukan dan keluaran
Penerjemah Anda dapat membaca program dari stdin atau dari file yang ditentukan melalui stdin atau argumen baris perintah. Setelah mengevaluasi setiap ekspresi, itu harus menampilkan hasil dari ekspresi itu ke stdout dengan baris baru yang tertinggal.
- Integer harus menjadi output dalam representasi paling alami bahasa implementasi Anda. Bilangan bulat negatif dapat berupa output, dengan tanda minus terkemuka.
- Simbol harus berupa output sebagai string, tanpa tanda kutip di sekitarnya atau lolos.
- Daftar harus berupa output dengan semua item dipisahkan dengan ruang dan dibungkus dengan tanda kurung. Ruang di dalam tanda kurung adalah opsional:
(1 2 3)
dan( 1 2 3 )
keduanya merupakan format yang dapat diterima. - Mengeluarkan fungsi dan makro bawaan adalah perilaku yang tidak terdefinisi. (Interpretasi referensi menampilkannya sebagai
<built-in function>
.)
Lain
Interpreter referensi termasuk lingkungan REPL dan kemampuan untuk memuat modul tinylisp dari file lain; ini disediakan untuk kenyamanan dan tidak diperlukan untuk tantangan ini.
Uji kasus
Kasing uji dipisahkan menjadi beberapa kelompok sehingga Anda dapat menguji yang lebih sederhana sebelum mengerjakan yang lebih rumit. Namun, mereka juga akan berfungsi dengan baik jika Anda membuang semuanya dalam satu file bersama. Hanya saja, jangan lupa untuk menghapus judul dan output yang diharapkan sebelum menjalankannya.
Jika Anda telah menerapkan rekursi panggilan-tailing dengan benar, test case akhir (multi-bagian) akan kembali tanpa menyebabkan stack overflow. Implementasi referensi menghitungnya dalam waktu sekitar enam detik di laptop saya.
-1
, saya masih bisa menghasilkan nilai -1 dengan melakukan (s 0 1)
.
F
tidak tersedia dalam fungsi G
jika F
panggilan G
(seperti dengan pelingkupan dinamis), tetapi mereka juga tidak tersedia dalam fungsi H
jika H
fungsi bersarang didefinisikan di dalam F
(seperti dengan pelingkupan leksikal) - lihat uji kasus 5. Jadi menyebutnya "leksikal "Mungkin menyesatkan.