Это ещё один «эзотерический» язык, не относитесь к нему слишком серьёзно 🙂
Некоторые языки хвастаются отсутствием циклов — например в 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
— и выполнение программы дальше 7й
строки. Если человек оказался старше, результат будет 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(…) в бейсике).
ссылка на оригинал статьи https://habr.com/ru/articles/868726/
Добавить комментарий