Программисты Лисп хвастаются, что Лисп - это мощный язык, который может быть создан из очень небольшого набора примитивных операций . Давайте воплотим эту идею в жизнь, играя в гольф переводчиком для диалекта под названием 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. Так называем его «лексический». "может вводить в заблуждение.Ответы:
Python 2,
685675660657646642640 байтСчитывает ввод из STDIN и записывает вывод в STDOUT.
Хотя это и не обязательно, интерпретатор поддерживает нулевые функции и макросы и оптимизирует хвостовые вызовы, выполняемые через
v
.объяснение
анализ
Для того, чтобы разобрать вход, мы сначала окружаем каждое вхождение
(
и)
с пробелами, и разделить полученную строку на слова; это дает нам список токенов. Мы поддерживаем стек выраженийE
, который изначально пуст. Сканируем токены по порядку:(
, мы помещаем пустой список в начало стека выражений;)
, мы выталкиваем значение в верхнюю часть стека выражений и добавляем его в список, который ранее был ниже его в стеке;Если при обработке обычного токена или после извлечения выражения из стека из-за того
)
, что стек выражений пуст, мы находимся на выражении верхнего уровня и оцениваем значение, которое мы иначе добавили бы, используяV()
и выведите результат, отформатированный соответствующим образомF()
.оценка
Мы поддерживаем глобальную область видимости
G
в виде списка пар ключ / значение. Изначально он содержит только встроенные функции (но не макросы и неv
, которые мы рассматриваем как макрос), которые реализованы как лямбды.Оценка происходит внутри
V()
, который принимает выражение для оценки,e
и локальная область,L
, который тоже, список пар ключ / значение (при оценке выражения верхнего уровня, локальная область пуст.) ВнутренностиV()
живой внутри бесконечного цикла, как мы выполняем оптимизацию хвостового вызова (TCO), как будет объяснено позже.Мы обрабатываем в
e
соответствии с его типом:если это пустой список или строка, конвертируемая в int, мы возвращаем его немедленно (возможно, после преобразования в int); иначе,
если это строка, мы ищем ее в словаре, построенном на основе объединения глобальной и локальной областей. Если мы находим ассоциированное значение, мы его возвращаем; в противном случае,
e
должно быть имя встроено макрос (т.е.q
,i
,d
илиv
), и мы возвращаем его без изменений. В противном случае, еслиe
это не строка,e
является (непустым) списком, то есть вызовом функции. Мы оцениваем первый элемент списка, то есть выражение функции, вызываяV()
рекурсивно (используя текущую локальную область видимости); мы называем результатf
. Остальная часть спискаA
, это список аргументов.f
может быть только строкой, в этом случае это встроенный макрос (или функцияv
), лямбда, в этом случае это встроенная функция или список, и в этом случае это пользовательская функция или макрос.Если
f
это строка, т. Е. Встроенный макрос, мы обрабатываем ее на месте. Если это макросi
илиv
, мы оцениваем его первый операнд и выбираем второй или третий операнд соответственно, в случаеi
, или используем результат первого операнда, в случаеv
; вместо рекурсивной оценки выбранного выражения, что приведет к победе над TCO, мы просто заменимe
указанное выражение и перейдем к началу цикла. Еслиf
это макросd
, мы добавляем пару, первый элемент которой является первым операндом, а второй элемент является результатом вычисления второго операнда, в глобальную областьG
и возвращаем первый операнд. В противном случаеf
это макросq
, и в этом случае мы просто возвращаем его операнд напрямую.Иначе, если
f
это лямбда или список, первым элементом которого не является()
, то это ненулевая функция, а не макрос, и в этом случае мы оцениваем ее аргументы, т. Е. ЭлементыA
, и заменяемA
на результат.Если
f
это лямбда, мы называем его, передавая ему распакованные аргументыA
, и возвращаем результат.В противном случае,
f
это список, то есть пользовательская функция или макрос; его список параметров является вторым по последнему элементу, а его тело - последним элементом. Как и в случае с макросами,i
иv
для выполнения TCO мы не рекурсивно оцениваем тело, а заменяемe
его телом и переходим к следующей итерации. В отличие отi
иv
, однако, мы также заменяем локальную область действияL
новой локальной областью действия функции. Если список параметров,P
, по сути, является списком, новая локальная область создается путем сжатия списка параметров со спискомP
аргументовA
; в противном случае мы имеем дело с функцией с переменным значением, и в этом случае новая локальная область имеет только один элемент - пару(P, A)
.РЕПЛ
Если вы хотите поиграть с ним, вот версия REPL переводчика. Он поддерживает переопределение символов и импорт файлов с помощью аргументов командной строки или
(import <filename>)
макроса. Чтобы выйти из интерпретатора, завершите ввод (обычно это Ctrl + D или Ctrl + Z).Показать фрагмент кода
А вот пример сеанса, реализующего сортировку слиянием:
Показать фрагмент кода
источник
import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
A[0]
некоторую переменную с однимA
в данном случае пусто), и я не хочу «регрессировать».C (GNU), 1095 байтов
Большая часть действия происходит в гигантской
v
функции. Вместо явной реализации хвостовой рекурсии,v
она структурирована так, что многие вызовы изv
доv
будет обрабатываться путем оптимизации хвостовой рекурсии ССЗА. Там нет сборки мусора.Это интенсивно использует расширения GCC, поэтому его можно скомпилировать только с помощью gcc (используйте команду
gcc -w -Os tl.c
). Он также использует некоторыеscanf
расширения, которые не были доступны в Windows, которые я обычно использую. Перспектива написания парсера со стандартомscanf
была настолько ужасна, что я вместо этого использовал виртуальную машину Linux, чтобы протестировать программу. Разбор безscanf
классов символов, вероятно, добавил бы более 100 байтов.Semi-ungolfed
источник
Цейлон, 2422 байта
(Я думаю, что это моя самая длинная гольф-программа.)
Я мог бы сыграть еще несколько байт, так как в некоторых местах я использовал двухбуквенные идентификаторы, но у меня кончились несколько значащих одиночных букв для них. Хотя даже так это не очень похоже на Цейлон ...
Это объектно-ориентированный реализация.
У нас есть интерфейс значений
V
с реализующими классамиL
(список - просто обертка вокруг последовательности ЦейлонаV
),S
(символ - обертка вокруг строки),I
(целое число - обертка вокруг целого числа Цейлона) иB
(встроенная функция или макрос, обертка вокруг Цейлонская функция).Я использую стандартную нотацию равенства Цейлона, реализуя
equals
метод (а такжеhash
атрибут, который действительно нужен только для символов), а также стандартstring
атрибут для вывода.У нас есть логический атрибут
b
(который по умолчанию имеет значение true, переопределяетсяI
иL
возвращает false для пустых списков) и два методаl
(вызов, т.е. использование этого объекта в качестве функции) иvO
(оценка одного шага). Оба возвращают либо значение, либо объект продолжения, который позволяет затем выполнить оценку для еще одного шага иvF
(полностью оценить) циклы, пока результат больше не будет продолжением.Контекстный интерфейс позволяет получить доступ к переменным. Существует две реализации
G
для глобального контекста (который позволяет добавлять переменные с помощьюd
встроенного) иLC
для локального контекста, который активен при оценке выражения пользовательской функции (он возвращается к глобальному контексту).Оценка символов обращается к контексту, списки (если не пустые) оценивают, сначала оценивая их первый элемент, а затем вызывая его метод вызова. Вызов реализуется только списками и встроенными функциями - он сначала оценивает аргумент (если функция, а не макрос), а затем выполняет действительно интересные вещи - для встроенных функций только то, что жестко закодировано, для списков он создает новый локальный контекст и возвращает продолжение с этим.
Для встроенных функций я использовал трюк, аналогичный тому, который я использовал в интерпретаторе Shift , который позволяет мне определять их с нужными им типами аргументов, но вызывать их с общей последовательностью, используя отражение (типы будут проверяться во время вызова). Это позволяет избежать проблем с преобразованием типов / утверждениями внутри функций / макросов, но требует функций верхнего уровня, чтобы я мог получить их
Function
объекты метамодели .p
(parse) разбивает строку на пробелы, символы новой строки и круглые скобки, затем перебирает токены и строит списки, используя стек и текущий список.Интерпретатор (в
run
методе, который является точкой входа) затем берет этот список выражений (которые являются просто значениями), оценивает каждое из них и печатает результат.Ниже приведена версия с комментариями и прогоном через форматтер.
Более ранняя версия до того, как я начал играть в гольф (и все еще с некоторыми недоразумениями относительно оценки списка), найдена в моем репозитории Github , я скоро добавлю ее (так что обязательно посмотрите на первую версию, если вы хотите оригинал).
источник