ru en

Синтаксис языка

Выражения

Приоритеты

Группа Выражения
binary 9 = += -= *= /= &= |= ^=
... => ...
x |> y()
ternary x ? y : z
binary 8 x ?? y
x -? y
binary 7 ||
binary 6 &&
binary 5 | & ^
binary 4 == !=
is
binary 3 >= <= > <
binary 2 + -
binary 1 * / %
binary 0 as
::
Группа Выражения
unary + -
* ^
~ !
@ @@
primary x.y
x(...) x<T>(...)
x[...]
x?(...) x?[...] x?.y
x..y <T>x..y
new T(...)
new context
new using T
new T[...] -[...] <T>-[...]
(x, ...)
[...] <T>[...]
{...} <TK, TV>{...}
(...)

Литералы

Числовые

  • Все числа поддерживают _ в описании
  • Можно писать целые числа в двоичном, восьмеричном и шестнадцатеричном виде с помощью префиксов: 0b101, 0o123456, 0xFF
Имя типа Суффикс Битность Суффикс
обязателен
Пример
f64 64 Нет 10.0, 2.0f64, 0f64
i64 10
f64 % Да 10%
u64 10u64
f32 32 0.0f32
i32 10i32
u32 10u32
i16 16 10i16
u16 10u16
i8 8 10i8
u8 10u8

Символьные

Символы состоят из одного знака и записываются в одинарных кавычках. Поддерживаются все escape-последовательности из строк, а в дополнение к ним \'.

  1. 'a' == 'а';

Строковые

Строки записываются в двойных кавычках. Список поддерживаемых escape-последовательностей: \", \\, \t, \r, \n

  1. str = "\\\t\r\n";

Присваивание

Присваивание осуществляется с помощью одинарного равенства =, присвоить что-либо можно только lvalue-выражению.

Имя

Имена — любые последовательности буквенных символов, цифр и знаков _, не начинающиеся с цифры. Регистр написания имени имеет значение также, как и в любых других C-подобных языках. Примеры имён: имя, x1_x2.

Расширенное присваивание

В языке существуют следующие операции расширенного присваивания: +=, -=, *=, /=, %=, &=, |=, ^=

  1. i = -1;
  2. while ((i += 1) < 10)
  3.     здесь_какой_то_код();
Создание цикла со счётчиком с помощью цикла while и расширенного присваивания

lvalue-выражения

lvalue-выражением могут быть:

  1. simple_name = 25;
  2. (x, y) = (5, 5);
  3. (lst = [1, 2, 3])[1] = x * y;
  4. self.result = simple_name == lst[1];
  1. (self.x, self["yz"]) = (1, (y, z) = (2, 3));
Вложенность присваиваний
  1. a = b = c = d = 10;
Множественное присваивание

Определение типа переменной

Начиная с обновления 0.7.0 Typing Update, в SLThree появилась возможность аннотировать тип переменной. Указание типа никак не влияет на значение, которые вы записываете в переменную, однако позволяет вам не запутаться. Кроме того, для оптимизации кода с помощью jit.opt() вам необходимо указать типы всех переменных и параметров.

Все случаи возможного описания типа переменной, кроме типов аргументов методов:

  1. i64 simple_name = 25;
  2. (i64 x, i64 y) = (5, 5);
  3. foreach (i64 x in [1, 2, 3])
  4.     x;

Методы

Вызов метода

Количество аргументов при вызове должно совпадать с количеством аргументов определения метода. Исключение составляют перегруженные методы платформы .NET. В их случае будет вызван первый попавшийся метод с нужным количеством аргументов.

  1. method_name(arg1, arg2, arg3);

Если вам нужно сделать какое-то действие ровно один раз, не засоряя контекст лишними именами, можно сразу же вызвать лямбду:

  1. i = 1;
  2. ((cnt) => cnt.i += 1)(self);

Передача контекста

Некоторые методы (например, eval) требуют для своего выполнения контекст. В SLThree существует несколько необходимых для этого ключевых слов, а также вы можете создавать свои контексты. Правильный вызов eval в текущем контексте выглядит так:

  1. using slt;
  2. slt.eval(self, "2 + 2");

Определение метода

Все методы в SLThree по умолчанию являются безымянными. Следовательно, простейшее описание метода выглядит так:

  1. () => {
  2.     return;
  3. };

Параметры описываются в скобках (если у метода параметр всего один и у него отсутствуют модификаторы, скобки можно опустить):

  1. (arg1, arg2) => arg1 + arg2;

Для привязки метода к имени, его нужно присвоить этому имени:

  1. method_name = (arg1, arg2, arg3) => arg1 + arg2 + arg3;

Модификаторы

Список возможных модификаторов:

  • explicit — отключение автоматического приведения числовых типов к максимальному в операциях. Арифметические операции не поддерживают никакие типы, кроме i64, u64 и f64. Однако, если вы используете именно их, этот модификатор может ускорить выполнение операций в два раза.
  • recursive — контекст аргументов будет генерироваться при каждом вызове метода. По умолчанию, контекст метода кэшируется.
  1. fib = recursive (n) => (n < 2) ? n : this.fib(n - 2) + this.fib(n - 1);
Рекурсивный алгоритм нахождения числа Фибоначчи

Операция |>

Эта операция позволяет писать последовательности из вызовов функций в более коротком и красивом виде. Левый параметр этой операции становится первым параметром вызова метода справа.

Было Стало
x |> y() y(x)
x(y) |> y(x) y(x(y), x)
x |> y(z()) y(x, z())
x |> y() |> z() z(y(x))
x |> (y() |> z()) z(x, y())

Типизация

Тип параметра метода пишется перед его именем. Тип возвращаемого значения пишется после двоеточия после списка параметров

  1. SLThree.Method sum = (i64 x, i64 y): i64 => x + y;

Обобщения

Начиная с 0.7.0 Typing Update в SLThree появились обобщённые методы. Их поддержка, главным образом, позволяет вызывать обобщённые методы из .NET.

  1. using tlinq;
  2. *<i64>1..100 |> tlinq.take<i64>(100) |> tlinq.select<i64, f64>(x => x / 100.0) |> tlinq.average<f64>();

Кроме того, появилась возможность создавать обобщённые методы SLThree. В этом случае обобщённые параметры пишутся между модификаторами и списком аргументов.

  1. my_generic_for = <T>(T a): bool => a is T;
  2. my_generic_for<i32>(2);

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

  1. x = <T>() => {
  2.     y = () => @T;
  3.     return y();
  4. };
  5. x(); // RuntimeError: Type "T" not found at 2:17

С помощью класса slt, вы также можете получить конкретный случай обобщённого метода:

  1. using slt;
  2. m = <T>() => @T;
  3. m_for_i64 = m |> slt.make_generic([@i64]);

Логические операции

Операции сравнения

При сравнениях на равенство == и неравенство != для первого объекта неявно вызывается Equals.

  1. 2 == 2;
  2. 2.Equals(2);

Другие сравнения (>, <, >=, <=) будут работать только если объект реализует интерфейс IComparable

  1. using System.DateTime;
  2. DateTime.Now > DateTime.MinValue;

И, ИЛИ

Всем известные логические операции операции. Про них нужно помнить, что исключающее ИЛИ ^ имеет приоритет бОльший, чем логическое И &&, так как она является побитовой операцией.

Операции && и || являются ленивыми: правая часть операции будет выполнена только при необходимости:

  1. a = 0; (a += 1) == 0 && (a += 1) != 0; a; // 1
  2. a = 0; (a += 1) == 0 &  (a += 1) != 0; a; // 2
  3. a = 0; (a += 1) != 0 || (a += 1) == 0; a; // 1
  4. a = 0; (a += 1) != 0 |  (a += 1) == 0; a; // 2

Соответствие типу

Операция is используется для проверки типа выражения слева.

  1. (5 / 2) is i64; // True

Кроме того, вы можете сразу же создать новую переменную при удачном соответствии:

  1. a = 2;
  2. if (a is i64 b) a == b;

Тернарная операция

Используется для ветвления на уровне выражений. Условие операции должно возвращать bool.

  1. true ? "true" : "false";

Проверка на null

Если вы предполагаете, что выражение может вернуть null, можете проверить это с помощью операции ??:

  1. repeat = () => {
  2.     if (need_init ?? true) {
  3.         need_init = false;
  4.         a = b = ^1..10;
  5.     }
  6.     return a + b;
  7. };

Безопасная операция

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

  1. 1 / 0 -? "деление на ноль";

Арифметические операции

Все арифметические операции поддерживают только три типа данных. В explicit режиме нельзя использовать остальные типы без преобразований. В implicit режиме можно использовать типы меньшей битности того же знака, однако результат всё равно будет 64-битный.

Левая часть Правая часть Результат
i64 i64 i64
u64 u64 u64
f64 f64 f64
i64 f64 f64
u64 f64 f64

Список арифметических операций:

  • Унарные + и -
  • Сложение + и вычитание -
  • Умножение *, деление /, остаток %
  • Побитовое И &, ИЛИ |, исключающее ИЛИ ^
  • Скобки ()

Доступ к члену

Всеми любимая точка .. Используется для доступа к членам следующих сущностей:

  • Объектов
  • Типов, если они используются в контексте
  • Контекстов
  • Словарей
  1. 2.Equals(2);
  2. using slt;
  3. slt.eval(self, "null");
  4. self.slt = null;
  5. dct = {};
  6. (dct.x, dct.y) = (2, 3);
  7. dct.Count;

Условие null

Кроме того, можно проверить выражение на null перед выполнением. Это работает с именами, вызовами (в т.ч. обобщёнными) и индексаторами. Если левая часть null, выражение тоже вернёт null.

  1. x?.y;
  2. x.?();
  3. x.?<i64>();
  4. x.?[10];

Индексаторы

Для индексации используется индексное свойство типа с нужным количеством параметров. Для индексации массивов, списков и кортежей используются 32-битные целые числа. В implicit режиме для них значение индекса будет автоматически приведено к нужному типу.

  1. a = [1, 2, 3];
  2. a[0];
  3. b = (1, 2, 3);
  4. b[0.33];
  5. c = -[1, 2, 3];
  6. c[0.66];
  7. d = new dict<i64, any>();
  8. d[1] = (a, b, c);

Индексаторы кортежей недоступны для записи значений.

Приведение к типу

Операция as выполняет явное ПРЕОБРАЗОВАНИЕ типа. Если оно невозможно — поведение не определено и зависит от конкретного типа.

  1. 1.1 as i64; // 1
  2. 0xFF as string; // "255"

Поддержка типов:

Левая часть Правая часть Результат
IConvertible Примитивный тип
Примитивный тип Enum
string Enum
context System.Type Объект переданного типа, публичные значения полей которого будут совпадать со значениями имён контекста
System.Type context Контекст со статическими значений переданного типа
any context Контекст с экземплярными значениями полей переданного объекта
any string
array<any> array<T>
list<any> array<T>
list<any> list<T>
dict<any, any> dict<TKey, TValue>
tuple<...> tuple<...>

Как есть

Кроме того, вы можете получать выражения SLThree "как есть" с помощью операции as is:

  1. a = (3 + 4) as is;
  2. a(); // 7

Проверив типы с помощью команды интерпретатора >-l --typed мы увидим, что тип переменной a это SLThree.BinaryAdd. Получить значение у такого выражения можно двумя способами:

  1. a = (2 + 2 * 2) as is;
  2. a(); // Просто вызвав его в языке
  3. a.GetValue(self.unwrap()); // Либо вызывать метод GetValue напрямую, но тогда придётся передавать контекст в чистом виде.

Специальные имена

self

self даёт ссылку на нынешний контекст. Это ключевое слово необходимо только для передачи контекста куда-то ещё. Например, можно вернуть контекст переменных из метода.

  1. x = () => {
  2.     (a, b) = (6, 66);
  3.     return self;
  4. };
  5. x().a;

this

this возвращает ссылку на контекст, в котором описан выполняемый в данный момент метод.

  1. x = () => this;
  2. x() == self;

global

global возвращает ссылку на глобальный контекст, доступный отовсюду. Старайтесь не пользоваться глобальным контекстом без крайней необходимости.

  1. global.a = 25;
  2. get_a = () => global.a;
  3. get_a();

super

super возвращает ссылку на контекст, в котором был создан текущий контекст.

  1. x = new context {
  2.     get_super = () => super;
  3. };
  4. x.get_super() == self;

upper

upper возвращает ссылку на контекст, из которого был вызван выполняемый сейчас метод.

  1. (x, y) = (1, 2);
  2. sum = () => upper.x + upper.y;
  3. sum();

private

private возвращает ссылку на скрытый контекст того контекста, в котором описан текущий контекст (this)

  1. x = new context {
  2.     private.x = 10;
  3.     get_x = () => private.x;
  4. };
  5. x.get_x();

Инициализаторы

Вызов конструктора

Для создания объекта типа используется класс Activator, поэтому при вызове конструктора, в отличие от вызова метода, перегрузка будет подобрана наиболее точно.

  1. using System.Random;
  2. new Random().NextDouble();

Создание массива

Без указания типа (тип array или array<any>):

  1. a = -[1, "str", 2.0, new context];

С указанием типа. В implicit режиме каждый элемент массива будет преобразован к нужному типу.

  1. a = <f64>-[1, 2, 3, ];

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

Создание кортежа

Кортежи всегда имеют чёткий тип и возвращают объекта типа System.ValueTuple<...>. Количество элементов в кортеже не ограничено.

  1. (1, 2.0, "3.0", new System.Exception(), null).GetType();

Кортеж из одного элемента:

  1. (1, );

Создание списка

Создание списка похоже на создание массива, однако знак - отсутствует:

  1. anylist = [1, 2, 3, 4, 5];
  2. typed = <i64>[1, 2, 3, 4, 5];

Создание словаря

Без указания типа (тип dict или dict<any, any>):

  1. dict = {
  2.     1: 10,
  3.     2: 20,
  4. };

С указанием типа ключей и значений. В implicit режиме каждые ключ и значение будут преобразован к нужному типу.

  1. dict = <i64, string>{
  2.     1: 10,
  3.     2: 20,
  4. };

Создание диапазона

Диапазоны — простые обёртки границ, реализующие перебор значений между ними как последовательности:

  1. sum = 0;
  2. foreach (x in 1..10) sum += x;

По умолчанию диапазон возвращает последовательность 64-битных целых. Но можно типизировать диапазон для возвращения целых другой битности:

  1. using tlinq;
  2. <i32>1..100 |> tlinq.sum<i32>();

Создание контекста

Контекст — комплексный тип данных языка SLThree. Инициализация контекста состоит из:

  • new context
  • [Опционально] Имени контекста
  • [Опционально] Типа приведения : T
  • [Опционально] Тела контекста { ... }

Наличие типа приведения при описании аналогично применению для контекста as к этому типу:

  1. new context {
  2.     Capacity = 2500;
  3. } as list;
  4. new context : list {
  5.     Capacity = 2500;
  6. };

Имя контекста позволяет отличить этот контекст от других при отладке. Имя контекста не зависит от переменной, в которую его записали:

  1. a = new context ЫЫЫ;
  2. a.unwrap().Name;

Тело контекста должно содержать только выражения-присваивания. При выполнении этих присваиваний контекст правой части это контекст, в котором производится инициализация, а контекст левой части — создаваемый контекст.

  1. new context A : list {
  2.     Capacity = 2500;
  3.     Capacity2 = Capacity; // Неправильно
  4. };

Помимо выражений-присваиваний, тело контекста может содержать оператор context.

Создание using

Выполнение using slt; запишет в переменную slt класс-обёртку для доступа к её внутренностям. Но можно получить этот доступ и без записывания в переменную:

  1. (new using slt).eval(self, "2 + 2 * 2");

Интерполированные строки

Интерполированные (форматные) строки сделаны как в C#, за исключением того, что они не поддерживают непосредственно форматы.

  1. (a, b) = (2, 2);
  2. str = $"{a} + {b} == {a + b}";

Операции случайного выбора

Существует две операции: унарная ^ позволяет выполнить выбор, унарная * позволяет получить генератор (ленивую бесконечную последовательность).

  1. random_num = ^1..100;
  2. using linq;
  3. random_nums = *1..100 |> linq.take(10) |> linq.jts();

Обе операции выбора можно выполнять для:

  • Словарей вида Dictionary<T, double>, где значение это вес ключа
  • Диапазонов
  • Любых последовательностей конечной длины
  1. d = *{
  2.     "0": 10%,
  3.     "1": 20%,
  4.     "2": 30%,
  5.     "3": 40%,
  6. };
  7. using linq;
  8. str = d |> linq.take(^10..20) |> linq.jts("");
Пример со словарём

Операции рефлексии

Получение типа

Для получения типа используется унарная операция @

  1. typeof = <T>() => @T;
  2. x = typeof<i64>();

Существует также операция @@. Делает то же самое, за исключением того, что она высчитывается на этапе парсинга. Поэтому, работает только с полными именами типов и их синонимов.

Получение конкретной перегрузки

Поскольку SLThree — динамический ЯП, он не поддерживают перегрузку методов C# в полной мере. В случае, если вам нужна определённая перегрузка, вы можете воспользоваться следующим синтаксисом:

  • @Type
  • ::MethodName
  • (argType1, argType2)
  1. writeln = <T>(arg) => (@System.Console::WriteLine(T))(arg);
  2. writeln<bool>(true);
  3. writeln<string>("str");
  4. writeln<tuple<i64, i64, i64>>((1, 2, 3));

Под тип any (System.Object) можно подводить любые типы!

Выражение match

Выражение match позволяет разветвлять программу на уровне выражений. Поскольку это выражение, когда вы пишете его в качестве утверждения, вы должны писать после } точку с запятой!

  1. (() => {
  2.     using console;
  3.     console.write("Введите ваше имя: ");
  4.     name = console.readln();
  5.     access = match (name.ToLower()) {
  6.         "борис", "ярослав" ==> true;
  7.         "джо" ==> {
  8.             using System.Environment;
  9.             Environment.Exit(-1i32);
  10.         };
  11.         () ==> false;
  12.     };
  13. })();

Операторы

Оператор-выражение

Самым главным оператором является выражение. Отличие от чистого выражения лишь в присутствии ;

  1. 2 + 2;

Блок

!Важно. Исходя из этой статьи, вы могли подумать, что блоки в SLThree точно такие же, как и в любых си-подобных языках. Но нет — важным отличием является то, что блок должен состоять как минимум из одного оператора!

  1. DICTIONARY = {};
  2. if (empty_block ?? true) {
  3.     ;
  4. }

Условный оператор

  1. if (2 + 2 == 4)
  2.     "true-block";
  3. else
  4.     "false-block";

Циклы

Цикл while

Этот цикл не нуждается в представлении.

  1. i = 1;
  2. while ((i = i + 1) < 100)
  3.     i -= ^[0, 1];

Цикл foreach

Цикл для перебора последовательностей вида IEnumerable.

  1. using console;
  2. foreach (x in [1, 2, 3])
  3.     console.writeln(x);

Прерывание

Операторы прерывания также не нуждаются в представлении.

  1. foreach (x in [1, 2, 3]) {
  2.     if (x == 1)
  3.         continue;
  4.     if (x < 3)
  5.         break;
  6. }

Оператор return

Используется для возврата значений из методов. Впрочем, любой контекст может иметь возвращаемое значение, а потому вы можете написать return в любом скрипте SLThree и достать его как свойство ReturnedValue. Строгости в возврате для методов, которые не отправлены в JIT нет. Если метод ничего не вернул, или был написан return без выражения, то возвращаемое значение равно null.

Оператор using

Аналог создания using, но в виде оператора. Эти две строки кода идентичны:

  1. x = new using slt;
  2. using slt as x;

Оператор context

Аналог создания context'а, но в виде оператора.

  1. a = new context a : i64 {
  2.     x = 2;
  3. };
  4. context a : i64 {
  5.     x = 2;
  6. }

Этот код только демонстрация. Разумеется, у типа i64 (System.Int64) нет никакого поля x.

Обработка исключений

  • В блоке try находится код, который необходимо обработать
  • В блоке catch находится перехват ошибки. В переменную из скобок будет записано перехваченное исключение.
  • В блоке finally находится код, который выполнится в любом случае.
  • Выбрасывать же исключения можно с помощью throw.
  1. using console;
  2. try {
  3.     if (^50%)
  4.         throw new System.Exception();
  5. } catch (e) {
  6.     console.writeln($"caught {e}");
  7. } finally {
  8.     console.writeln("finally");
  9. }