Блог типа

Ад Концепций

Что если всё это время понятие жизнь программы недооценивали? Жизнь, а точнее, жизненный цикл программы можно описать повторяющейся последовательностью конечных процессов. Обязательно конечных, в каком-то разумном временном отрезке.

Когда появляется программа? Скорее всего, программа появляется в голове у проектировщика/разработчика, можно назвать это design-time. Но так как этот момент не поддаётся контролю компьютера (пока), то предположим, что моментом появления программы является момент создания минимального запускаемого (о подробном смысле этого термина стоит поговорить отдельно) исходного кода. В контексте Оберона - программа рождается, когда появляется минимальный модуль.

Затем, после этапа написания некоторого программного кода, программа передается компилятору. Компилятор обеспечивает т.н. compile-time - время компиляции. В результате выполнения процесса компиляции мы получаем компилят (то есть непосредственный результат обработки нашего исходного кода).

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

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

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

После получения компилята, над ним, сразу или отложенно должен быть исполнен процесс связывания или линковки. Так как компилят обычно хранится в файле, то возникает время загрузки - load-time. В это время над компилятом могут быть произведены дополнительные преобразования в код целевой платформы, и в это время может быть выполнен код времени загрузки, например, обработка компилята оптимизирующим компилятором. На данный момент известна только одна технология, slim-binaries, поздняя кодогенерация на целевой платформе, которая хоть как-то описывает время загрузки.

Обычно, линковка необходима, так как компилятор всегда производит компилят для одного модуля для непосредственного исполнения на целевой машине. Однако в реальной модульной системе на машине одновременно будут исполнены несколько модулей. Эта ситуация в многомодульной системе приводит к необходимости связывания. В старых системах связывание могло производиться непосредственно после компиляции, но этот вырожденный случай мы не рассматриваем. Итак, возникает время связывания, или link-time.

Мало что известно про выполнение кода во время связывания, однако понятно, что в момент связывания возможно выполнение кода, например, Dependency Injection и динамического наследования, как это может быть реализовано в рамках работы внутри jvm. Такой контроль над еще не запущенным в действие, но уже готовым к исполнению кодом программы позволяет реализовывать автоматизированную настройку и так далее. После связывания и настройки код непосредственно готов к запуску.

Запуск и дальнейшая работа. Представлены временем инициализации (init-time) и временем исполнения (run-time). В сущности, результат работы этого этапа жизненного цикла и является обычно непосредственной целью написания программы. Можно дополнительно выделить время завершения работы программы (close-time). Однако сейчас все три времени работы обычно принято называть run-time, а логическое деление на три этапа реализовывать уже в рамках клиентского программного кода. Такой подход снижает требования к среде исполнения (run-time environment) но не дает гарантий исполнения того или иного этапа в нужной последовательности из-за возможных ошибок исполнения (run-time error) после которых исполнение, обычно, должно завершиться аварийно.

Отдельным важным временем жизни программы является посмертное время (death-time), в которое, вопреки распространенным представлениям тоже является частью жизненного цикла программы. Объяснение тут простое, так как целью работы программы обычно является некий результат, обычно зависящий от входных данных, которых может быть очень много, программы строятся с применением методов, позволяющих итеративно обрабатывать входные данные и производить выходные данные, которые могут быть поданы на вход следующей итерации. Обычно такие данные структурированы (БД), версионны и их содержимое и интерпретация зависит от реализации работающей программы. То есть, программа это алгоритмы и данные, в том числе внешние и степень связанности этих внешних данных с программой никак не влияет на сам факт наличия такой связанности на любом этапе жизненного цикла.

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

Итак, рассмотрены этапы жизненного цикла программы: compile-time -> load-time -> link-time -> init-time -> run-time -> close-time -> death-time.

Применение

По прошествии некоторого времени после окончания периода активной разработки проектов LEAF и LOMO становится понятно, что примеров применения языков не найти. Возможно, Вирт изначально поcтупал более верно и разрабатывал язык общего назначения под конкретный проект, в котором язык проходил “крещение огнем” и дальше уже жил взрослой жизнью. Для новых языков, особенно концептуальных и минималистичных, как lomo, так просто проекта не найти.

Одно из применений связке LEAF + LOMO мне видится в создании систем построения веб-страниц. Аналог HTML, но с большим уклоном именно в программирование всего подряд, тюринг-полноту каждой сущности, а не в text markup, как это видели в 90-х.

Интеграция

Как и следовало ожидать, после того как я полностью скопировал фичи типов данных из LEAF, идея интегрировать LEAF в LOMO в качестве особого агента не заставила себя долго ждать. Так как LEAF-машина однопоточная, то никаких трудностей не возникло, выполнение работы обертывается в формат правила, входное сообщение передается псевдоагенту, а выходное поступает на запись в переменную обычного агента.

UNIT Top
	VAR x ANY
PROCESS
	<<"type": "ping">> \LEAF "TestEcho"  -> x
END Top

Типа того.

Концепции Lomo

Многие решения касаемо синтаксиса взяты из LEAF. Минимум разделителей, прямые заимствования из Оберона.

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

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

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

При таком подходе иначе выглядит модульность. Модули всё так же представляют клиентам переменные для чтения и записи, но при этом все модули исполняются параллельно, а для клиентов модули представлены как экземпляры объектов, хранящиеся в переменных. Работа всех модулей начинается одновременно, и здесь действует то же правило доступа к переменным, что и внутри одного модуля, чтение значения возможно только после записи.

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

При этом существенно разделяют переменные только для чтения и переменные только для записи.

Таким образом реализуется восприятие и активность агента. Агент воспринимает входящие значения и порождает новых агентов и исходящие сообщения.

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

Таким образом реализуется внутреннее состояние агента/системы агентов.

Работа Кипит

При работе над компилятором LOMO проявляются тонкие моменты, которые не были очевидны на бумаге.

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

UNIT Top
	REG x INTEGER
	VAR z INTEGER
PROCESS
	x := x = 0 ? 1 :: x = 1 ? 2 :: 0
	z := x
END Top

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

Такие вот тонкие моменты.