Програмісти Lisp вихваляються тим, що Lisp - це потужна мова, яку можна створити з дуже невеликого набору примітивних операцій . Давайте зберемо цю ідею на практиці гольф інтерпретатор діалекті називається tinylisp
.
Специфікація мови
У цій специфікації будь-яка умова, результат якої описується як "невизначений", може зробити що завгодно у вашому інтерпретаторі: вийти з ладу, вийти з ладу в мовчазному режимі, створити випадковий gobbldegook або працювати як слід. Довідкова реалізація в Python 3 доступна тут .
Синтаксис
Лексеми в tinylisp є (
, )
або будь-який рядок з одного або декількох друкованих ASCII символів , крім дужок або простору. (Тобто наступний регулярний вираз:) [()]|[^() ]+
. Будь-який маркер, який повністю складається з цифр, є цілим літералом. (Нулі в порядку.) Будь-який маркер , який містить нецифровий символ, навіть числові виглядають прикладів подобаються 123abc
, 3.14
і -10
. Усі пробіли (включаючи, як мінімум, символи 32 і 10 ASCII) ігноруються, за винятком випадків, коли він розділяє лексеми.
Програма тинілісп складається з серії виразів. Кожен вираз є або цілим числом, символом, або s-виразом (список). Списки складаються з нуля або більше виразів, загорнутих у дужки. Між елементами не використовується розділювач. Ось приклади виразів:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Висловлення, які не є добре сформованими (зокрема, що мають невідповідні дужки), дають не визначену поведінку. (Реалізація посилань автоматично закриває відкриті паролі і зупиняє аналіз на неперевершених закритих паролях.)
Типи даних
Типи даних тинілісп - цілі числа, символи та списки. Вбудовані функції та макроси також можна вважати типом, хоча їх вихідний формат не визначений. Список може містити будь-яку кількість значень будь-якого типу і може бути вкладений довільно глибоко. Цілі особи повинні підтримуватися принаймні від -2 ^ 31 до 2 ^ 31-1.
Порожній список ()
- також званий нулем - і ціле число 0
- єдині значення, які вважаються логічно помилковими; всі інші цілі числа, непорожні списки, вбудовані та всі символи логічно вірні.
Оцінка
Вирази в програмі оцінюються в порядку і результати кожного надсилаються в stdout (докладніше про форматування виводу пізніше).
- Ціле число буквально оцінює себе.
- Порожній список
()
оцінює сам. - Список одного або декількох елементів оцінює його перший елемент і розглядає його як функцію або макрос, називаючи його з рештою елементами як аргументи. Якщо елемент не є функцією / макросом, поведінка не визначена.
- Символ оцінюється як ім'я, надаючи значення, пов'язане з цим ім'ям у поточній функції. Якщо ім'я не визначено в поточній функції, воно оцінюється до значення, пов'язаного з ним у глобальному масштабі. Якщо ім’я не визначено в поточній чи глобальній області, результат не визначений (реалізація посилання дає повідомлення про помилку та повертає нуль).
Вбудовані функції та макроси
У тиніліспі є сім вбудованих функцій. Функція оцінює кожен її аргумент, перш ніж застосувати до них якусь операцію і повернути результат.
c
- мінуси [список довідок]. Бере два аргументи, значення і список, і повертає новий список, отриманий додаванням значення в передній частині списку.h
- голова ( машина , в термінології Лісп). Бере список і повертає перший елемент у ньому, або нуль, якщо дано нуль.t
- хвіст ( cdr , в термінології Лісп). Бере список і повертає новий список, що містить усі, крім першого пункту, або нуль, якщо дано нуль.s
- відняти. Бере два цілих числа і повертає перший мінус другий.l
- менше, ніж. Бере два цілих числа; повертає 1, якщо перший менше другого, 0 в іншому випадку.e
- рівний. Приймає два значення одного типу (обидва цілі числа, обидва списки або обидва символи); повертає 1, якщо два рівні (або однакові в кожному елементі), 0 в іншому випадку. Тестування вбудованих рівнів є невизначеним (посилання на реалізацію працює як очікується).v
- eval. Бере один список, ціле число або символ, що представляє вираз, і оцінює його. Наприклад(v (q (c a b)))
, те саме, що робити(c a b)
;(v 1)
дає1
.
"Значення" тут включає будь-який список, ціле число, символ або вбудований, якщо інше не вказано. Якщо функція вказана як прийняття конкретних типів, передача її різним типам є невизначеною поведінкою, як і передача неправильної кількості аргументів (реалізація посилань зазвичай виходить з ладу).
У тиніліспі є три вбудовані макроси. Макрос, на відміну від функції, не оцінює свої аргументи перед тим, як застосувати до них операції.
q
- цитата. Бере один вираз і повертає його неоціненим. Наприклад, оцінка(1 2 3)
дає помилку, оскільки вона намагається викликати1
як функцію чи макрос, але(q (1 2 3))
повертає список(1 2 3)
. Оцінюванняa
дає значення, пов'язане з іменемa
, але(q a)
дає саме ім'я.i
- якщо. Бере три вирази: умова, вираз iftrue та iffalse. Спочатку оцінює стан. Якщо результат є хибним (0
або нульовим), оцінює і повертає вираз iffalse. В іншому випадку оцінює і повертає вираз iftrue. Зауважте, що вираз, який не повертається, ніколи не оцінюється.d
- деф. Бере символ і вираз. Оцінює вираз і прив'язує його до заданого символу, який розглядається як ім'я в глобальному масштабі , а потім повертає символ. Спроба переосмислити ім’я має бути невдалою (мовчки, з повідомленням або збоєм; у виконанні посилання відображається повідомлення про помилку). Примітка: немає необхідності цитувати ім'я , перш ніж передати йогоd
, хоча необхідно процитувати вислів , якщо це список або символ ви не хочете оцінили: наприклад,(d x (q (1 2 3)))
.
Передача неправильної кількості аргументів макросу - це невизначена поведінка (збої в застосуванні посилань). Передача чогось, що не є символом, оскільки перший аргумент d
не визначеного поведінки (реалізація посилання не дає помилки, але значення не може бути посилається згодом).
Функції та макроси, визначені користувачем
Починаючи з цих десяти вбудованих, мову можна розширити, побудувавши нові функції та макроси. Вони не мають виділеного типу даних; вони просто списки з певною структурою:
- Функція - це список двох елементів. Перший - це або список одного або декількох імен параметрів, або одне ім'я, яке отримає перелік будь-яких аргументів, переданих функції (таким чином, дозволяючи виконувати функції змінної арності). Другий - це вираз, який є тілом функції.
- Макрос - це те саме, що і функція, за винятком того, що він містить нуль перед назвою (-ми) параметрів, таким чином, перетворюючи його на три-елементний список. (Спроба викликати трипозиційні списки, які не починаються з нуля, - це невизначена поведінка; реалізація посилань ігнорує перший аргумент і трактує їх також як макроси.)
Наприклад, наступне вираження - це функція, яка додає два цілих числа:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
І макрос, який бере будь-яку кількість аргументів і оцінює та повертає перший:
(q
(
()
args
(v (h args))
)
)
Функції та макроси можна викликати безпосередньо, прив’язати до імен за допомогою d
та передати іншим функціям чи макросам.
Оскільки тіла функцій не виконуються в час визначення, рекурсивні функції легко визначити:
(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
)
))
)
Зауважте, що вищезазначене не є гарним способом визначення функції довжини, оскільки він не використовує ...
Рекурсія хвоста
Рекурсія на виклик хвоста - важлива концепція в Ліспі. Він реалізує певні види рекурсії у вигляді циклів, тим самим зберігаючи стек виклику малим. Ваш перекладач тинілісп повинен реалізовувати належну рекурсивну хворобу !
- Якщо вираз повернення визначеної користувачем функції або макросу є викликом до іншої визначеної користувачем функції або макросу, ваш інтерпретатор не повинен використовувати рекурсію для оцінки цього виклику. Натомість він повинен замінити поточну функцію та аргументи на нову функцію та аргументи та циклічно, поки ланцюг викликів не буде вирішено.
- Якщо вираз повернення визначеної користувачем функції або макросу є викликом до
i
, не одразу оцінюйте обрану гілку. Натомість перевірте, чи це виклик до іншої визначеної користувачем функції або макрос. Якщо так, замініть функцію та аргументи, як описано вище. Це стосується довільно глибоко вкладених подійi
.
Хвостова рекурсія повинна працювати як для прямої рекурсії (функція викликає саму себе), так і непрямої рекурсії (функція a
виклику функція, b
яка викликає [тощо], яка викликає функцію a
).
Хвостово-рекурсивна функція довжини (з допоміжною функцією len*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
Ця реалізація працює для довільно великих списків, обмежених лише максимальним цілим розміром.
Область застосування
Параметри функції - це локальні змінні (фактично константи, оскільки їх неможливо змінити). Вони знаходяться в межах, поки тіло цього виклику цієї функції виконується, і виходить за межі під час будь-яких більш глибоких викликів та після повернення функції. Вони можуть "затінювати" глобально визначені імена, тим самим роблячи глобальне ім'я тимчасово недоступним. Наприклад, наступний код повертає 5, а не 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
Однак наступний код повертає 41, оскільки x
на рівні виклику 1 недоступний з рівня 2 виклику:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
Єдиними іменами в області застосування в будь-який момент часу є 1) локальні імена поточно виконуваної функції, якщо такі є, і 2) глобальні імена.
Вимоги до подання
Вхід і вихід
Ваш перекладач може прочитати програму зі stdin або з файлу, вказаного через аргумент stdin або аргумент командного рядка. Оцінивши кожен вираз, він повинен вивести результат цього виразу в stdout із зворотним новим рядком.
- Цілі лічильники повинні виводитись у найбільш природне уявлення мови вашої реалізації. Негативні цілі числа можуть виводитись із провідними знаками мінус.
- Символи повинні виводитись у вигляді рядків, без навколишніх лапок чи пробілів.
- Списки повинні бути виведені з усіма елементами, розділеними пробілом, і загорнуті в круглі дужки. Простір усередині дужок є необов'язковим:
(1 2 3)
і( 1 2 3 )
обидва прийнятні формати. - Виведення вбудованих функцій та макросів - це невизначена поведінка. (Довідкове тлумачення відображає їх як
<built-in function>
.)
Інший
Довідковий інтерпретатор включає середовище REPL та можливість завантажувати модулі тинілісп з інших файлів; вони надаються для зручності і не потрібні для цього завдання.
Тестові справи
Тестові приклади розділені на кілька груп, так що ви можете протестувати більш прості, перш ніж працювати над більш складними. Однак вони також працюватимуть чудово, якщо ви скинете їх у один файл разом. Просто не забудьте видалити заголовки та очікуваний результат перед його запуском.
Якщо ви належним чином реалізували рекурсію хвостового виклику, остаточний (багатоскладовий) тестовий випадок повернеться, не викликаючи переповнення стека. Референтна реалізація обчислює його приблизно за шість секунд на моєму ноутбуці.
-1
, я все одно можу генерувати значення -1, роблячи це (s 0 1)
.
F
недоступні у функції, G
якщо F
дзвінки G
(як при динамічному масштабуванні), але вони також недоступні у функції, H
якщо H
це вкладена функція, визначена всередині F
(як при лексичному обміні) - див. Тестовий випадок 5. Отже, називаючи це "лексичним "може ввести в оману.