Программисты Лисп хвастаются, что Лисп - это мощный язык, который может быть создан из очень небольшого набора примитивных операций . Давайте воплотим эту идею в жизнь, играя в гольф переводчиком для диалекта под названием tinylisp
.
Спецификация языка
В этой спецификации любое условие, результат которого описан как «неопределенный», может делать что-либо в вашем интерпретаторе: аварийно завершать работу, молча терпеть неудачу, генерировать случайный gobbldegook или работать как ожидалось. Ссылочная реализация в Python 3 доступна здесь .
Синтаксис
Лексемы в tinylisp являются (
, )
или любая строка из одного или нескольких печатаемых ASCII символов , кроме скобок или пространства. (Т.е. следующее регулярное выражение:. [()]|[^() ]+
) Любой токен, состоящий полностью из цифр, является целочисленным литералом. (Нули в порядке.) Любой маркер , который содержит нецифровой символ, даже числовые выглядящие примерам нравятся 123abc
, 3.14
и -10
. Все пробелы (включая, как минимум, символы ASCII 32 и 10) игнорируются, кроме случаев, когда они разделяют токены.
Программа tinylisp состоит из серии выражений. Каждое выражение представляет собой целое число, символ или s-выражение (список). Списки состоят из нуля или более выражений, заключенных в скобки. Разделитель между элементами не используется. Вот примеры выражений:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Выражения, которые не являются правильно сформированными (в частности, имеют несопоставимые скобки), дают неопределенное поведение. (Референтная реализация автоматически закрывает открытые парены и прекращает анализ на непревзойденных закрытых паренах.)
Типы данных
Типы данных tinylisp - это целые числа, символы и списки. Встроенные функции и макросы также можно рассматривать как тип, хотя их формат вывода не определен. Список может содержать любое количество значений любого типа и может быть вложен произвольно глубоко. Целые числа должны поддерживаться как минимум от -2 ^ 31 до 2 ^ 31-1.
Пустой список - ()
также называемый nil - и целое число 0
- единственные значения, которые считаются логически ложными; все остальные целые числа, непустые списки, встроенные и все символы логически верны.
оценка
Выражения в программе оцениваются по порядку, и результаты каждого из них отправляются на стандартный вывод (подробнее о форматировании вывода позже).
- Целочисленный литерал оценивает сам по себе.
- Пустой список
()
оценивает сам по себе. - Список из одного или нескольких элементов оценивает его первый элемент и обрабатывает его как функцию или макрос, вызывая его с остальными элементами в качестве аргументов. Если элемент не является функцией / макросом, поведение не определено.
- Символ оценивается как имя, давая значение, связанное с этим именем в текущей функции. Если имя не определено в текущей функции, оно оценивается как значение, привязанное к нему в глобальной области видимости. Если имя не определено в текущей или глобальной области видимости, результат не определен (эталонная реализация выдает сообщение об ошибке и возвращает nil).
Встроенные функции и макросы
В тинилиспе есть семь встроенных функций. Функция оценивает каждый из своих аргументов, прежде чем применить к ним какую-либо операцию и вернуть результат.
c
- минусы [список правил]. Принимает два аргумента, значение и список, и возвращает новый список, полученный путем добавления значения в начале списка.h
- голова ( машина , в терминологии Лисп). Принимает список и возвращает первый элемент в нем, или ноль, если задан ноль.t
- tail ( cdr , в терминологии Lisp). Принимает список и возвращает новый список, содержащий все элементы, кроме первого, или ноль, если задано ноль.s
- вычесть. Берет два целых числа и возвращает первое минус второе.l
- меньше, чем. Занимает два целых числа; возвращает 1, если первое меньше второго, 0 в противном случае.e
- равный. Принимает два значения одного типа (оба целых числа, оба списка или оба символа); возвращает 1, если два равны (или идентичны в каждом элементе), 0 в противном случае. Тестирование встроенных функций на равенство не определено (эталонная реализация работает как положено).v
- Эвал. Берет один список, целое число или символ, представляющий выражение, и оценивает его. Например, делать(v (q (c a b)))
то же самое, что делать(c a b)
;(v 1)
дает1
.
«Значение» здесь включает любой список, целое число, символ или встроенную функцию, если не указано иное. Если функция указана как принимающая определенные типы, передача ее другим типам является неопределенным поведением, как и передача неверного числа аргументов (ссылочная реализация обычно дает сбой).
В tinylisp есть три встроенных макроса. Макрос, в отличие от функции, не оценивает свои аргументы перед применением к ним операций.
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
является неопределенным поведением (ссылочная реализация не выдает ошибку, но впоследствии на значение нельзя ссылаться).
Пользовательские функции и макросы
Начиная с этих десяти встроенных функций, язык можно расширять, создавая новые функции и макросы. У них нет выделенного типа данных; это просто списки с определенной структурой:
- Функция представляет собой список из двух элементов. Первый - это либо список из одного или нескольких имен параметров, либо одно имя, которое получит список любых аргументов, переданных функции (таким образом, учитывая функции переменной арности). Второе - это выражение, которое является телом функции.
- Макрос такой же, как функция, за исключением того, что он содержит nil перед именами параметров, что делает его списком из трех элементов. (Попытка вызвать трехэлементные списки, которые не начинаются с nil, является неопределенным поведением; эталонная реализация игнорирует первый аргумент и обрабатывает их как макросы.)
Например, следующее выражение - это функция, которая добавляет два целых числа:
(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
)
))
)
Обратите внимание, однако, что вышеприведенное не является хорошим способом определения функции длины, потому что она не использует ...
Хвостовая рекурсия
Хвостовая рекурсия является важной концепцией в Лиспе. Он реализует определенные виды рекурсии в виде циклов, таким образом сохраняя стек вызовов небольшим. Ваш интерпретатор tinylisp должен реализовать правильную рекурсию хвостового вызова!
- Если возвращаемое выражение пользовательской функции или макроса является вызовом другой пользовательской функции или макроса, ваш интерпретатор не должен использовать рекурсию для оценки этого вызова. Вместо этого он должен заменить текущую функцию и аргументы новой функцией и аргументами и выполнить цикл, пока цепочка вызовов не будет разрешена.
- Если возвращаемое выражение пользовательской функции или макроса является вызовом
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) глобальные имена.
Требования к подаче
Вход и выход
Ваш интерпретатор может читать программу из стандартного ввода или из файла, указанного с помощью стандартного ввода или аргумента командной строки. После оценки каждого выражения он должен вывести результат этого выражения в стандартный вывод с завершающим переводом строки.
- Целые числа должны быть выведены в наиболее естественном представлении вашего языка реализации. Могут быть выведены отрицательные целые числа с ведущими знаками минус.
- Символы должны выводиться в виде строк без окружающих кавычек или экранированных символов .
- Списки должны выводиться со всеми элементами, разделенными пробелами и заключенными в скобки. Пространство внутри скобок является необязательным:
(1 2 3)
и( 1 2 3 )
оба приемлемые форматы. - Вывод встроенных функций и макросов - неопределенное поведение. (Ссылочная интерпретация отображает их как
<built-in function>
.)
Другие
Справочный интерпретатор включает среду REPL и возможность загружать модули tinylisp из других файлов; они предоставляются для удобства и не требуются для этой задачи.
Контрольные примеры
Тестовые случаи разделены на несколько групп, так что вы можете тестировать более простые, прежде чем переходить к более сложным. Тем не менее, они также будут отлично работать, если вы свалите их все в один файл вместе. Только не забудьте удалить заголовки и ожидаемый результат перед запуском.
Если вы правильно реализовали рекурсию хвостового вызова, последний (многокомпонентный) контрольный пример вернется без переполнения стека. Эталонная реализация вычисляет его примерно за шесть секунд на моем ноутбуке.
-1
, я все равно могу сгенерировать значение -1 (s 0 1)
.
F
не доступны в функции, G
если F
вызовы G
(как при динамическом определении объема), но они также недоступны в функции, H
если H
вложенная функция определена внутри F
(как в случае с лексическим определением области видимости) - см. Тестовый пример 5. Так называем его «лексический». "может вводить в заблуждение.