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.
Комментарии (28)
inetstar
24.06.2022 17:34+7По-моему не хватает нескольких строк о том, какую проблему решает этот новый язык.
rsashka Автор
24.06.2022 18:30На текущем этапе - создать непротиворечивую грамматику языка программирования без использования ключевых слов, которая могла бы легко интегрироваться с основными языками программирования, например, как надстройка над уже существующим синтаксисом. Хотя это я уже писал в начале статьи.
dopusteam
24.06.2022 19:04А глобально какие цели?
rsashka Автор
24.06.2022 19:21+1Давайте не будем бежать далеко вперед и ограничимся хотя бы 3-5 годами.
За это время цель минимум - быть живым и рабочим языком с JIT компилятором и с не менее чем 100 активными пользователями. Максиму, попасть в TIOBE Index :-)
dopusteam
24.06.2022 19:35+7Это не очень хорошая цель. Зачем людям писать на языке, цель которого - быть живым языком?
Обычно язык появляется как решение каких то проблем, а не просто так
rsashka Автор
24.06.2022 19:44+2Сам по себе язык, это просто инструмент, который можно использовать для решения проблем пользователя, но сам он не имеет никакой цели. Я же писал про свою цель, как автор языка. И надеюсь, что NewLang станет удобным инструментом для решения проблем пользователей, у которых программирование не является основной профессией.
DavidNadejdin
25.06.2022 06:25+4Сам по себе язык, это просто инструмент, который можно использовать для решения проблем пользователя, но сам он не имеет никакой цели
Как бы красиво это не звучало, в реальности мы видим ровно обратную картину, с тучей языков(казалось бы зачем, если все так просто). Проблемы и задачи которые решают эти языки, могли сформироваться с течением времени и/или изначально обозначены.
И надеюсь, что NewLang станет удобным инструментом для решения проблем пользователей, у которых программирование не является основной профессией
Эту проблему язык никак не решает, так как она в целом очень странная. Если вы целитесь в совсем не понимающих ничего в программировании людей, то хотелось бы посмотреть как они собрали бы..."Интерпретатор?"...даже по инструкции. Если в тех кто понимает хоть чтото(да даже если не понимает), то JS выглядит куда привлекательнее во всем
alexrodygin
24.06.2022 18:11Используется сборщик мусора? Какая примерно производительность в сравнении с другими языками?
rsashka Автор
24.06.2022 18:24Сборщик мусора в явном виде не используется, т.к. все объекты, это std::shared_ptr из С++, а у них подсчет ссылок ведется автоматически.
Производительность специально не замерял, да она будет очень сильно зависеть от теста т.к. для работы с тензорами используется нативная библиотека libtorch. Парсер синтаксиса очень простой, а и вызовы ядра происходят напрямую без генерации промежуточного кода.
doomguy49
25.06.2022 00:11+4Ответа на вопрос «Зачем?» так и не увидел, но зато увидел «Зачем не надо»:
Синтаксис не прост и не логичен, наоборот, конструкции выглядят ужасно (при том что я знаком с питоном, си, js, ts, php и кучей шаблонизаторов, тут синтаксис хуже всего что я видел);
Нет сообщества;
Надо учить ещё один, достаточно «оригинальный» язык;
Отсутствие готовых решений, библиотек;
Отсутствие коммерческих задач;
-
Отсутствие понимания, какие прикладные задачи решает этот язык;
Новый язык должен решать задачи, которые старый решить не может, либо делать это эффективнее. Значительно, чтобы время на изучение и написание своих решений вместо готовых компенсировалось
auddu_k
25.06.2022 19:33Так в самом начале ссылка на мнение автора про зачем :-)
А по мне, например, если не пробовать создавать новое, то через ХХХ лет ничего нового и не появится
rsashka Автор
25.06.2022 19:38Спасибо за поддержку!
Действительно морально устаешь отвечать на вопросы, ответы на которые подробно расписаны в самой статье.
auddu_k
26.06.2022 15:00+1Вообще идея создать полноценный язык - предельно круто.
Тем не менее вопросов очень много ????♂️
Тут многие говорят про нишу и это действительно важно уже сейчас - если не будет задач, которые удобнее делать так, а не на одном из современных языков, то не образуется сообщество и язык умрет, маркетинг, формирование рынка, никто не отменял - это не придирка, а совет сообщества :-)
Сама по себе идея «синтаксис без ключевых слов» - тоже тот ещё вопрос. Кроме заявленного «усложнения» ключевые слова решают вполне конкретную проблему: облегчает анализ текста. Естественные языки легко читать, но сложно анализировать, чтобы найти нужное место в свободном тексте, нужно весь этот текст прочитать. Чтобы найти нужное в тексте на ЯП, можно пользоваться ключевыми словами, как якорями.
Короче вопросов много - будем следить за успехами. Спасибо, что не ленитесь писать
rsashka Автор
26.06.2022 15:17Спасибо за добрые пожелания!
В статье я писал, что в настоящий момент ориентируюсь на нишу задач тензорных вычислений и быстрого прототипирования. По сути, это ниша Python и других языков для обучения и научных исследований, но на кодовой базе C++.
Синтаксис без ключевых слов ставит главную цель, создать основу для реализации полноценных DSL диалектов. В следующей версии, про это я тоже писал в статье, будут добавлены макросы (префикс бэкслэш "\"), с помощью которых можно превратить универсальный синтаксис в более привычный текст, и вместо:
count:=5; [count<10]-->>{{ [count>5]-->{ --100--; }; count += 1; }};
Можно будет писать вот так:
count:=5; \while(count<10) {{ \if(count>5) { \return(100); }; count += 1; }};
doomguy49
26.06.2022 19:30+1Так не увидел не только в этом мнении, но и между строк. Увидел только аргументы против.
Пробовать, определенно, стоит – и начинать стоит с целей и задач. Тогда и вероятность сделать что-то значимое возрастает экспоненциально.
Например, глядя на Golang и php – на конструкции языка и бенчмарки – я могу сказать, что звучит убедительно, или, по крайней мере, интересно.
Тут же я вижу массу потраченных усилий (безусловно, проделана колоссальная работа) – к сожалению, в результате выглядящих, как два шага назад, потому что, на мой взгляд:
Цель ради цели;
Сложная реализация, синтаксис ужасен, даже если допустить, что есть целый сегмент прикладных задач, которые можно решить только на этом языке, а это не так.
auddu_k
26.06.2022 23:05Наверное нужно примеры. Если целевой «аудиторией» являются тензорный вычисление, нужно посмотреть на вычисления тут и сравнить с вычислениями там.
Нужны не ‘крестики-нолики’, а какой-то вычислительный проект.
andreyds95
25.06.2022 12:31Не очень понятны плюсы этого языка над уже существующими вроде Python. Так как синтаксис для тех, кто не заканчивал CS и не работал с C/C++, выглядит очень перегруженным
Apoheliy
25.06.2022 18:39Основные проблемы языка указали комментаторы выше. Попробую зайти с другой стороны:
Было бы отлично зафиксировать "язык реализации": т.е. используем только ???. Вот только, наверное, лучше НЕ использовать C/C++: у программистов этих языков есть свои библиотеки на те же тензоры и им сильно новое особо не нужно. Тем более не понятно, что с производительностью: сишники не любят лишние расходы "ни на что".
Ещё не раскрыт вопрос с "много-исходников". Периодически создаются языки, нацеленные на единый файл исходников. Но мало у кого (никого?) получалось остаться в этой логике. Поэтому: как писать в нескольких исходных файлах и это всё срастить? Срастить с использованием внешних исходников "языка реализации"?
В результате:
соглашусь с предыдущими комментаторами: НУЖНО определиться, какую проблему решает новый язык?
Пример-Аналогия: Эсперанто самый распространённый искуственный язык (по мнению википедии). Но изучают почему-то английский, китайский. Даже дотракийский (который вот нафиг не нужен). Эсперанто? Ну-у-у, а зачем?
Поэтому задел с языком "прикольный". Найдите нишу! Например, выгрузка математических тензорных "формул/монад/..." в вычислимое "нечто".
rsashka Автор
25.06.2022 19:37Было бы отлично зафиксировать "язык реализации": т.е. используем только ???.
Задам тот же вопрос, зачем?
Зачем фиксировать язык реализации, если одна из основных целей - создание универсальной грамматики не зависимой от языка реализации.
Apoheliy
25.06.2022 21:24Из Вашей постановки об языке следует, что есть Основной синтаксис (универсальный), и Расширенный синтаксис.
Расширенный синтаксис позволяет сделать тот функционал, который не покрывается базовыми вещами.
Вообще очень много языков программирования (чуть менее, чем все?), которые позволяют делать "странные" вещи: начиная с вызов функций из внешних динамических библиотек, продолжая записью в аппаратные пины на материнской плате, пляски с GPU и многое, много другое. И да, рано или поздно почти всем требуется запрограммировать что-то "странненькое".
Однако, очень часто (всегда?) для программирования этих "странных" вещей не нужно учить новый язык. И работа с тем-же динамическими библиотеками может быть как простой из под чистого C или с рядом тонкостей из (например) Python. Но питониста не заставляют учить C.
К чему все эти измышления: Если нужно программировать "странные" вещи, то пусть это будет тот же язык, возможно с "прибабахами". Если ли же невозможно это сделать штатными вещами, то будет лучше, если человек будет знать, с чем придётся работать. Иначе может быть цирк: написал расширенный синтаксис на C, а потом вдруг всё переигралось на питон. Э-э-э, и что теперь: сишный код на питон переписывать?
rsashka Автор
25.06.2022 21:48Мне видится немного другой подход. Расширенный синтаксис нужен как временное исключение, если нельзя сделать по другому. Но если вы реализуете на нем большую часть логики, то зачем вам вообще выбирать этот язык? Ведь не только инструмент должен подходить для задачи, но и нужно уметь этим инструментом пользоваться. И если вы лучше всего знаете Питон, то и пишите сразу на нем.
А если вы пишите сразу на С/С++, то лучше сразу его и использовать. Тем более, и Python и NewLang поддерживают вызовы нативных сишных функций. В статье даже пример есть:
printf := @import('printf(format:Format, ...):Int');
Зачем все усложнять, если одной командой можно импортировать сишную функцию и использовать её в дальнейшем как нативную без использования вставок на языке реализации?
Naf2000
26.06.2022 13:43Деревья выражений есть?
rsashka Автор
26.06.2022 14:01Уточните пожалуйста.
Интересует https://en.wikipedia.org/wiki/Binary_expression_tree (по сути, форма представления AST) или речь идет об аналоге LINQ из С# с вычислением в виде деревьев выражений?
Naf2000
26.06.2022 14:48Я подразумевал ближе к linq
rsashka Автор
26.06.2022 15:01Примерно так и думал, но сперва решил в этом убедится. Ответ и да и нет. Нет, в том смысле, что отсутствует сама возможность записи запросов.
И все же ответ скорее Да, так как выборки делать можно за зачет применения обычных и лямбда функции в качестве фильтров данных в итераторах, т.е. выборки делаются не за счет вычисления деревьев выражений, а с помощью вызовов функций.
markgrave
Ничего себе размер статьи
Подумал, что здесь весь язык будет расписан с его синтаксисом
rsashka Автор
Почти угадали
Так как это первый полны релиз, то размер статьи действительно получился ощутимый. Тут уж ничего не поделаешь. Но зато теперь можно будет маленькими частями подавать его особенности, а если кто захочет сразу полное описание, то можно давать ссылку сюда.