esolang
January 6, 2023

Язык Piet: программы, выглядящие как абстрактные картины

Существует отдельный класс языков программирования, называемых эзотерическими. Они придуманы не для решения реальных задач, а ради шутки, для исследования границ возможностей языков или для доказательства какой-то идеи.

Например, программы на языке Whitespace состоят только из пробелов и табуляций. Программы на Shakespeare выглядят как сонеты Шекспира, на Rockstar — как power metal баллады. А программу на Malbolge написать практически невозможно — язык специально сделан максимально сложным.

Piet — это один из эзотерических языков, придуманный физиком и автором комиксов Дэвидом Морган-Маром. Название отсылает к художнику Питу Мондриану и его абстрактным композициям.

Победа Буги-Вуги (Victory Boogie Woogie) — последняя
незаконченная картина Пита Мондриана. Начата в 1944 году.

Программы на Piet выглядят как разноцветные геометрические абстрактные картины. При этом существует много возможных способов написать одну и ту же программу. Например, картинка ниже — один из вариантов "Hello World".

Сущности языка

Простейшая сущность Piet — это кодель (codel) — один пиксель. Слово образовано из слияния слов «код» и «пиксель».

Несколько находящихся рядом пикселей одного цвета образуют цветовой блок (color block).

Piet работает только с целыми числами и символами юникода. Поэтому чаще всего количество пикселей одного цвета используют, чтобы задать конкретное целое число.

В основе языка лежит тип данных стек — это список, работающий по принципу LIFO («последним пришёл — первым вышел»).

Исполнение программы

Программа на языке Piet — это изображение, составленное из цветовых блоков по своим особым правилам. Исполняется эта программа начиная с левого верхнего угла: от одного коделя к другому ориентируясь на значения двух указателей.

Первый — указатель направления (direction pointer). Он может указывать направо, вниз, налево или вверх и нужен, чтобы интерпретатор понимал, какой кодель читать следующим.

Второй — переключатель коделей (codel chooser). Он может указывать направо или налево и помогает интерпретатору определить, в какую сторону двигаться, если возможных вариантов продолжения программы несколько.

Естественными ограничителями программы являются границы изображения и блоки чёрного цвета — через них пройти нельзя. Если указатель направления показывает на чёрный блок, то направление чтения коделей изменится на следующее возможное.

Программа завершается, если за восемь последовательных попыток интерпретатор застревает и не может сдвинуться с текущего цветового блока.

Цветовое кодирование

Как можно понять из примера программы, для написания кода используются шесть основных секторов цветового круга с их оттенками, дополненные белым и чёрным цветами.

Чёрные блоки нужны, чтобы задавать границы исполнения. Белые — просто пустое пространство, интерпретатор их игнорирует.

Остальные цвета кодируют команды языка. Команды определяются через изменения в оттенке и насыщенности двух соседних цветовых блоков.

Я не буду сейчас приводить описания каждой команды, их можно прочитать на сайте автора. Но часть команд я чуть позже подробно объясню на примере.

Интерпретаторы

Для написания и отладки кода я использовала программу Pietron. В сети также можно найти несколько онлайн интерпретаторов, например, вот этот. Сразу предупрежу, что разные интерпретаторы немного вольно трактуют некоторые правила, поэтому программа, написанная в одном редакторе, может сломаться в другом.

Как написать программу на Piet

Изначально идея освоить Piet у меня возникла, когда я решала задачи из Advent of Code этого года. Я хотела решить одну из них, но в итоге начала с чего-то более простого. Так я написала программу, которая вычисляет сумму любого количества переданных на вход положительных целых чисел.

И сейчас я расскажу, из каких блоков она состоит и как работает. Для удобства я визуально разбила содержательную часть программы на части.

1. Инициализация

Я буду суммировать входные значения и хранить в стеке промежуточное значение суммы. Изначально сумма равна нулю, поэтому мне нужно поместить в стек 0.

Это делают три коделя первого блока. Начальный кодель программы может быть любого цвета, это влияет только на цветовую гамму. Я выбрала тёмно-синий цвет. Идущий вторым кодель светло-синего цвета — команда push, она помещает в стек число, равное количеству коделей предыдущего блока. В результате в стеке окажется число 1. Чтобы превратить 1 в 0 я использовала команду not, она берёт значение из стека, инвертирует его и помещает обратно в стек.

На подобных картинках (они будут после описания)
я буду показывать, какие команды выполняются и как меняется стек

Как определяются команды

Сразу на примере объясню про цвета и оттенки. Чтобы понять, какой цвет выбрать для следующего блока, нужно смотреть на таблицу команд и цвет предыдущего блока.

Для первого коделя я выбрала тёмно-синий цвет. В этом нет скрытого смысла, мне просто так захотелось. Чтобы получить следом команду push, как показано в таблице, мне нужно остаться в том же оттенке, но сдвинуть его насыщенность на один шаг темнее. Темнее тёмно-синего в палитре нет, поэтому идём в начало и берём светло-синий.

Похоже получается и команда not. По таблице из верхней левой ячейки нужно сделать сдвиг на два шага по оттенку, получив красный вместо синего, и на два шага по насыщенности, получив тёмный вместо светлого.

Кстати, можно не пугаться — по таблице вручную ничего определять не придётся. Во всех интерпретаторах языка есть подсказки и расшифровки команд.

Так из тёмно-синего получается светло-синий, а из светло-синего тёмно-красный

2. Суммирование

Когда у меня всё заготовлено, я могу начинать суммировать значения.

Значения я получаю из входящих данных, в интерпретаторах это выглядит как обычное текстовое поле, которое можно заполнить до запуска программы. Мои данные — это положительные числа, каждое на новой строчке. Для удобства последним числом будет цифра 0. Появление нуля я буду использовать как критерий завершения суммирования.

Вторая часть моей программы тоже состоит из всего трёх коделей. Первый из них — команда dup, дублирование последнего значения в стеке. Непосредственно для суммирования она мне не нужна, но очень поможет в будущем. Дело в том, что большинство команд Piet берут параметры из стека и меняют их. Поэтому единственный способ что-то сделать со значением, но не потерять его при этом — это скопировать его заранее.

Второй кодель — это команда in. Она читает число из входного потока данных и помещает его в стек.

Третий кодель — команда add. Она вынимает два последних числа из стека, складывает их и помещает результат обратно в стек. В конце в стеке оказывается старое начальное значение 0 и новая промежуточная сумма 0 + N1, где N1 — первое число из входных данных.

3. Проверка

Это самый большой и наиболее сложный для понимания блок.

Так как количество суммируемых чисел у меня может быть любым, то я не могу бесконечно копировать последовательность команд in-add, мне нужен цикл. Циклы в Piet делаются визуальным разветвлением цветовых блоков. Исполняется тело цикла, потом делается проверка и если условие продолжения цикла выполняется, то с помощью специальных команд можно направить интерпретатор так, чтобы он вернулся в самое начало программы.

Условия для цикла можно придумать разные. Здесь я остановилась на таком: если после прибавления последнего введённого значения сумма не изменилась, то надо заканчивать цикл. Неизменная сумма будет означать, что во входных данных встретился 0, и это мой критерий завершения программы.

На этом этапе у меня в стеке как раз находятся два числа — предыдущее и текущее значения суммы. И есть команда great, которая может сказать мне, какое из чисел больше. Но не всё так просто: great, как и многие другие команды Piet, меняет стек. Я не могу сразу сравнить две суммы, потому что тогда я их потеряю — они исчезнут из стека и будут заменены на результат сравнения.

Поэтому первая команда у меня опять dup, я хочу продублировать текущее значение суммы, чтобы сохранить его после сравнения. Мой стек станет равен <0, N1, N1>. Но теперь я уже не могу ничего нормально сравнить. Я ведь хотела сравнивать 0 и N1, а 0 в самом низу.

Чтобы это исправить, мне нужно перемешать стек так, чтобы 0 оказался сверху. Перемешивание делается с помощью команды roll. Она берёт два последних значения стека <K, L>, где L — последнее, а K — предпоследнее, и циклично сдвигает следующие K позиций в стеке L раз. Например, если K — 4, L — 1, а оставшийся стек <1, 2, 3, 4, 5>, то после roll стек превратится в <1, 5, 4, 3, 2>.

Мне нужно из <0, N1, N1> получить <N1, N1, 0>, то есть вызвать roll(3, 2). Параметры 3 и 2 берутся из стека, и сначала их нужно туда записать двумя командами push. Чтобы записать 3, я делаю предыдущую команду dup блоком из трёх коделей, а чтобы записать 2, я делаю команду push блоком из двух.

Теперь можно сделать roll и получить нужный мне для сравнения стек. После я вызываю команду great, она берёт два последних значения из стека, сравнивает их и помещает в стек 1, если второе число больше первого и 0, если наоборот.

4. Цикл

У меня всё готово для цикла. Условие звучит так: если в стеке сверху лежит 1, то суммирование должно продолжиться, а если лежит 0, то нужно заканчивать.

Как я писала выше, циклы в Piet визуальные — нужно повернуть направление чтения коделей таким образом, чтобы в результате оказаться в самом начале.

За выбор следующего коделя отвечает указатель направления. Изначально он указывает направо, поэтому программа начинает исполнение из левого верхнего угла по прямой направо.

Указатель направления можно поменять командой point. Она принимает один числовой параметр и поворачивает указатель нужное число раз. Например, point(1) изменит указатель со значения «направо» на значение «вниз», а point(2) — на значение «налево».

У меня всё подготовлено для блока point — наверху стека может лежать 0 или 1. Если там 0, то исполнение программы продолжится направо, а если 1, то повернёт вниз.

Казалось бы, всё отлично, но есть нюанс. Я хочу вернуться в начало цикла и чтобы первой командой там по-прежнему была команда dup. Но после блока цвета тёмной мадженты тёмно-красный блок станет командой add, а тёмно-синий — in. Если я просто поверну, не сбросив цвета, то я сломаю себе весь цикл.

Но я могу воспользоваться интересной особенностью языка. По таблице команд светло-голубые кодели после тёмной мадженты — команды roll. По идее они должны менять стек, но если перед ними оказываются блоки белого цвета, то они игнорируются и ничего не делают. Ещё я могу использовать чёрные блоки вокруг для ограничения рабочей области — через них интерпретатор пройти не сможет.

После команд roll у меня есть одна команда push, которая поместит в стек 1. И я сразу попадаю в тёмно-красный блок, который оказывается командой point, забирает единицу из стека и поворачивает указатель направления.

Я пришла в начало, следующим стал тёмно-синий блок, который как мне и нужно, представляет команду dup.

5. Выход из цикла

На последней итерации цикла я получу из входных данных 0. Прибавление нуля не изменит предыдущее значение, и команда roll ничего не перемешает, ведь в стеке окажутся три одинаковых значения. Условие цикла, команда great, вернёт 0, а команда point не поменяет указатель — исполнение программы продолжится в том же направлении, напечатав результат с помощью команды out. Интерпретатор окажется в белой области без каких-либо команд и завершит исполнение программы.

Вот, что происходит на последней итерации цикла. Nn — итоговое значение суммы.

Итог

Ничего из задач Advent of Code я так в итоге на Piet и не решила.

Но я рада, что наконец реализовала свою давнюю хотелку и написала свою первую полноценную программу, пусть она и довольно простая. Мне было весело и интересно!

Возможно, в будущем я и другие эзотерические языки попробую. А вы пробовали?