Это ещё один "эзотерический" язык, не относитесь к нему слишком серьёзно :)

Некоторые языки хвастаются отсутствием циклов - например в Erlang или Scheme циклы реализованы через хвостовую рекурсию. Иногда "отсутствием циклов" называют конструкцию в духе (1..10).forEach(something) - а отсутствием условного оператора, какую-нибудь разновидность match. Честно говоря, выглядит просто как альтернативный синтаксис.

А как можно "совсем без"? Вот в машине Тьюринга и подобных автоматах мы переключаем "состояние" и дальнейшее исполнение программы зависит от того в какое состояние мы попали. Это похоже на GOTO у которого параметр не обязан быть константой.

Попробуем смастерить интерпретатор в котором базовыми являются всего две операции - присваивание и "вычисляемый GOTO" - и посмотрим что получилось. На маленьких примерах разберем синтаксис - а два побольше, с простыми числами и с игрой в кости - дадут возможность поэкспериментировать с "реалистичной" задачей.

Конечно, это "прототипичная" версия - в ней не хватает многих фишек - и она абсолютно открытая к вашим предложениям и идеям!

Общие замечания

Наш язык похож на Бейсик из которого выкинуто почти всё. В то же время он умеет работать с числовыми и строковыми данными, с массивами (тоже с числовыми или строковыми индексами - в духе JS и PHP). Также и метки для переходов могут быть и числовыми и строковыми. Выражения поддерживают все популярные бинарные операции (арифметические, сравнения, логические).

Интерпретатор на скорую ручку написан на JS, поэтому он умеет взаимодействовать (через операцию EXEC) с элементами веб-страницы. Исходный код в гитхабе, но нам больше интересна "песочница" упомянутая далее - для экспериментов с языком:

так выглядит работа с интерпретатором в "песочнице"
так выглядит работа с интерпретатором в "песочнице"

Название ZLE пока просто рабочее. Из известных мне языков оно как минимум в польском означает "Плохо!" - ну вот и хорошо, это отражает суть :) Аббревиатура, как оказалось, используется также для "Zsh Line Editor" но т.к. это не язык, надеюсь, не страшно. Рабочая "расшифровка" - Zany Language for Enthusiasts - "ненормальный" язык для энтузиастов.

Первые шаги - оператор EXEC и песочница

Формально это вспомогательный оператор. Но чтобы от языка была хоть минимальная польза (да и чтобы видеть что происходит, отлаживать) - нужно как-то уметь взаимодействовать с окружающей средой. Для этого служит EXEC которому можно передать имя внешней функции и параметры, например:

exec "alarm", "Preved, Medved!"
exec "console.log", "I'm borken"
exec "Math.exp", 4
exec "prompt", "your name?"

В принципе в нашем случае с его помощью можно дотянуться до любых функций DOM. Если нужен результат возвращённый функцией, он будет записан в переменную _ (как в случае с Math.exp и prompt).

Откройте страничку с "песочницей" - здесь небольшая textarea для ввода кода, кнопка для запуска - и ниже зона куда можно писать вызывая рукотворную js-функцию output - это немного удобнее чем alert. Попробуйте ввести и выполнить такую программу:

exec "prompt", "What is your name?"
exec "output", "Glad to see you, " + _ + "!"

При запуске вы должны увидеть стандартный диаложек спрашивающий как вас зовут. Введите что-нибудь (например, Pedros, если не придумать лучше). В поле вывода появится Glad to see you, Pedros!

Присваивание, переменные, массивы

Присваивание пишется обычным образом, с одинарным знаком равенства. Можно (как в старом добром бейсике) использовать оператор LET (милый архаизм, т.к. код был обокраден с php-basic где этот оператор оставался для совместимости).

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

Массивы одномерные, зато допускают строковые индексы (то есть они же хэш-таблицы).

x = 13
y = x * x
z[5] = "what a beautiful number"
exec "output", y + y + " - " + z[5]

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

Наконец, про GOTO и метки

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

В качестве первого примера рассмотрим программу печатающие квадраты чисел от 1 до 10:

i = 1                              # инициализируем счетчик
repeat:                            # метка для повторения
exec "output", i + ": " + (i*i)    # печатаем счетчик и квадрат его
goto i                             # пытаемся выйти из цикла
i=i+1; goto repeat                 # увеличиваем счетчик и возвращаемся назад
10 exec "output", "Well done!"     # строка с номером 10 - сюда мы выходим

Заметим пару мелочей для удобства: можно писать комментарии со знаком #, также можно писать несколько команд в одной строке через точку-с-запятой.

Смысл первых трёх строк понятен из комментариев.

В четвёртой то самое GOTO - мы пытаемся перейти на строчку с меткой (номером) содержащимся в переменной i. Что случится когда i=10 понятно - произойдёт переход на последнюю строку программы (она здесь отмечена в бейсиковом стиле номером 10). Если же требуемой метки не обнаруживается, переход не происходит и программа просто выполняется дальше. Такое поведение пока кажется наиболее простым, но в принципе это обсуждаемо.

Таким образом, пока i не дошло до 10 будет выполняться следующая строчка i=i+1 и goto repeat - переход к повтору всех манипуляций.

Здесь маленькая неоднозначность - по-хорошему repeat в выражении нужно было бы писать в кавычках - иначе goto должен бы воспринять его как переменную, значение которой надо взять и использовать как целевую метку. Но ради простоты записи сделан такой вот "syntactic sugar" - если в выражении только одно слово (имя), то goto сначала проверит есть ли метка с таким именем - и если есть, то трактует имя как константу а не как выражение. В общем, если добавите кавычки, ничего не сломается.

Выражения и операторы

Выражения используются в привычной "инфиксной" нотации и большинство операторов тоже более-менее привычны:

  • арифметические +, -, *, / без пояснений, также %, \ - взятие остатка и целочисленное деление - и наконец возведение в степень ^;

  • сравнения <, >, <=, >=, !=, == в соответствии с "сишным" синтаксисом - а также две дублирующих <>, = неравенство и равенство ("бейсиковый" вариант) - все они возвращают 1 в качестве истины и 0 в противном случае;

  • строковые - из уже упомянутых + и операции сравнения адекватно работают со строками (конкатенация, алфавитное сравнение)

  • логические & и | в качестве AND и OR - причем они в качестве истины возвращают один из своих операндов (как в Lua); ложью считается 0 и пустая строка.

Эти операторы позволяют создавать в выражении для GOTO всякие полезные конструкции. Для примера рассмотрим комическую ситуацию когда перед началом игры у пользователя спрашивают возраст:

exec "prompt", "your age?"; age = _

goto "person" + (age - 16) \ (64-16)

exec "output", "you are too young to play!"; end
person1: exec "output", "you are too old to play!"; end
person0: exec "output", "check passed, let's play!"

Здесь арифметическое выражение в строчке 3 даст в результате 0 для возрастов от 16 до 63 - будет переход на метку person0 - и выполнение программы дальше строки. Если человек оказался старше, результат будет 1 и переход на метку person1 - в ней человека предупреждают что он слишком стар и программа заканчивается командой end. Наконец для слишком молодого человека нужной метки не найдётся и выполнение закончится в строчке 5.

Из очевидных недостатков - выражение и метки довольно туманные - а кроме того человек от 112 лет и старше узнает что он "слишком молод".

Улучшим этот код с помощью операторов сравнения

exec "prompt", "your age?"; age = _

goto "person" + ((age > 64) - (age < 16))

exec "output", "you are too young to play!"; end
person1: exec "output", "you are too old to play!"; end
person0: exec "output", "check passed, let's play!"

Здесь изменилось только выражение в goto - поскольку операторы сравнения возвращают 0 или 1 то результат будет строго одним из трёх 0, 1, 2 - правда сами метки всё-таки имеют не очень понятные имена.

Можно улучшить и это используя логические операторы

exec "prompt", "your age?"; age = _

goto "person." + ((age > 64 & 'old') | (age < 16 & 'young'))

exec "output", "you are eligible to play!"
# some game here
end

person.old: exec "output", "you are too old to play!"; end
person.young: exec "output", "you are too young to play!"; end

Как видите, здесь выражение уже генерирует вполне человеко-читаемую метку. Нужно только соблюдать приоритет операций. Отметим также что строковые значения можно указывать и в одиночных кавычках (на данный момент разницы с двойными нет).

Стеки и Подпрограммы

В первоначальном варианте были предусмотрены команды CALL и RETURN для перехода к подпрограмме и возврата, но вскоре стало понятно что они излишни. Достаточно сделать чтобы интерпретатор мог манипулировать стеком и указателем инструкций.

Для операций со стеком добавлены привычные команды PUSH и POP - первая сохраняет значение переданного выражения на стеке, вторая наоборот выталкивает ранее сохранённое значение в указанную переменную (или ячейку массива). Их можно использовать и не только для вызовов подпрограмм, но и для временного хранения значений. Быть может интересно было бы добавить несколько команд-операций прямо на стеке в духе FORTH но пока это не цель нашего мини-языка.

Кроме того обе команды принимают дополнительный параметр в котором можно предать имя стека (то есть можно использовать не только стек по умолчанию а любой массив). Пока не 100% уверен что это нужная фича :)

push "Arzamas-"          # пуш в дефолтный стек
push 16, "other"         # пуш в стек (массив) с именем 'other'
pop x                    # поп из дефолтного стека
pop y, "other"           # поп из массива с именем 'other'
exec "output", x + y

Две переменные с невыразительными именами PC и SP связаны с исполняющей системой интерпретатора - первая это счетчик инструкций (program counter) а вторая является указателем стека (stack pointer).

Первая позволяет выполнять переходы к подпрограммам и возвраты таким образом:

exec "output", "Starting..."
push pc+1; goto test_subroutine
exec "output", "Complete!"
end

test_subroutine:
exec "output", "Preved Medved"
pop pc

Здесь перед выполнением goto мы заталкиваем в стек адрес инструкции следующей за goto (тут немного тонко: при выполнении самого push счетчик pc уже указывает на следующую за ней goto - т.к. он увеличивается при выборке команды, до исполнения - поэтому прибавив к нему единицу мы получим правильный адрес возврата).

А команда pop pc попросту вытолкнет этот сохранённый ранее адрес обратно в указатель инструкций и "переход" произойдёт сам собой. Здесь закрадывается мысль - ведь присваивая значения к PC мы могли бы вообще без goto обходиться, одними присваиваниями - к сожалению манипулировать физическими номерами строк вместо меток конечно уж слишком неудобно и "низкоуровнево". Хотя такая возможность остается - можете поэкспериментировать!

Переменная SP позволяет "подглядывать" значения в стеке (и даже подменять их). Дело в том что сам стек хранится как массив STACK поэтому значения в нём доступны в программе:

push 8
push 13
exec "output"; stack[sp] + ':' + stack[sp-1]  # напечатает 13:8

Пожалуй, эти трюки можно использовать для передачи параметров в сишном стиле (но эту мысль я ещё не додумал).

Пример побольше - Простые Числа

Попробуем изобразить программу заполняющую массив простыми числами (способом trial division). Массив простых чисел будет P[...] - и в начале работы удобно первые несколько элементов в нём предзаполнить. Тут-то и сказывается один из недостатков текущей версии языка - у нас нет литералов для массивов. Ну не беда, возьмём число 7532 и разобрав его на цифры занесем их в массив. В остальном программа незамысловатая - числа-кандидаты в переменной i проверяются с помощью подпрограммы trynext, которая возвращает в _ либо 0 либо 1 в зависимости от простоты числа. Если число не простое, переходим к метке skipthis. Останавливаемся дойдя до 100.

В данной версии программа печатает найденные числа - но можно убрать команду печати и увеличить лимит, чтобы оценить насколько быстро (медленно) работает интерпретатор.

x = 7532; n = 0
initp: p[n] = x%10; x = x\10; n = n+1; goto x=0|"initp"

i = 11
addmore:
push pc+1; goto trynext; goto _|"skipthis"
p[n] = i
exec "output", n + ": " + i
n = n+1
skipthis:
i = i+2; goto i>100|"addmore"
end

trynext:
_ = 1; t = 1
nextdiv:
d = p[t]; goto d*d<=i|"isprime"
goto i%d|"notprime"
t = t+1; goto nextdiv
notprime:
_ = 0
isprime:
pop pc

Пример взаимодействия с JS - игра в кости

Сэм Лойд приводит такую игру в кости под названием Fair Dice Game (слово "fair" лукаво - может означать "честная" или "ярмарочная"): игрок выбирает число от 1 до 6 и бросает три кубика. Если число выпадает хоть на каком-нибудь кубике, он получает выигрыш в размере ставки (удвоенной или утроенной если число появилось на 2 или 3 кубиках сразу). В противном случае его ставку забирает "казино".

Как вам кажется, справедливы ли шансы в этой игре?

скриншот веб-страницы нашей игры
скриншот веб-страницы нашей игры

Вот отличная возможность проверить - небольшая реализация этой игры в веб-страничке. Здесь логика написана на ZLE - в то время как JavaScript используется для связи элементов страницы с интерпретатором. Происходит это так:

  • код "игровой логики" записан в переменную в скрипте, в виде многострочного литерала

  • он тут же парсится с помощью zle.parseLines(...) и готов к выполнению

  • к кнопкам выбора числа (1..6) привязан вызов функции запускающей интерпретатор

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

  • каждый раз после выполнения кода игровой логики интерпретатором JS проверяет переменную continue и повторяет вызов интерпретатора (с небольшой задержкой) покуда это необходимо - это позволяет из кода на ZLE устроить простейшую "анимацию" бросания костей

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

Полюбуйтесь например на код, который "катает" кости - он выбрасывает всё новые и новые значения для каждого кубика, пока значение не повторится (с предыдущим) - после чего отмечается что данный кубик остановился (в массиве F) - сами значения костей попадают в массив D, а значения на кубике генерируются подпрограммой "cast".

cont:
i = 0; s = 0
roll: push pc+1; goto cast
goto _ <> d[i] & f[i] | 'rollstop'    # особенно прекрасная строчка
d[i]=_; s=s+1; goto rollnext
rollstop: f[i]=0
rollnext: i=i+1; goto i=3|'roll'
exec "console.log", "rolled " + s
continue = s>0

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

Вообще "божественную" функцию для обновления элементов на странице можно было бы заменить на парочку setTextById и setValueById, которые интерпретатор сможет дёргать независимо.

Заключение

В целом мы видим, что при написании "практического" кода мы обычно используем логические выражения в GOTO, так что на самом деле разница с IF-ELSE-ами не слишком велика. Выражения позволяют, впрочем, конструировать достаточно хитроумные переходы, но это не слишком часто нужно (и не всегда это удаётся сделать удобно). Хотя может быть нужно развивать привычку к таким выражениям чтобы эффективнее их использовать :)

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

Из фишек которые полезно бы добавить в первую очередь, наверное, стоит назвать функции - встроенные и пользовательские. В идеале - так чтобы функция вызывалась так же по метке (как и подпрограмма) с передачей параметров через стек. Из краеугольных вещей которые на данный момент отсутствуют - операции со строками (подстроки, итерации по символам) - может быть для этого можно придумать отдельный синтаксис или создать специальные функции (вроде MID(...) в бейсике).

Комментарии (1)