Релиз языка программрования NewLang
Внимание!!!
Данная статья содержит описание синтаксиса NewLnag предыдущей версии.
Актуальную версию синтаксиса языка можно посмотреть тут.
NewLang - это язык программирования высокого уровня, в котором можно сочетать стандартные алгоритмические конструкции с декларативным программированием и тензорными вычислениями для задач машинного обучения.
Основной особенностью языка является простой, логичный и не противоречивый синтаксис, который основан не на использовании зарезервированных ключевых слов, а на строгой системе грамматических правил с использованием знаков препинания (в список которых входят и операторы языка).
Новое по сравнению с прошлым выпуском:
- Реализованы все основные алгоритмические конструкции: проверка условий, различные варианты циклов, сравнение по образцу, прерывание последовательности команд и возврат данных, обработка ошибок и т.д.
- Переработан основной синтаксис для отдельных языковых конструкций.
- Реализованы большинство операций с переменными, включая операторы раскрытие списков и тензоров.
- Серьезно переработана система встроенных типов, реализованы функции для их преобразования и для определения данных (последнее можно назвать data comprehensions).
- Добавлены новые тесты и пр.
Зачем нужен NewLang?
У всех современных языков программирования происходит постоянное развитие (читай усложнение) синтаксиса по мере выхода новых версий. Это является своего рода, платой за появление новых возможностей и воспринимается пользователями как естественное явление.
Но одновременно является и серьезной проблемой, т.к. с выходом новых версий добавляются новые ключевые слова и синтаксические конструкции, что неизбежно повышает порог входа для новых пользователей. Еще одним следствием этого процесса становится постоянное повышение сложности разработки и трудоемкости поддержки уже созданных программных продуктов, когда старый код дорабатывается с применением уже новых стандартов.
У NewLang сложность языковых конструкций естественно ограничена за счет разделения синтаксиса языка на две части, что упрощает его изучение и использование.
Основной синтаксис — для написания программ в объектно-ориентированном (императивном) и декларативном стилях, который основан не на зарезервированных ключевых словах, а на строгих грамматических правилах и Расширенный синтаксис — когда основного синтаксиса становится недостаточно, или требуется использовать языковую конструкцию языка реализации.
Еще одно неудобство современных мейнстримовых языков, большинство из них были созданы до начала эпохи машинного обучения, поэтому тензорные вычисления у них выполнены в виде отдельных библиотек, а не встроены в основной синтаксис языка и систему базовых типов.
У NewLang тензорные вычисления доступны «из коробки» (используется библиотека libtorch), а обычные числа являются скалярами (тензорами нулевой размерности).
Синтаксис NewLang
При разработке синтаксиса я старался придерживаться уже сложившихся в IT индустрии правил, чтобы не генерировать множественных смыслов, которые будут зависеть от контекста.
Основы
- Операторы разделяются точкой с запятой «;».
- Отступы и переводы строк игнорируются (очень хотелось иметь возможность автоматического форматирование кода).
- Многострочные комментарии в исходном коде соответствуют стилю С/С++ и должны располагаться между символами /* и */. Вложенность многострочных комментариев не поддерживается.
- Однострочные комментарии начинаются с символа «#» до перевода строки, что соответствует комментариям в стиле Python и Bash.
- Последовательность команд, которая должна выполняться как единое целое, заключается в фигурные скобки «{}».
- Программные вставки расширенного синтаксиса на языке реализации заключается в фигурные скобки со знаком процента %{ /* тут может быть любой код на C/C++*/ %}, как в лексерах lex и flex.
Создания объектов и присвоения новых значений
Для создания объектов и присвоения им новых значений в NewLang используется сразу три разных оператора:
- ::= используется только для создания новых объектов, а если объект с таким именем уже существует, то генерируется ошибка.
- := используется для тех же целей, но если объект с таким именем уже существует, то ошибки не происходит, а новое значение присваивается уже существующему объекту.
- = применяется только для присвоения значения уже существующим объектам, и если объект с указанным именем отсутствует, то тоже происходит ошибка выполнения.
Использование трех разных операторов для создания/изменения объектов позволяет более гибко контролировать подобные операции и выявлять логические ошибки в коде на более раннем этапе.
var ::= 1.0; # Создать новую переменную var без указания типа
var = 100; # Присвоить новое значение уже существующей переменной
printf := @import('printf(format:Format, ...):Int'); /* Создать новый или переопределить объект printf, который будет результатом выполнения глобальной функции @import */
Идентификаторы объектов и модификаторы
В качестве идентификаторов можно использовать буквы, цифры и знаки подчеркивания в любых комбинациях, при условии, что первый символ идентификатора не является цифрой.
В NewLang существует возможность указания области видимости и времени жизни объекта с помощью модификатора — специального символа перед именем переменной. Это может показаться немного похожим на венгерскую нотацию, но в отличие от нее, модификатор не имеет отношения к типу объекта и не является частью имени идентификатора. К тому же в качестве модификаторов используется строго определённые символы, назначение которых определено заранее.
- $ - в начале имени обозначает локальную переменную, время жизни которой ограничено текущей областью видимости и при её завершении локальная переменная уничтожается.
- @ - обозначает глобальную переменную, а сам объект сохраняет свое состояние даже после выхода из текущей области видимости.
- : - двоеточие вначале имени используется в качестве модификатора для указания типа.
Семантика обращения к аргументам функций очень похоже на работу с аргументами в bash скриптах, где $1 или $arg — порядковый номер или имя аргумента (происходит обращение к локальным переменным в текущей области видимости).
Использование модификаторов является обязательным только в двух случаях:
- При создании нового типа данных, так как типы всегда создаются в глобальной области видимости, а их символьные имена должны быть уникальными.
- При обращении к объектам NewLang внутри программных вставок кода на языке реализации, так как они используется как маркеры при поиске идентификаторов NewLang в коде С/С++.
В остальных случаях, для обращения к переменным указывать их модификаторы необязательно. И если при обращении к объекту модификатор не указан, то сперва ищется локальная переменная, а потом глобальная с таким же именем. Причем, локальная переменная будет перекрывать глобальную.
Так же следует иметь в виду, что компилятор может генерировать код для прямого обращения к локальным объектам уже на этапе компиляции, тогда как для обращения к глобальным объектам, или если модификатор области видимости отсутствует, компилятор вынужден каждый раз встраивать runtime вызов функции поиска объекта в глобальной таблице символов.
Система типов
Так как система типов языка динамическая, то явное указание типа не влияет на размер переменной и является только своего рода логическим ограничением на возможность присвоения переменной значения другого типа.
Информация о типах используется при проверке их совместимости, когда существующему объекту присваивается значение другого типа. Такая операция возможна только когда типы совместимы между собой и допускают автоматическое приведение. Это справедливо как во время парсинга/компиляции исходного теста, так и во время выполнения в режимах интерпретатора и/или скомпилированного файла.
Простые типы
Арифметические значения
Арифметические типы данных являются тензорами — массивами чисел одного типа с произвольным количеством измерений и одинаковым размером столбцов в каждом. Единичное число тоже тензор нулевого размера.
Поддерживаются только знаковые целые числа, т.к. в беззнаковых числах особая нужда отсутствует, а проблем с ними можно найти очень много на ровном месте.
Проблемы беззнаковых чисел (из интернета):
Во-первых, вычитание двух беззнаковых чисел, например 3 и 5. 3 минус 5 равно 4294967294, т.к. -2 не может быть представлено как беззнаковое число. Во-вторых, непредвиденное поведение может возникнуть при смешивании целочисленных значений со знаком и без знака. С++ может свободно преобразовывать числа со знаком и без знака, но не проверяет диапазон, чтобы убедиться, что вы не переполняете свой тип данных. В C++ всё же есть несколько случаев, когда можно (или необходимо) использовать беззнаковые числа. Во-первых, числа без знака предпочтительнее при работе с битами. Во-вторых, использование беззнаковых чисел связанных с индексацией массивов.
Но это не мой случай, так как индекс может быть отрицательным и даже не числом, а диапазоном или многоточием.
З.Ы. И даже зная об этом, все равно умудрился недавно словить баг с отрицательными индексами у словарей!
Имена встроенных арифметических типов говорят сами за себя: Char, Short, Int, Long, Float, Double, ComplexFloat, ComplexDouble. Отдельным типом идет логический тип Bool, который может принимать значения только 0 или 1 (false/true соответственно), и в зависимости от выполняемой операции может быть отнесен к целочисленным типам, так и не входить в их состав (данный подход интерпретации логического типа данных был взят из библиотеки Torch).
// Treat bool as a distinct "category," to be consistent with type promotion
// rules (e.g. `bool_tensor + 5 -> int64_tensor`). If `5` was in the same
// category as `bool_tensor`, we would not promote. Differing categories
// implies `bool_tensor += 5` is disallowed.
//
// NB: numpy distinguishes "unsigned" as a category to get the desired
// `bool_tensor + 5 -> int64_tensor` behavior. We don't, because:
// * We don't want the performance hit of checking the runtime sign of Scalars.
// * `uint8_tensor + 5 -> int64_tensor` would be undesirable.
В будущем планируется добавить классы чисел для длинной арифметики и дробей, для чего зарезервированы названия типов BigNum, Currency и Fraction.
Доступ к элементам тензора происходит по целочисленному индексу, который начинается с 0. Для многомерного тензора, индексы элемента перечисляются в квадратных скобках через запятую. Поддерживается доступ к элементам через отрицательный индекс, который обрабатывается точно так же, как в Python (-1 последний элемент, -2 предпоследний и т.д.).
Литерал тензор в тексте программы записывается в квадратных скобках с обязательной завершающей запятой, т.е. [1, 2,] — это литерал одномерный тензор из двух чисел. После закрывающей скобки тип тензора может быть указан в явном виде. Если тип не указан, то он выводится автоматически на основании указанных данных и выбирается минимально возможный байтовый размер, который позволяет сохранить все значения без потери точности.
Примеры:
$var_char := 123; # Тип Char выводится автоматически
$var_short := 1000; # Тип Short выводится автоматически
$var_bool := [0, 1, 0, 1,]; # Тензор из 4 элементов. Тип Bool выводится автоматически
$tensor[10,10]:Int := 1; # Тензор Int размером 2x2 инициализированный 1
$scalar := $tensor[5,5]; # Присвоить скаляру значение указанного элемента тензора
Строки
Поддерживаются два типа строк, StrWide — символьные (широкие) и StrChar — байтовые. Различия между ними заключается в типе единичного элемента. У символьных строк единичным элементом является широкий символ wchar_t, а у байтовой строки единичным элементом является один байт (точнее char, т.е. байт со знаком). Символьные строки литералы в исходном тексте записывается в «двойных кавычках», а байтовые строки в ‘одинарных кавычках’.
Количество элементов символьной строки возвращается в широких символах, а размер байтовой строки в байтах, поэтому и обращение к элементу строки по индексу происходит соответственно либо к символу, либо к байту.
Важный момент. К любой переменной можно обратиться так же, как к функции (записав после её имени круглые скобки). Результатом этой операции будет создание копии/клона объекта. Причем некоторые типы (словари, классы и символьные строки) можно использовать в качестве шаблона при создании копии объекта с модифицированными свойствами, если новые и/или изменяемые значения указать в скобках, как аргументы при вызовах функций. Так, если при создании копии в скобках указать набор новых данных, то результирующая копия будет содержать уже измененные данные.
Например:
$template := "${name} $1"; # Обычная строка
$result := $template("шаблон", name = "Строка"); # result = "Строка шаблон"
Системные
:Pointer - указатель на системную область памяти
Так как любой программе приходится взаимодействовать с внешним миром, то по неволе приходится закладывать возможность использования других библиотек и системы типов данных, и для этих целей служит тип Pointer. Он создается при импорте функций из внешних библиотек и вручную его создать нельзя. Но можно вывести его значение, например для отладки.
:Plain - указатель на представление данных в бинарном виде
Для взаимодействия с внешними библиотеками требуется еще и обмен данными. И для этих целей служит тип данных Plain - который также является указателем, но на двоичное представление данных в единой области памяти. Конечно, если их можно представить в виде единого фрагмента.
Составные типы данных:
Словарь
Словарь (:Dictionary) — набор данных произвольного типа с доступом к отдельным элементам по целочисленному индексу или по имени элемента при его наличии (он похож и на tuple и на структуру одновременно). Словари от тензоров отличаются тем, что являются только одномерными массивами, но каждый элемент может содержать произвольное количество элементов любого типа, в том числе и другие словари.
Доступ к элементам словарей происходит по имени элемента, которое записывается через точку от имени переменной, либо по целочисленному индексу. Индекс также начинается с 0 и как у тензоров и тоже может быть отрицательным.
Литерал с типом «словарь» в тексте программы записывается в круглых скобках с обязательной завершающей запятой, т. е. (,) — пустой словарь, (1, 2= «2», name=3,).
Перечисление, структура и объединение
:Enum, :Struct и :Union — это такие же словари, только на их элементы накладываются определённые ограничения. Каждый элемент должен иметь уникальное имя, а его тип данных должен быть простым, т.е. числом или строкой фиксированного размера.
Классы
Класс (реализован частично) — тип данных, с помощью которого реализуется один из принципов ООП — наследование. При создании экземпляра класса создается новая переменная, у которой сохраняется информацию о своем родителе и которая наследует от него свойства и методы. Тип данных :Class аналогичен словарю, но все свойства обязаны иметь имена (хотя доступ к свойствам класса по индексу так же возможен).
Функции
Синтаксис NewLang поддерживать несколько типов функций (а в будущем и методов классов): обычные функции, чистые функции и простые чистые функции.
Для всех типов функций поддерживаются аргументы по умолчанию. При создании функции, её аргументы указываются как в Python, т.е. вначале идут обязательные аргументы, потом аргументы со значениями по умолчанию, где имя аргумента отделяется от его значения по умолчанию знаком равно =. Если функция допускает обработку произвольного количества аргументов, то последним в списке параметров указывается многоточие … (три точки подряд) .
Обычная функция
Обычная функция — такие функции являются именно обычными функциями в понимании С/С++. Внутри них можно писать совершенно любой код, включая проверки условий, циклы, вызовы других функций и т.д.
Внутри обычной функции можно обращаться к локальным и глобальным объектам, и они могут содержаться вставки на языке реализации*, например, для вызова функций из внешних библиотек.
Вставки на языке реализации оформляются в виде %{ %} и могут содержать любой текст на С/С++, а прямо из него можно обращаться к локальным и глобальным объектам NewLang так же, как и в обычном синтаксисе, указывая первым символом имени соответствующий модификатор ($ для локальных объектов и @ для глобальных).
Технически, такая программная вставка просто переносится трансплайтером непосредственно в исходный текст генерируемого файла, а все идентификаторы NewLang специальным образом декорируются (добавляются специальные маркеры для их идентификации), после этого исходный текст подается на вход обычному компилятору С++. Для локальных объектов трансплайтер может генерировать код для прямого доступа к объекту на этапе компиляции, а для работы с глобальными объектами вынужден использовать runtime вызовы функции поиска в таблице символов.
*) - Программные вставки на языке реализации обрабатываются только во время компиляции
Например:
print(str) := {
%{
printf("%s", static_cast<const char *>($str)); /* Прямой вызов С функции */
%}
};
Чистые функции
Чистая функция — это тоже обычная функция, только в том смысле, какой в него вкладывает функциональное программирование. Создания чистой функции происходит с помощью оператора :-, а сам оператор заимствован из языка Пролог. У чистой функции отсутствует доступ к контексту и глобальным переменным, поэтому она может обрабатывать только те данные, которые были ей переданы в качестве аргументов.
Программные вставки на языке реализации внутри чистых функций не запрещены и могут использоваться, например, для отладки. Но делается это на страх и риск разработчика. Именно он отвечает за их «чистоту», например при вызове функций из внешних библиотек.
Sum(arg1, arg2) :- {$arg1+$arg2;}; # Вернуть сумму аргументов
Простые чистые функции
Простые чистые функции — отдельный класс чистых функций, которые предназначены только для вычисления логического результата (т.е. они являются предикатами) и их отличает упрощенная формой записи. Тело простой чистой функции состоит из последовательности операторов, которые разделяются запятыми и заканчиваются, как и любое выражение, точкой с запятой. Все операторы простой чистой функции всегда приводятся к булевому значению, а итоговый результат функции вычисляется по одной из возможных логических операций: И, ИЛИ и исключающее ИЛИ.
Например:
func_and(arg1, arg2) :&&= arg1==3, arg2 > 0; # Простая чистая функция Логическое И
func_or(arg1, arg2) :||= arg1==3, arg2 > 0; # Простая чистая функция Логическое ИЛИ
func_xor(arg1, arg2) :^^= arg1==3, arg2 > 0; # Простая чистая функция Исключающее ИЛИ
Специальные типы данных:
Пусто (:None)
:None (пусто) — не содержит значения (точнее имеет одно значение None) и совместим с любым другим типом данных. Указывается в тексте программы как один подчерк «_». None имеют не инициализированные переменные и при попытке чтения из такой переменной возникает ошибка.
Тип переменной может быть явно указан или выведен автоматически из присваиваемого значения. Присвоить новое значение уже инициализированной переменной можно только для совместимого типа, так как неявное преобразование типов не допускаются.
$var := _; # Создать не инициализированную переменную
$var2 := var; # Ошибка!!! Нельзя прочитать неинициализированную переменную var
$var = 1000; # У переменной будет тип Short (минимальный размер для хранения значения)
$var = 0,5; # Ошибка!!! Short ← Float не совместимы
$var = _; # Очистить значение переменной
$var = 0,5; # Теперь можно, т. к. None совместим с любым типом
Диапазон (:Range)
Диапазон — специальный тип данных, являющейся приблизительным аналогом типа «генератор» в Python. К диапазону можно обращаться как к итератору и он будет поочередно выдавать элементы в указанном интервале с заданным шагом. Диапазон в тексте программы указывается как два или три элемента через две точки, например 1..5 — диапазон от единицы до пяти с шагом по умолчанию 1. В качестве параметров диапазона можно указывать не только литералы, но и имена переменных. Например, 0,1..$stop..0,1 — диапазон от 0,1 до значения, указанного в переменной $stop с шагом 0,1.
Диапазон для целых чисел можно использовать в качестве индекса у тензоров (точнее, у любых объектов, которые допускают доступ к своим элементам по индексу, т.е. тензоры, словари и текстовые строки). Фактический, это поведение аналогично slice в языке Python и array[1:5] в Python означает тоже самое, что и array[1..5] в NewLang.
В качестве индекса у тензоров еще можно указать произвольное количество измерений с помощью многоточия, т.е.
$tensor[…, 0] = 0; # Обнулить все первые элементы в каждом измерении.
Преобразование типов
Явное приведение типов
Несмотря на динамическую типизацию языка, если тип переменной указан явно, то автоматическое приведение типов не выполняется, и чтобы присвоить переменой значение не совместимого типа, требуется явное преобразование.
Так как символьные названия типов относятся к деталям реализации, то явное преобразование в конкретный тип данных производится с помощью вызова функции с именем типа, т.е. :Bool(), :StrWide(), :Long() и т.д.
Для преобразования любого типа данных в строку ещё можно использовать оператор конкатенации строк, которой преобразует любой тип данных в строковое представление. Но, так как строковых типов два (байтовые и широкие строки), то тип строки определяется первым аргументом в операторе конкатенации/сцепления ++. Также преобразовать любое значение в строковое можно с помощью строки-шаблона.
"" ++ 123; # "123" - Строка широких символов
'' ++ 123; # '123' - Байтовая строка
val := 12345; # Число
"$1"(val); # Будет строка "12345"
Так как тензоры могут иметь больше одного значения, то и в качестве аргументов могут принимать их произвольное количество, а итоговым результатом будет тензор, в котором все переданные данные преобразованы к требуемому типу автоматически.
Примеры:
> tstr := :Tensor("Тест"); # Создать тензор из строки широких символов
[1058, 1077, 1089, 1090,]:Int
> t2 := :Tensor[2,2]("Тест"); # Тоже самое, но тензор указанной размерности
[
[1058, 1077,], [1089, 1090,],
]:Int
> :StrWide(tstr) # Создать символьную строку из тензора
Тест
> :Double(t2) # Изменить тип данных тезора без изменения размерности
[
[1058, 1077,], [1089, 1090,],
]:Double
> t3 := :Char[4]( t2 ) # Изменить размерность тензора и его тип (в данном случае с частичной потерей данных)
[34, 53, 65, 66,]:Char
>:Tensor( (1,2,3,) ); # Тензор из словаря
[1, 2, 3,]:Char
>:Tensor( 'first second' ) # Байтовая строка в тензор
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Char
>:Tensor( (first='first', space=32, second='second',) ) # Получаем тензор из словаря с такими же данными
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Char
>:Double[10,2]( 0, ...) # Тензор заданного формата с нулями, где многоточие повторяет последние указанные данные до получения тензора требуемого размера
[
[0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,],
]:Double
>:Int[3,2]( ... rand() ...) # Тензор со случайными данными, где между многоточиями указана функция, которую следует вызывать каждый раз при получении нового элемента тензора
# Пришлось придумывать новую конструкцию, т.к. многоточие перед именем, это оператор раскрытия словаря, а многоточие после имени, это повторение последнего значения до конца заданной размерности.
[
[1804289383, 846930886,], [1681692777, 1714636915,], [1957747793, 424238335,],
]:Int
>:Int[5,2]( 0..10 ); # Создание тензора из диапазона
[
[0, 1,], [2, 3,], [4, 5,], [6, 7,], [8, 9,],
]:Int
>:Tensor( 0..0.99..0.1 ); # Или даже так
[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,]:Double
Операторы и управляющие конструкции
Операторы:
Все операторы имеют парный аналог с присвоением значения:
- + и += - сложение арифметических типов данных;
- - и -= - вычитание арифметических типов данных;
- / и /= - деление (результат число с плавающей точкой);
- // и //= - целочисленное деление с округлением к меньшему числу (как в Python);
- * и *= - умножение;
- ** и **= - возведение в степень (он же используется и для повторения текстовых строк);
- ++ и ++= - конкатенация строк с автоматическим приведением аргументов к стоковому типу (символ инкремента специально используется вместо одиночного плюса для того, чтобы в явном виде разделить конкатенацию строк и операторы арифметического сложения).
Операторы сравнения:
- <, >, <=, >= - классические для сравнения скаляров
- == и != - операторы сравнения с автоматическим приведением совместимых типов для любых объектов
- === и !== - оператор точного сравнения для любых объектов (автоматического приведения типов не выполняется)
Проверки типа (имени класса объекта):
Для оператора проверки имени класса объекта используется символ тильда ~. Он немного похож на оператор instanceof в Java. Левым операндом должен быть проверяемый объект, а правым — проверяемый тип, который можно указать строкой литералом, переменной строкового типа или именем проверяемого класса непосредственно. Результатом операции будет истина, если правый операнд содержит название класса проверяемого объекта или он присутствует в иерархии наследования.
name := "class"; # Строковая переменная с именем класса
var ~ name;
var ~ :class; # Имя типа
var ~ "class"; # Строка литерал с именем типа
Утиная типизация
Оператор утиной типизации, два символа тильны ~~ — приблизительный аналог функции isinstance() в Python, который для простых типов сравнивает непосредственную совместимость типа левого операнда по отношению к правому. А для словарей и классов в левом операнде проверяется наличие всех имен полей, присутствующих у правого операнда, т.е.:
(field1=«value», field2=2,) ~~ (); # Истина (т. е. левый операнд словарь)
(field1=«value», field2=2,) ~~ (field1=_); # Тоже истина (т. к. поле field1 присутствует у левого операнда)
(field1=«value», field2=2,) ~~ (not_found=_); # Ложь, т.к. поле not_found у левого операнда отсутствует
Строгая утиная типизация ~~~ — для простых типов сравнивается идентичности типов без учета совместимости, а для составных типов происходит сравнение всех свойств с помощью оператора строгого равенства. Для данной операции, пустой тип совместим только с другим пустим типом.
Управляющие конструкции
К управляющим конструкциям языка NewLang относятся условный оператор, два вида циклов, оператор оценки выражения, оператор прерывания последовательности выполнения команд и перехват прерывания. Операторы проверки условий всегда указываются в квадратных скобках, а последовательность команд для выполнения - в фигурных.
Условный оператор
В качестве оператора проверки условия используется синтаксическая конструкция, соответствующая по смыслу термину «следует», т.е. тире и угловая скобка -> или с двумя тире для большей наглядности –>. Такая запись условного оператора очень похожа на математическую и легко объединяется в последовательности для проверки множественных условий вида «else if».
В общем случае условный оператор имеет вид: [ условие ] -> действие;
или [ условие ] -> {действие};
или [ условие1 || условие2 ] --> {действие}, [_] --> {действие иначе};
Для наглядности записанный с отступами:
[ условие1 ] -> { действие1 },
[ условие2 ] -> действие2,
[ условие3 ] -> действие3,
[_] -> {действие_иначе};
Оценка выражения
Синтаксическая конструкция с помощью которой реализуется аналог оператора switch выглядит следующим образом:
[ $var ] ==> {
[1] -> { code }; # Выполнится проверка условия $var == 1
[1, 2] -> { code }; # Выполнится проверка условия ($var == 1 || $var == 2)
[_] -> { code default }; # Ветка условия иначе
};
Причем в качестве оператора для оценки могут быть использован любые имеющиеся операторы сравнения на равенство:
- ==> - проверка на равенство с приведением типов;
- ===> - проверка на точное равенство;
- ~> - проверка типа (имени класса);
- ~~> - утиная типизация;
- ~~~> - строгая утиная типизация.
И если в качестве оператора сравнения использовать оператор утиной типизации, то оценка выражения превращается в классический Pattern Matching:
$value := (f1=1, f2="2",);
[ $value ] ~~~> {
[ (f1=_, ), (f1=_, f2=0, ) ] -> { code }; # Поле f2 отсутствует или число
[(f1=_, f2="",), (f1=_, f2='',)] -> { code }; # Поле f2 строка
[_] -> { code default }; # Код по умолчанию
};
Операторы циклов
Для указания операторов циклов используются управляющие -» или –» (с двумя стрелками по направлению от проверки условия к телу цикла). И в зависимости от расположения условия и тела цикла, он может быть с предусловием (while) или постусловием (do while). Хотя пока эти синтаксические конструкции не “отлежались” и их можно считать временными из-за того, что их легко перепутать с условным оператором, и, возможно, имеет смысл заменить разные операторы цикла одной единственной конструкцией вида: <–> или «-», которая сильнее отличается от оператора следования.
Но в настоящий момент циклы реализованы вот так:
[условие while] ->> {
тело цикла while
};
{
тело цикла do while
} <<-- [условие do while];
Реализация цикла foreach на примере суммирования всех элементов словаря (или одномерного тензора):
summa := 0;
dict := (1,2,3,4,5,);
[ dict ] -->> { # Условие цикла, пока есть данные
item, dict := ... dict; # Результат оператора раскрытия словаря - первый его элемент перемещается в item
summa += item; # Вычисление суммы всех элементов словаря
};
Операторы прерывания (возврата)
В качестве оператора прерывания/возврата используется два символа минус –. Оператор позволяет прервать выполнение последовательности команд и/или вернуть данные из текущей функции/блока кода и является самым близким аналогом оператора return и throw одновременно. Для того чтобы вернуть данные, их необходимо указать между двумя операторами прерывания, т.е. --100--; # Вернуть указанное значение
. Если возвращаемое значение не указано явно, то будет возвращено значение None.
Следование (блок кода/лямбда функция)
Алгоритмическая конструкция, которая отвечает последовательное выполнение нескольких команд/операторов и возвращающая результат выполнения последнего из них. Также, результатом выполнения может быть значение, которое возвращается с помощью оператора прерывания (возврата). Это очень похоже на классическую лямбда функцию, только она выполняется сразу во время определения, а в переменную сохраняется уже результат её выполнения.
Следование без перехвата прерывания оформляется в виде последовательности обычных и фигурных скобок, т.е. (){ run code };
или тоже самое, но сохраняет результата выполнения в переменной: $result := (){ run(); code() };
. Но если внутри такой функции будет выполнен оператор прерывания, то она никогда вернет управления и не сохранит возвращаемое значение в переменой $result!
Чтобы перехватывать прерывания, в том числе и возвращаемые значения, необходимо использовать конструкция следования с перехватом прерываний, которая оформляется в виде последовательности обычных и двойных фигурных скобок, т.е. $error := (){{ run(); error();code() }};
. Такая конструкция перехватывает все возвраты и прерывания, если они возникают во время выполнения последовательности команд. В этом случае любой результат будет сохранен в переменной $error как при нормальном завершении, так и в случае возникновения ошибки.
Для более тонкой настройки перехвата прерываний следует использовать типизированную конструкцию, когда в явном виде указывается, какие типы прерываний следует перехватывать. $runtime := (){{ run(); error(); code() }}:ErrorRuntime;
. Такая конструкция вернет результат только в случае успешного завершения (когда с помощью оператора прерывания возвращается не типизированное значение, например, --"Строка"--;
), или при создании прерывания с указанием конкретного типа –:ErrorRuntime(“Описание ошибки”)–;. А вот при возникновении любого другого типизированного прерывания, значение перехвачено не будет и все отработает как самый первый вариант, т.е. без перехвата прерывания и без сохранения возвращаемого значения в переменную.
Стратегия обработки ошибок
Обработка ошибок состоит из комбинации двух элементов: оператора прерывания выполнения с указанием типа возвращаемого значения и алгоритмической конструкции следование с возможностью перехвата прерывания заданного типа.
Это немного отличается от классического варианта обработки исключений, который в обычных языках программирования обычно оформляется ключевыми словами try… catch… finally с различными вариациями. Ведь основная цель подобных синтаксических конструкций — выделить участок кода, где возможно возникновение ошибки, перехватить и обработать правильный тип данных (исключений), т.к. NewLang не делает различий между операторами возврата и генерации исключения.
Подход к обработке исключений следующий:
Программный код, который может привести к ошибке, заключается в двойные фигурные скобки, а результат выполнения такого блока кода присваивается переменной. После этого анализируется возвращенное значение, например, оператором сравнения по образцу:
$result := (){{ # начало аналога блока try
$value := call_or_exception1();
[условие1] -> { -- :Error -- };
[условие2] -> { -- $value -- };
$value := call_or_exception2();
}}; # конец аналога блока try
[$result] ~> { # Для сравнения по образцу использовать оператор проверки типа (имени класса)
[:ErrorParser] -> {Код обработки ошибок парсера};
[:ErrorRunTime] -> {Код обработки ошибок времени выполнения};
[:Error] -> { Код обработки остальных ошибок };
[_] -> { Обработка нормальных данных $value без ошибок };
};
Сборка REPL из исходников (пока только под Linux)
Подготовка репозитория
- Скачать исходники
- Скачать и развернуть архив libtorch в каталоге contrib (PyTorch Build: Stable (1.10.*) -> Your OS: Linux -> Package: LibTorch -> Language: C++ / Java -> Compute Platform: CPU -> Download here (cxx11 ABI): libtorch-cxx11-abi-shared-with-deps-1.10.2+cpu.zip)
- Активировать и скачать исходники субмодулей (
git submodule init && git submodule update
) - В каталоге contrib запустить файл
build.sh
для сборки библиотеки libffi - В каталоге core запустить файл
compile_syntax.sh
для генерации файлов парсера и лексического анализатора. Также может потребоваться установка утилит flex и bison. Если что, у меня установлены flex 2.6.4 и bison (GNU Bison) 3.7.4
Собрать
- Юнит-тесты (newlang_test): в каталоге core выполнить команду
make CONF=UnitTest
* - Интерпретатор (nlc): в каталоге core выполнить команду
make CONF=Debug
*
*) - Сборка проекта выполняется обычной утилитой make, но сборочные файлы генерируются автоматически в давно устаревшей версии NetBeans 8.2, т.к. это единственная универсальная среда разработки с поддержкой Makefile “из коробки”, тогда как в текущей версии Apache NetBeans полноценная поддержка разработки на С/С++ вообще отсутствует. Начал постепенный переход на использование редактора VSCodium (аналога VSCode, в котором вычищена телеметрия от Microsoft) и генерацию скриптов сборки с помощью сmake, но этот процесс пока не завершен.
Планы на будущее
Текущая версия языка значительно обогатилась возможностями, но все еще является тестовой платформой для проверки декларируемых концепций и основного синтаксиса. Если говорить о планах, то в настоящий момент роадмап развития NewLang следующий:
- Реализовать макросы для более привычного использования языка (за счет использования DSL);
- Добавить в арифметические типы длинные числа и дроби;
- Сделать какую-нибудь логическую игру (крестики нолики, судоку или что-то похожее) с алгоритмическим выбором следующего хода и его вычислением с помощью машинного обучения;
- Написать еще больше разных примеров для оценки синтаксиса;
- Зафиксировать синтаксис с учетом полученного опыта и обратной связи;
- Восстановить работоспособность компилятора для генерации исполняемых файлов;
- Сделать очередную большую чистку кода;
- Переработать и задокументировать получившуюся семантику языка с учетом всех возможностей и выпустить первую полнофункциональную версию NewLang.