Back to list
Описание CX
- Введение в CX
- Репозиторий проекта
- Синтаксис
- Доступности
- Сильная типизация
- Компиляция и интерпретация
- Встроенный эволюционный алгоритм
- Сериализация
Введение в CX
CX - это одновременно спецификация и язык программирования, разработанные для реализации новой парадигмы программирования, основанной на концепции доступностей. Доступности позволяют программе узнать, какие действия она может производить, а какие нет. Например, мы можем спросить программу, какие аргументы могут быть отправлены функции, и программа вернет список возможных действий. Если этот список вас устривает, выбраете один из вариантов и программа исполнит выбранное действие. В результате использования системы СХ-доступностей строится алгоритм генетического программирования и преобразуется в нативную функцию, которая может быть использована для оптимизации структуры программы во время исполнения.
Спецификация CX устанавливает, что программисту должны быть доступны и компилятор, и интерпретатор. Доступ к интерпретатору возможен через цикл “read-eval-print”, в котором программист может интерактивно добавлять и удалять элементы программы. После завершения написания кода программы, он может быть скомпилирован для повышения производительности.
У системы СХ очень сильная типизация. Единственное приведение типов происходит при определении парсером целых чисел, чисел с плавающей точкой, чисел логического типа, строк и массивов. Если функция требует, к примеру, 64-битное целое число, должна быть явно использована функция преобразования в требуемый тип.
Наконец, СХ-программа может быть полностью сериализована в байтовые массивы, поддерживающие её состояние исполнения и структуру. Эту сериализованную версию программы можно позже десериализовать и продолжить выполнение в любом устройстве, которое имеет CX-интерпретатор / компилятор.
Функции СХ, упомянутые выше, будут подробно описаны в следующих разделах.
Репозиторий проекта
Исходный код проекта может быть загружен с репозитрия на Github: https://github.com/skycoin/cx. Репозиторий содержит файл спецификации, документацию, примеры и сам исходный код.
Ситаксис
Как было упомянуто во введении, CX является как спецификацией, так и языком программирования. Спецификация CX не навязывает синтаксис, скорее описывает структуры и процессы, которые диалект CX должен реализовывать для того, чтобы считаться CX. Как следствие, можно реализовать два диалекта CX, один с синтаксисом, подобным Lisp, и другой с синтаксисом типа C. Этот лежащий в основе язык называется CX Base, или “базовый язык”. В этом документе реализация используется для демонстрации возможности спецификации, хотя ее цель не столько в том, чтобы служить академическим инструментом, но в тоМ, чтобы стать полный и ясным языком, который может использоваться для разных целей.
CX, используемый в этом документе, имеет синтаксис, аналогичный синтаксису Go, насколько это возможно.
Доступности
Программисту необходимо принять множество решений при написании программы, это, среди прочего, сколько параметров должна получить функция, сколько параметров надо вернуть, какие вычисления необходимы для получения желаемой функциональности, и какие аргументы должны быть отправлены как параметры для функций. Система доступностей в CX может быть запрошена для получения списка возможных действий, которые могут быть применены к элементу. В этом контексте примеры элементов - это функции, структуры, модули и выражения.
Не имея набора правил и фактов, которые определяют логику и цели программы, можно, по крайней мере, определить некоторые основные ограничения, которые гарантируют, что программа будет семантически корректна. Система доступностей предоставляет такие ограничения как фильтр первого уровня, что будет пояснено ниже.
Ограничения арности
Выражения в CX могут возвращать несколько значений. Это создает проблему для системы доступности, так как количество переменных, получающих аргументы на выходе выражения должны соответствовать числу выходных данных, определенных оператором выражения.
out1, out2, ..., outN := op(inp1, inp2, ..., inpM)
Если приведенный выше пример верен, то oр необходимо вывести N аргументов. Эта проблема может стать еще более сложной, если мы считаем, что op может быть в будущем переопределено системой доступностей самостоятельно или пользователем: как только op будет переопределено, новые возможности могут быть применены к любому выражению, который использует op в качестве своего оператора, поэтому число переменных, которые получают операционные аргументы, теперь не согласовано.
В предыдущей логике также подразумевается, что если число принимающих переменных соответствуют количеству выходных параметров оператора выражения, операция добавления новых принимающих переменных больше не может быть совершена.
Ограничения арности также применяются к входным аргументам в выражениях, то есть если вызов функции уже содержит все свои входные аргументы, то система доступности не должна включать в список дополнительные аргументы как возможное действие. Аналогично, если выражение пытается вызвать оператора с меньшим количеством аргументов, чем требуется, система доступности должна при запросе сказать программисту, что добавление нового аргумента при вызове функции возможно.
Пример:
- Примечание: конкатенация строк еще не реализована. Так же функции печати всегда добавляют новую строку в конце выводимой строки. Будущая версия CX решит эти проблемы. *
var age i32 = 18
var steps i32 = 23
func advance (direction str, numberSteps i32) () {
printStr("Advancing:")
printStr(direction)
printStr("Number of steps:")
printI32(numberSteps)
}
func main () () {
advance("North")
}
В приведенном выше примере в вызове функции advance из функции main не хватает одного аргумента. Если запросить систему доступности, система должена включить в список, помимо прочего, действие, подобное:
...
(k) AddArgument advance age
(k+1) AddArgument advance steps
...
где k - произвольный индекс. Как видно, система доступности сообщает программисту о двух возможных действиях по добавлению аргумента в функцию advance, и глобальные переменные age и steps могут быть выбраны в качестве аргументов.
Следует отметить, что доступности должны быть корректно перечислены, а их порядок является постоянным при разных вызовах система доступности. Причина этого в том, что программист после изучения запроса должен иметь возможность указывать системе, какая доступность должна быть применена.
Ограничения типа
Общим правилом в языках программирования является наличие системы типов, которая ограничивает программиста от отправки аргументов неверного типа при вызове функций. Даже в слабо типизированных языках программирования операция «true / “hello world” » должна вызывать ошибку (кроме, конечно, случая эзотерического языка). В CX очень сильная типизация и аргументы, которые явно не соответствуют ожидаемому типу, не должны рассматриваться в качестве возможного варианта в системе доступностей (хотя обходным путем является обертывание таких аргументов в функции приведения типа, тогда они будут показаны как доступные).
Ограничения типа также должны учитываться при присвоении нового значения уже существующей переменной. В CX при объявлении переменной указывается ее тип, остающийся неизменным в течение всего срока ее жизни (если тип не удален с помощью команд/функций метапрограммирования и не объявлен заново). Таким образом, переменная, объявленная как хранящая 32-битное целое число, не должна рассмотриваться как кандидат на присвоение значения аргумента, содержащего 64-битное число с плавающей точкой.
Ограничения существования
Этот вид ограничения может показаться тривиальным на первый взгляд: если элемент не существует, доступность, которая его включает, тоже не должна существать. Тем не менее, это ограничение становится проблемой, если мы рассматриваем ситуацию, когда функция была переименована, и она уже использовалася как оператор в выражениях программы. Если программа находится в форме исходного кода, эта проблема сводится к простому процессу «поиска и замены», но если проблема проявилась во время исполнения, система доступности становится очень полезной: доступность по изменению идентификатора, связанного с этим оператором.
Даже если элемент не был переименован, определение того, существует элемент или нет, не является тривиальным. Элементы, используемые в доступностях, необходимо искать в текущей области стека вызовов, в глобальной области программы и в глобальных областях других модулей.
Ограничения идентификатора
Добавление новых именованных элементов обычно является кандидатом для доступностей. Ограничение, возникающее при попытке применить такой тип доступности, заключается в обеспечении уникального идентификатора для каждого нового элемента, чтобы избежать случайного переопределения. Система доступности может либо сгенерировать уникальный в области доступа идентификатор для элемента, либо запросить программиста предоставить подходящий идентификатор.
Ограничения границ
CX предоставляет нативные функции для доступа и изменения элементов массивов. Примеры чтения и записи массива:
readI32([]i32{0, 10, 20, 30}, 3)
writeF32([]f32{0.0, 10.10, 20.20}, 1, 5.5)
В первом выражении считывается элемент с индексом 3 массива из четырех 32-битных целых чисел, операция возвращает последний элемент массива. Во втором выражении второй элемент массива из трех 32-битных чисел с плавающей точкой изменен на 5.5. Если к какому-либо из этих массивов обратиться за элементом с отрицательным индексом или индексом, превышающим длину массива, будет получена ошибка «вне границ».
Повинуясь только ограничениям типа, система доступностей сообщит программисту, что любой 32-разрядный целочисленный аргумент может использоваться как индекс для доступа к массиву. Хотя эти программы будут компилироваться, ошибка границ весьма вероятна, если программист не обратит особое внимание на то, что выбирает для применения.
Система доступности должна фильтровать допущения в соответствии со следующими критериями: отбросить любое отрицательное 32-битное целое число и отбросить любое 32-битное число, которое превышает длину массива, отправляемого как индекс для считывания или записи элементов массива.
Пользовательские ограничения
Система ограничений, устанавливаемых пользователем, еще находится в экспериментальной стадии.
Основные ограничения, описанные выше, должны гарантировать, что программа не сталкивается с ошибками во время выполнения. Этих ограничений должно быть достаточно для создания некоторых интересных систем, таких как нативный эволюционный алгоритм СХ. тем не менее, в некоторых ситуациях требуется более надежная система. Для этой цели оговорки, запросы и объекты используются для описания окружения модуля. Эти элементы определяются с помощью интегрированного Prolog интерпретатора и нативных функций CX setClauses, setQuery и addObject.
Наиболее общим описанием этой системы ограничения является то, что программист определяет серию оговорок Prolog (факты и правила), которые будет запрашиваться с использованием определенного запроса Prolog для каждого из добавленных объектов. Это вряд ли имеет смысл для тех, кто читает это в первый раз. Пример может разъясненить конценпцию и процесс несколько лучше:
setClauses("move(robot, north, X, R) :- X = northWall, R = false.")
setQuery("move(robot, %s, %s, R).")
В этом примере оговорено только одно правило. Правило может быть грубо интерпретировано как «если робот хочет двигаться на север, спросите, какое X. Если X это northWall, то он не может двигаться». Запрос - это просто форматированная строка, которая будет служить в качестве запроса для действия move и для элемента robot, который получит еще два аргумента: направление и объект.
Объекты могут быть определены с помощью функции addObject:
addObject("southWall")
addObject("northWall")
Система ограничения запросит систему для каждого из объектов, присутствующих в модуле. В этом примере система сначала выполнит запрос «move (robot, north, southWall)», и система ответит «nil», что означает, что у него нет правила, определенного для обработки подобной ситуация, а действие по умолчанию - не отвергать доступность. Второй запрос будет «move (robot, north, northWall)», и система ответит «false». В этом случае доступность не проходят тест и отвергается.
В приведенном примере показано, как правила могут отвергать доступность, используя условия. Но правила могут также использоваться для принятия доступностей даже после того, как были отменены предыдущие правила.
setClauses("move(robot, north, X, R) :- X = northWall, R = false.
move(robot, north, X, R) :- X = northWormhole, R = true.")
setQuery("move(robot, %s, %s, R).")
Добавленное в код правило сообщает системе о допустимости движения робота
к северу, если имеется червоточина. Если массив объектов остается прежним,
доступность движения будет по-прежнему отвергаться, но если выполнить
addObject ("NorthWormhole"), то чревоточина будет добавлена и робот
сможет пройти через стену, используя червоточину.
Сильная типизация
Как упоминалось во Введении, в CX нет автоматического приведения типов. Из-за этого в модуле ядра определены несколько версий для каждого из примитивных типов. Например, существуют четыре собственные функции для добавления: addI32, addI64, addF32 и addF64.
Парсер привязывает тип по умолчанию к данным, которые он находит
в исходном коде: если считано целое число, его тип по умолчанию - i32
или 32-битное целое число; и если считывается число с плавающей точкой,
его тип по умолчанию - f32 или 32-битное число с плавающей точкой.
Нет никакой двусмысленности в отношении других данных, считываемых парсером:
true и false всегда логический тип; серия символов, заключенных
между двойными кавычками, всегда являются строкой; у массива
должен быть указан тип данных перед списком элементов, например:
[]i64{1, 2, 3}.
В тех случаях, когда программисту необходимо явно привести данные
одного типа к другому, ядро обеспечивает ряд функций приведения для работы
с примитивными типами. Например, byteAToStr приведет массив байтов
к строке, а i32ToF32 приводит 32-битное целое число к 32-битовому числу
с плавающей точкой.
Компиляция и интерпретация
Спецификация CX обеспечивает дилекты CX для обеспечения разработчика как интерпретатором, так и компилятором. Интерпретированная программа намного медленнее, чем ее скомпилированный аналог, как ожидается, но при этом можно сделать более гибкую программу. Эта гибкость обеспечивается метапрограммными функциями и возможностями, которые могут изменять структуру программы во время выполнения.
Скомпилированная программа нуждается в более жесткой структуре, чем интерпретированная, так как многие из оптимизаций используют эту жесткость. Как следствие, система доступности и любая функция, которая воздействует на структуру программы, будет ограничена функциональностью в скомпилированной программе.
Компилятор должен использоваться, когда производительность наиболее важна, в то время как программа должна оставаться интерпретируемой, пока программисту требуется вся гибкость, предоставляемая функциями CX. В следующих подразделах некоторые из этих функций будут представлены в ознакомительных целях, не в качестве учебника.
Цикл Read-Eval-Print
Цикл read-eval-print (REPL) - это интерактивный инструмент, с помощью которого программист может вводить новые программные элементы и изменять их. На старте нового сеанса REPL выведет на консоль следующие сообщения:
CX REPL
More information about CX is available at https://github.com/skycoin/cx
*
«*» сообщает программисту, что REPL готов принять новую строку кода. REPL продолжит чтение вводимых данных от пользователя до тех пор, пока не встретит точку с запятой или символ перевода строки.
Если программа не была первоначально загружена в REPL, CX начнет с
пустой программы. Это можно увидеть, если команда
метапрограммирования : dProgram true; указана как начальная:
* :dProgram true;
Program
*
REPL только печатает слово «Program», за которым следует пустая строка. В качестве первого шага можно объявить новый модуль и функцию.
Объявлены новый модуль main и новая функция main:
* package main;
Program
0.- Module: main
* func main () () {};
Program
0.- Module: main
Functions
0.- Function: main () ()
*
Как видно, структура программы выводится каждый раз, когда новый элемент добавляется в программу.
Команды метапрограммирования
:dProgram был упомянут в подразделе выше. Любое утверждение,
которое начинается с двоеточия (:), является частью категории
“инструкции”, известные как «команды метапрограммирования».
Объявление элементов в REPL дает указание CX добавить их в структуру программы. Но, как и во многих других языках программирования, эти декларации ограничиваются только добавлением, и в лучшем случае могут быть переопределены.
Команды метапрограммирования позволяют программисту намного лучше управлять изменениями структуры программмы.
:dProgram, :dState, and :dStack используются только для отладки:
для вывода структуры программы, текущего состояние вызова и полного
стека вызовов для пользователя соответственно.
: step дает инструкцию интерпретатору идти вперед или назад при исполнении.
: package,: func и : struct, называемые селекторы,
используются для изменения области действия программы.
: rem дает программисту доступ к ремуверам и может использоваться
для выборочного удаления элементов из структуры программы.
: aff используется для доступа к системе доступности CX; эта команда
метапрограммирования используется для запросов и применения доступностей
к различным элементам программы.
: clauses используется для установки оговорок модуля, которые будут
использоваться в системе пользовательских ограничений;
: object и: objects используются для добавления и вывода объектов;
: query используется для установки модуля запроса;
: dQuery помощник для отладки пользовательских ограничений.
Степпинг
Программа, запущенная в режиме REPL, может быть инициализирована со структурой, определенной в исходном файле. Например:
Текущий каталог
$ ./cx --load examples/looping.cx
Загружаем looping.cx из каталога примеров (полный список
примеров можно найти в репозитории проекта).
Хотя программа была загружена, она еще не выполнена. В REPL для
выполнения программы необходимо использовать команду метапрограммирования
: step. Чтобы запустить программу до конца, необходимо использовать : step 0;.
Но : step интересен тем, что он может принимать другие целые числа в качестве
аргументов (даже отрицательные целые числа). Например:
CX REPL
More information about CX is available at https://github.com/skycoin/cx
* :dStack false;
* :step 5;
0
* :step 5;
1
* :step 5;
2
*
Программа examples/looping.cx выполняет 5 шагов за раз. Как мы видимь, требуется 5 шагов для того, чтобы программа переоценила условие while, вывела счетчик и добавила 1 к счетчику.
Аналогично, мы должны “вернуться назад во времени”, если REPL
получает инструкцию :step -5.
...
* :step 5;
2
* :step -5;
* :step 5;
2
*
После того, как CX выполнит 5 шагов, 2 снова выведено на консоль. Следует отметить, что счетчик не просто получает другое значение. Cтек вызовов возвращается в предыдущее состояние.
Интерактивная отладка
Программа CX войдет в режим REPL после нахождения ошибки. Такое поведение дает программисту возможность отладить программу перед тем как попытаться возобновить ее выполнение.
В приведенном ниже примере появляется ошибка деления на 0, REPL предупреждает программиста об ошибке, последний вызов в стеке вызовов сбрасывается и REPL продолжает выполнение.
CX REPL
More information about CX is available at https://github.com/skycoin/cx
* package main;
* func main () () {};
* :func main;
main
:func main {...
* foo := divI32(5, 3);
main
:func main {...
* bar := divI32(10, 0);
main
:func main {...
* :step 0;
fn:main ln:0, locals:
>> 1
fn:main ln:1, locals: foo: 1
Call's State:
foo: 1
divI32() Arguments:
0: 10
1: 0
0: divI32: Division by 0
main
:func main {...
*
Аналогично, если программа передается на вход интерпретатору CX без вызова REPL, но возникает ошибка, REPL отправит запрос программисту или системному администратору об отладке программы:
$ ./cx examples/program-halt.cx
1
Call's State:
nonAssign_0: 1
nonAssign_1: 1
divI32() Arguments:
0: 5
1: 0
5: divI32: Division by 0
CX REPL
More information about CX is available at https://github.com/skycoin/cx
*
Встроенный эволюционный алгоритм
Система доступностей и функции метапрограммирования в CX позволяют легко и гибко измененять структуру программы. Однако доступности могут быть автоматизированы путем использования функций, выбирающих индекс доступности для применения.
evolve - это нативная функция, которая создает пользовательские функции,
используя случайные доступности. Итерационный процесс используется для тестирования
evolve, следуя принципам эволюционных вычислений. В частности,
эволюция реализует подход, называемый генетическим программированием.
Генетическое программирование пытается найти комбинацию
операторов и аргументы, которые решают проблему. Например, вы может дать
указание evolve найти комбинацию операторов, которая при отправке
10 аргументов возвращает 20. Это может показаться тривиальным, но
генетическое программирование и другие эволюционные алгоритмы могут
решать сложные проблемы.
В репозитории в каталоге examples можно найти пример (examples/evolving-a-function.cx), который описывает пример функции аппроксимации кривой.
Сериализация
Программа в CX может быть частично или полностью сериализована в байтовый массив. Эта возможность сериализации позволяет программе создавать образ программы (аналогично системным изображениям), с поддержкой точного состояния, в котором программа была сериализована. Это означает, что сериализованная программа может быть десериализована, чтобы позже возобновить исполнение. Сериализация также может быть использована для создания резервных копий.
Программа CX может использовать свои интегрированные функции для создания некоторых интересных сценариев. Например, программа может быть сериализована для создания резервной копии самой себя и запустить эволюционный алгоритм на одной из копий. Если эволюционный алгоритм найдет функцию, которая работает лучше, чем предыдущая, можно сохранить эту новую версию программы. Однако, если эволюционный алгоритм выполняется плохо, программа может быть восстановлена из сохраненной резервной копии. Все эти задачи могут быть автоматизированы.