Các lập trình viên Lisp tự hào rằng Lisp là một ngôn ngữ mạnh mẽ có thể được xây dựng từ một tập hợp rất nhỏ các hoạt động nguyên thủy . Chúng ta hãy thực hiện ý tưởng đó bằng cách đánh golf một thông dịch viên cho một phương ngữ được gọi tinylisp
.
Đặc tả ngôn ngữ
Trong thông số kỹ thuật này, bất kỳ điều kiện nào có kết quả được mô tả là "không xác định" có thể làm bất cứ điều gì trong trình thông dịch của bạn: sự cố, thất bại trong âm thầm, tạo ra gobbldegook ngẫu nhiên hoặc hoạt động như mong đợi. Một triển khai tham chiếu trong Python 3 có sẵn ở đây .
Cú pháp
Mã thông báo trong tinylisp là (
, )
hoặc bất kỳ chuỗi nào của một hoặc nhiều ký tự ASCII có thể in được trừ dấu ngoặc đơn hoặc dấu cách. (Tức là regex sau : [()]|[^() ]+
.) Bất kỳ mã thông báo nào bao gồm hoàn toàn các chữ số là một số nguyên. (Số không hàng đầu là ổn.) Bất kỳ dấu hiệu có chứa chữ số không là một biểu tượng, thậm chí ví dụ số-looking thích 123abc
, 3.14
và -10
. Tất cả các khoảng trắng (bao gồm, ở mức tối thiểu, các ký tự ASCII 32 và 10) bị bỏ qua, ngoại trừ trong trường hợp nó tách các mã thông báo.
Một chương trình tinylisp bao gồm một loạt các biểu thức. Mỗi biểu thức là một số nguyên, ký hiệu hoặc biểu thức s (danh sách). Danh sách bao gồm 0 hoặc nhiều biểu thức được gói trong ngoặc đơn. Không có dải phân cách được sử dụng giữa các mục. Dưới đây là ví dụ về biểu thức:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Các biểu thức không được định dạng tốt (đặc biệt là các dấu ngoặc đơn chưa từng có) đưa ra hành vi không xác định. (Việc triển khai tham chiếu tự động đóng các parens mở và dừng phân tích cú pháp trên các parens đóng chưa từng có.)
Loai du lieu
Các kiểu dữ liệu của tinylisp là số nguyên, ký hiệu và danh sách. Các hàm và macro tích hợp cũng có thể được coi là một loại, mặc dù định dạng đầu ra của chúng không được xác định. Một danh sách có thể chứa bất kỳ số lượng giá trị thuộc bất kỳ loại nào và có thể được lồng sâu tùy ý. Số nguyên phải được hỗ trợ ít nhất từ -2 ^ 31 đến 2 ^ 31-1.
Danh sách trống - ()
cũng được gọi là nil - và số nguyên0
là các giá trị duy nhất được coi là sai về mặt logic; tất cả các số nguyên khác, danh sách không trống, nội dung và tất cả các biểu tượng đều đúng.
Đánh giá
Biểu thức trong một chương trình được đánh giá theo thứ tự và kết quả của mỗi lần gửi đến thiết bị xuất chuẩn (nhiều hơn về định dạng đầu ra sau này).
- Một số nguyên đánh giá chính nó.
- Danh sách trống
()
đánh giá chính nó. - Một danh sách của một hoặc nhiều mục đánh giá mục đầu tiên của nó và coi nó là một hàm hoặc macro, gọi nó với các mục còn lại làm đối số. Nếu mục không phải là hàm / macro, hành vi không được xác định.
- Một biểu tượng đánh giá như một tên, đưa ra giá trị ràng buộc với tên đó trong hàm hiện tại. Nếu tên không được xác định trong hàm hiện tại, nó sẽ ước lượng giá trị ràng buộc với nó ở phạm vi toàn cục. Nếu tên không được xác định ở phạm vi hiện tại hoặc toàn cầu, kết quả không được xác định (triển khai tham chiếu đưa ra thông báo lỗi và trả về nil).
Các hàm và macro tích hợp
Có bảy chức năng tích hợp trong tinylisp. Một hàm đánh giá từng đối số của nó trước khi áp dụng một số thao tác cho chúng và trả về kết quả.
c
- khuyết điểm [danh sách đường dẫn]. Đưa ra hai đối số, một giá trị và một danh sách và trả về một danh sách mới thu được bằng cách thêm giá trị ở phía trước danh sách.h
- đầu ( xe hơi , theo thuật ngữ Lisp). Lấy một danh sách và trả về mục đầu tiên trong đó, hoặc không nếu không.t
- đuôi ( cdr , theo thuật ngữ Lisp). Lấy một danh sách và trả về một danh sách mới có chứa tất cả trừ mục đầu tiên hoặc không nếu không được cung cấp.s
- trừ đi. Lấy hai số nguyên và trả về số trừ thứ nhất.l
- ít hơn. Có hai số nguyên; trả về 1 nếu cái đầu tiên nhỏ hơn cái thứ hai, 0 nếu không.e
- công bằng. Lấy hai giá trị cùng loại (cả hai số nguyên, cả hai danh sách hoặc cả hai ký hiệu); trả về 1 nếu hai số bằng nhau (hoặc giống hệt nhau trong mọi phần tử), 0 nếu không. Các nội dung kiểm tra tính bằng nhau không được xác định (triển khai tham chiếu hoạt động như mong đợi).v
- đánh giá. Lấy một danh sách, số nguyên hoặc ký hiệu, biểu thị một biểu thức và đánh giá nó. Ví dụ: làm(v (q (c a b)))
giống như làm(c a b)
;(v 1)
cho1
.
"Giá trị" ở đây bao gồm mọi danh sách, số nguyên, ký hiệu hoặc nội dung, trừ khi có quy định khác. Nếu một hàm được liệt kê là lấy các loại cụ thể, thì việc chuyển các loại khác nhau đó là hành vi không xác định, như đang truyền sai số lượng đối số (việc triển khai tham chiếu thường gặp sự cố).
Có ba macro tích hợp trong tinylisp. Một macro, không giống như một hàm, không đánh giá các đối số của nó trước khi áp dụng các hoạt động cho chúng.
q
- Trích dẫn. Có một biểu thức và trả về nó không được đánh giá. Ví dụ, việc đánh giá(1 2 3)
đưa ra lỗi vì nó cố gọi1
là hàm hoặc macro, nhưng(q (1 2 3))
trả về danh sách(1 2 3)
. Đánh giáa
cho giá trị ràng buộc với têna
, nhưng(q a)
đưa ra tên của chính nó.i
- nếu. Có ba biểu thức: một điều kiện, một biểu thức iftrue và một biểu thức iffalse. Đánh giá điều kiện trước. Nếu kết quả là sai (0
hoặc không), hãy đánh giá và trả về biểu thức iffalse. Mặt khác, đánh giá và trả về biểu thức iftrue. Lưu ý rằng biểu thức không được trả lại không bao giờ được đánh giá.d
- def. Có một biểu tượng và một biểu thức. Đánh giá biểu thức và liên kết nó với biểu tượng đã cho được coi là tên ở phạm vi toàn cầu , sau đó trả về biểu tượng. Cố gắng xác định lại tên sẽ thất bại (âm thầm, với một thông báo hoặc do sự cố; triển khai tham chiếu hiển thị thông báo lỗi). Lưu ý: không cần trích dẫn tên trước khi chuyển quad
, mặc dù cần phải trích dẫn biểu thức nếu đó là danh sách hoặc ký hiệu bạn không muốn đánh giá: vd(d x (q (1 2 3)))
.
Truyền sai số lượng đối số cho một macro là hành vi không xác định (sự cố triển khai tham chiếu). Truyền một cái gì đó không phải là một biểu tượng như là đối số đầu tiên của d
hành vi không xác định (thực hiện tham chiếu không gây ra lỗi, nhưng giá trị không thể được tham chiếu sau đó).
Các hàm và macro do người dùng định nghĩa
Bắt đầu từ mười phần dựng sẵn này, ngôn ngữ có thể được mở rộng bằng cách xây dựng các hàm và macro mới. Chúng không có kiểu dữ liệu chuyên dụng; chúng chỉ đơn giản là danh sách với một cấu trúc nhất định:
- Một chức năng là một danh sách của hai mục. Đầu tiên là danh sách một hoặc nhiều tên tham số hoặc một tên duy nhất sẽ nhận được danh sách bất kỳ đối số nào được truyền cho hàm (do đó cho phép các hàm arity biến). Thứ hai là một biểu thức là cơ thể chức năng.
- Một macro giống như một hàm, ngoại trừ việc nó chứa nil trước (các) tên tham số, do đó làm cho nó trở thành một danh sách ba mục. (Cố gắng gọi các danh sách ba mục không bắt đầu bằng nil là hành vi không xác định; triển khai tham chiếu bỏ qua đối số đầu tiên và cũng coi chúng là macro.)
Ví dụ: biểu thức sau đây là hàm thêm hai số nguyên:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
Và một macro lấy bất kỳ số lượng đối số và đánh giá và trả về cái đầu tiên:
(q
(
()
args
(v (h args))
)
)
Các hàm và macro có thể được gọi trực tiếp, ràng buộc với các tên bằng cách sử dụng d
và được truyền cho các hàm hoặc macro khác.
Vì các thân hàm không được thực thi tại thời điểm định nghĩa, nên các hàm đệ quy có thể dễ dàng xác định:
(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
)
))
)
Tuy nhiên, xin lưu ý rằng ở trên không phải là một cách tốt để xác định hàm độ dài vì nó không sử dụng ...
Đệ quy cuộc gọi
Đệ quy cuộc gọi đuôi là một khái niệm quan trọng trong Lisp. Nó thực hiện một số loại đệ quy nhất định như các vòng lặp, do đó giữ cho ngăn xếp cuộc gọi nhỏ. Thông dịch viên tinylisp của bạn phải thực hiện đệ quy cuộc gọi đuôi thích hợp!
- Nếu biểu thức trả về của hàm hoặc macro do người dùng xác định là cuộc gọi đến hàm hoặc macro do người dùng xác định khác, trình thông dịch của bạn không được sử dụng đệ quy để đánh giá cuộc gọi đó. Thay vào đó, nó phải thay thế hàm và đối số hiện tại bằng hàm mới và đối số và vòng lặp cho đến khi chuỗi cuộc gọi được giải quyết.
- Nếu biểu thức trả về của hàm hoặc macro do người dùng xác định là lệnh gọi
i
, không đánh giá ngay lập tức nhánh được chọn. Thay vào đó, hãy kiểm tra xem đó có phải là một cuộc gọi đến một hàm hoặc macro do người dùng định nghĩa khác không. Nếu vậy, trao đổi chức năng và đối số như trên. Điều này áp dụng cho các lần xuất hiện lồng nhau sâu tùy ýi
.
Đệ quy đuôi phải hoạt động cả cho đệ quy trực tiếp (một hàm gọi chính nó) và đệ quy gián tiếp (hàm a
gọi hàm b
gọi [etc] gọi hàm a
).
Hàm độ dài đệ quy đuôi (có chức năng trợ giúp len*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
Việc triển khai này hoạt động đối với các danh sách lớn tùy ý, chỉ giới hạn bởi kích thước số nguyên tối đa.
Phạm vi
Các tham số hàm là các biến cục bộ (thực tế là các hằng số, vì chúng không thể được sửa đổi). Chúng nằm trong phạm vi trong khi phần thân của lệnh gọi đó đang được thực thi và nằm ngoài phạm vi trong bất kỳ lệnh gọi sâu hơn và sau khi hàm trả về. Họ có thể "phủ bóng" các tên được xác định toàn cầu, do đó làm cho tên toàn cầu tạm thời không khả dụng. Ví dụ: đoạn mã sau trả về 5, không phải 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
Tuy nhiên, đoạn mã sau trả về 41, vì x
ở cấp độ cuộc gọi 1 không thể truy cập được từ cấp độ cuộc gọi 2:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
Các tên duy nhất trong phạm vi tại bất kỳ thời điểm nào là 1) tên cục bộ của hàm hiện đang thực thi, nếu có và 2) tên toàn cầu.
Yêu cầu nộp đơn
Đầu vào và đầu ra
Trình thông dịch của bạn có thể đọc chương trình từ stdin hoặc từ một tệp được chỉ định thông qua stdin hoặc đối số dòng lệnh. Sau khi đánh giá từng biểu thức, nó sẽ xuất kết quả của biểu thức đó thành thiết bị xuất chuẩn với một dòng mới.
- Số nguyên phải là đầu ra trong biểu diễn tự nhiên nhất của ngôn ngữ thực hiện của bạn. Số nguyên âm có thể là đầu ra, với các dấu trừ hàng đầu.
- Các biểu tượng phải là đầu ra dưới dạng chuỗi, không có dấu ngoặc kép hoặc thoát.
- Danh sách nên được xuất với tất cả các mục được phân tách bằng dấu cách và được gói trong ngoặc đơn. Một khoảng trắng bên trong dấu ngoặc đơn là tùy chọn:
(1 2 3)
và( 1 2 3 )
cả hai định dạng đều được chấp nhận. - Xuất ra các hàm và macro tích hợp là hành vi không xác định. (Giải thích tham chiếu hiển thị chúng dưới dạng
<built-in function>
.)
Khác
Trình thông dịch tham chiếu bao gồm môi trường REPL và khả năng tải các mô-đun tinylisp từ các tệp khác; những thứ này được cung cấp để thuận tiện và không bắt buộc cho thử thách này.
Các trường hợp thử nghiệm
Các trường hợp thử nghiệm được tách thành nhiều nhóm để bạn có thể kiểm tra các trường hợp đơn giản hơn trước khi làm việc với các nhóm phức tạp hơn. Tuy nhiên, chúng cũng sẽ hoạt động tốt nếu bạn kết hợp tất cả chúng trong một tệp lại với nhau. Chỉ cần đừng quên loại bỏ các tiêu đề và đầu ra dự kiến trước khi chạy nó.
Nếu bạn đã thực hiện đệ quy cuộc gọi đuôi đúng cách, trường hợp kiểm tra cuối cùng (đa phần) sẽ trả về mà không gây ra lỗi tràn ngăn xếp. Việc thực hiện tham chiếu tính toán nó trong khoảng sáu giây trên máy tính xách tay của tôi.
-1
, tôi vẫn có thể tạo giá trị -1 bằng cách thực hiện (s 0 1)
.
F
không có sẵn trong hàm G
nếu F
các lệnh gọi G
(như với phạm vi động), nhưng chúng cũng không có sẵn trong hàm H
nếu H
là một hàm lồng nhau được xác định bên trong F
(như với phạm vi từ vựng) - xem trường hợp kiểm tra 5. Vì vậy, gọi nó là "từ vựng "Có thể gây hiểu nhầm.