Синтаксис языка
Выражения
Приоритеты
Группа | Выражения |
---|---|
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-последовательности из строк, а в дополнение к ним \'
.
- 'a' == 'а';
Строковые
Строки записываются в двойных кавычках. Список поддерживаемых escape-последовательностей: \"
, \\
, \t
, \r
, \n
- str = "\\\t\r\n";
Присваивание
Присваивание осуществляется с помощью одинарного равенства =
, присвоить что-либо можно только lvalue-выражению.
Имя
Имена — любые последовательности буквенных символов, цифр и знаков _
, не начинающиеся с цифры. Регистр написания имени имеет значение также, как и в любых других C-подобных языках. Примеры имён: имя
, x1_x2
.
Расширенное присваивание
В языке существуют следующие операции расширенного присваивания: +=
, -=
, *=
, /=
, %=
, &=
, |=
, ^=
- i = -1;
- while ((i += 1) < 10)
- здесь_какой_то_код();
lvalue-выражения
lvalue-выражением могут быть:
- simple_name = 25;
- (x, y) = (5, 5);
- (lst = [1, 2, 3])[1] = x * y;
- self.result = simple_name == lst[1];
- (self.x, self["yz"]) = (1, (y, z) = (2, 3));
- a = b = c = d = 10;
Определение типа переменной
Начиная с обновления 0.7.0 Typing Update, в SLThree появилась возможность аннотировать тип переменной. Указание типа никак не влияет на значение, которые вы записываете в переменную, однако позволяет вам не запутаться. Кроме того, для оптимизации кода с помощью jit.opt()
вам необходимо указать типы всех переменных и параметров.
Все случаи возможного описания типа переменной, кроме типов аргументов методов:
- i64 simple_name = 25;
- (i64 x, i64 y) = (5, 5);
- foreach (i64 x in [1, 2, 3])
- x;
Методы
Вызов метода
Количество аргументов при вызове должно совпадать с количеством аргументов определения метода. Исключение составляют перегруженные методы платформы .NET. В их случае будет вызван первый попавшийся метод с нужным количеством аргументов.
- method_name(arg1, arg2, arg3);
Если вам нужно сделать какое-то действие ровно один раз, не засоряя контекст лишними именами, можно сразу же вызвать лямбду:
- i = 1;
- ((cnt) => cnt.i += 1)(self);
Передача контекста
Некоторые методы (например, eval) требуют для своего выполнения контекст. В SLThree существует несколько необходимых для этого ключевых слов, а также вы можете создавать свои контексты. Правильный вызов eval в текущем контексте выглядит так:
- using slt;
- slt.eval(self, "2 + 2");
Определение метода
Все методы в SLThree по умолчанию являются безымянными. Следовательно, простейшее описание метода выглядит так:
- () => {
- return;
- };
Параметры описываются в скобках (если у метода параметр всего один и у него отсутствуют модификаторы, скобки можно опустить):
- (arg1, arg2) => arg1 + arg2;
Для привязки метода к имени, его нужно присвоить этому имени:
- method_name = (arg1, arg2, arg3) => arg1 + arg2 + arg3;
Модификаторы
Список возможных модификаторов:
- explicit — отключение автоматического приведения числовых типов к максимальному в операциях. Арифметические операции не поддерживают никакие типы, кроме i64, u64 и f64. Однако, если вы используете именно их, этот модификатор может ускорить выполнение операций в два раза.
- recursive — контекст аргументов будет генерироваться при каждом вызове метода. По умолчанию, контекст метода кэшируется.
- 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()) |
Типизация
Тип параметра метода пишется перед его именем. Тип возвращаемого значения пишется после двоеточия после списка параметров
- SLThree.Method sum = (i64 x, i64 y): i64 => x + y;
Обобщения
Начиная с 0.7.0 Typing Update в SLThree появились обобщённые методы. Их поддержка, главным образом, позволяет вызывать обобщённые методы из .NET.
- using tlinq;
- *<i64>1..100 |> tlinq.take<i64>(100) |> tlinq.select<i64, f64>(x => x / 100.0) |> tlinq.average<f64>();
Кроме того, появилась возможность создавать обобщённые методы SLThree. В этом случае обобщённые параметры пишутся между модификаторами и списком аргументов.
- my_generic_for = <T>(T a): bool => a is T;
- my_generic_for<i32>(2);
Подстановки типов влияют на все его использования в методе, за исключением тех, что относятся к вложенным методам.
- x = <T>() => {
- y = () => @T;
- return y();
- };
- x(); // RuntimeError: Type "T" not found at 2:17
С помощью класса slt
, вы также можете получить конкретный случай обобщённого метода:
- using slt;
- m = <T>() => @T;
- m_for_i64 = m |> slt.make_generic([@i64]);
Логические операции
Операции сравнения
При сравнениях на равенство ==
и неравенство !=
для первого объекта неявно вызывается Equals.
- 2 == 2;
- 2.Equals(2);
Другие сравнения (>
, <
, >=
, <=
) будут работать только если объект реализует интерфейс IComparable
- using System.DateTime;
- DateTime.Now > DateTime.MinValue;
И, ИЛИ
Всем известные логические операции операции. Про них нужно помнить, что исключающее ИЛИ ^
имеет приоритет бОльший, чем логическое И &&
, так как она является побитовой операцией.
Операции &&
и ||
являются ленивыми: правая часть операции будет выполнена только при необходимости:
- a = 0; (a += 1) == 0 && (a += 1) != 0; a; // 1
- a = 0; (a += 1) == 0 & (a += 1) != 0; a; // 2
- a = 0; (a += 1) != 0 || (a += 1) == 0; a; // 1
- a = 0; (a += 1) != 0 | (a += 1) == 0; a; // 2
Соответствие типу
Операция is
используется для проверки типа выражения слева.
- (5 / 2) is i64; // True
Кроме того, вы можете сразу же создать новую переменную при удачном соответствии:
- a = 2;
- if (a is i64 b) a == b;
Тернарная операция
Используется для ветвления на уровне выражений. Условие операции должно возвращать bool.
- true ? "true" : "false";
Проверка на null
Если вы предполагаете, что выражение может вернуть null, можете проверить это с помощью операции ??
:
- repeat = () => {
- if (need_init ?? true) {
- need_init = false;
- a = b = ^1..10;
- }
- return a + b;
- };
Безопасная операция
Похож на проверку на null, но делает левое выражение безопасным и если при его выполнении произошла ошибка, возвращает правую часть.
- 1 / 0 -? "деление на ноль";
Арифметические операции
Все арифметические операции поддерживают только три типа данных. В explicit режиме нельзя использовать остальные типы без преобразований. В implicit режиме можно использовать типы меньшей битности того же знака, однако результат всё равно будет 64-битный.
Левая часть | Правая часть | Результат |
---|---|---|
i64 | i64 | i64 |
u64 | u64 | u64 |
f64 | f64 | f64 |
i64 | f64 | f64 |
u64 | f64 | f64 |
Список арифметических операций:
- Унарные
+
и-
- Сложение
+
и вычитание-
- Умножение
*
, деление/
, остаток%
- Побитовое И
&
, ИЛИ|
, исключающее ИЛИ^
- Скобки
()
Доступ к члену
Всеми любимая точка .
. Используется для доступа к членам следующих сущностей:
- Объектов
- Типов, если они используются в контексте
- Контекстов
- Словарей
- 2.Equals(2);
- using slt;
- slt.eval(self, "null");
- self.slt = null;
- dct = {};
- (dct.x, dct.y) = (2, 3);
- dct.Count;
Условие null
Кроме того, можно проверить выражение на null перед выполнением. Это работает с именами, вызовами (в т.ч. обобщёнными) и индексаторами. Если левая часть null, выражение тоже вернёт null.
- x?.y;
- x.?();
- x.?<i64>();
- x.?[10];
Индексаторы
Для индексации используется индексное свойство типа с нужным количеством параметров. Для индексации массивов, списков и кортежей используются 32-битные целые числа. В implicit режиме для них значение индекса будет автоматически приведено к нужному типу.
- a = [1, 2, 3];
- a[0];
- b = (1, 2, 3);
- b[0.33];
- c = -[1, 2, 3];
- c[0.66];
- d = new dict<i64, any>();
- d[1] = (a, b, c);
Индексаторы кортежей недоступны для записи значений.
Приведение к типу
Операция as
выполняет явное ПРЕОБРАЗОВАНИЕ типа. Если оно невозможно — поведение не определено и зависит от конкретного типа.
- 1.1 as i64; // 1
- 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
:
- a = (3 + 4) as is;
- a(); // 7
Проверив типы с помощью команды интерпретатора >-l --typed
мы увидим, что тип переменной a
это SLThree.BinaryAdd
. Получить значение у такого выражения можно двумя способами:
- a = (2 + 2 * 2) as is;
- a(); // Просто вызвав его в языке
- a.GetValue(self.unwrap()); // Либо вызывать метод GetValue напрямую, но тогда придётся передавать контекст в чистом виде.
Специальные имена
self
self
даёт ссылку на нынешний контекст. Это ключевое слово необходимо только для передачи контекста куда-то ещё. Например, можно вернуть контекст переменных из метода.
- x = () => {
- (a, b) = (6, 66);
- return self;
- };
- x().a;
this
this
возвращает ссылку на контекст, в котором описан выполняемый в данный момент метод.
- x = () => this;
- x() == self;
global
global
возвращает ссылку на глобальный контекст, доступный отовсюду. Старайтесь не пользоваться глобальным контекстом без крайней необходимости.
- global.a = 25;
- get_a = () => global.a;
- get_a();
super
super
возвращает ссылку на контекст, в котором был создан текущий контекст.
- x = new context {
- get_super = () => super;
- };
- x.get_super() == self;
upper
upper
возвращает ссылку на контекст, из которого был вызван выполняемый сейчас метод.
- (x, y) = (1, 2);
- sum = () => upper.x + upper.y;
- sum();
private
private
возвращает ссылку на скрытый контекст того контекста, в котором описан текущий контекст (this
)
- x = new context {
- private.x = 10;
- get_x = () => private.x;
- };
- x.get_x();
Инициализаторы
Вызов конструктора
Для создания объекта типа используется класс Activator
, поэтому при вызове конструктора, в отличие от вызова метода, перегрузка будет подобрана наиболее точно.
- using System.Random;
- new Random().NextDouble();
Создание массива
Без указания типа (тип array
или array<any>
):
- a = -[1, "str", 2.0, new context];
С указанием типа. В implicit режиме каждый элемент массива будет преобразован к нужному типу.
- a = <f64>-[1, 2, 3, ];
Обратите также внимание, что при создании массива, кортежа, списка или словаря можно писать запятую в конце.
Создание кортежа
Кортежи всегда имеют чёткий тип и возвращают объекта типа System.ValueTuple<...>. Количество элементов в кортеже не ограничено.
- (1, 2.0, "3.0", new System.Exception(), null).GetType();
Кортеж из одного элемента:
- (1, );
Создание списка
Создание списка похоже на создание массива, однако знак -
отсутствует:
- anylist = [1, 2, 3, 4, 5];
- typed = <i64>[1, 2, 3, 4, 5];
Создание словаря
Без указания типа (тип dict
или dict<any, any>
):
- dict = {
- 1: 10,
- 2: 20,
- };
С указанием типа ключей и значений. В implicit режиме каждые ключ и значение будут преобразован к нужному типу.
- dict = <i64, string>{
- 1: 10,
- 2: 20,
- };
Создание диапазона
Диапазоны — простые обёртки границ, реализующие перебор значений между ними как последовательности:
- sum = 0;
- foreach (x in 1..10) sum += x;
По умолчанию диапазон возвращает последовательность 64-битных целых. Но можно типизировать диапазон для возвращения целых другой битности:
- using tlinq;
- <i32>1..100 |> tlinq.sum<i32>();
Создание контекста
Контекст — комплексный тип данных языка SLThree. Инициализация контекста состоит из:
new context
- [Опционально] Имени контекста
- [Опционально] Типа приведения
: T
- [Опционально] Тела контекста
{ ... }
Наличие типа приведения при описании аналогично применению для контекста as
к этому типу:
- new context {
- Capacity = 2500;
- } as list;
- new context : list {
- Capacity = 2500;
- };
Имя контекста позволяет отличить этот контекст от других при отладке. Имя контекста не зависит от переменной, в которую его записали:
- a = new context ЫЫЫ;
- a.unwrap().Name;
Тело контекста должно содержать только выражения-присваивания. При выполнении этих присваиваний контекст правой части это контекст, в котором производится инициализация, а контекст левой части — создаваемый контекст.
- new context A : list {
- Capacity = 2500;
- Capacity2 = Capacity; // Неправильно
- };
Помимо выражений-присваиваний, тело контекста может содержать оператор context.
Создание using
Выполнение using slt;
запишет в переменную slt
класс-обёртку для доступа к её внутренностям. Но можно получить этот доступ и без записывания в переменную:
- (new using slt).eval(self, "2 + 2 * 2");
Интерполированные строки
Интерполированные (форматные) строки сделаны как в C#, за исключением того, что они не поддерживают непосредственно форматы.
- (a, b) = (2, 2);
- str = $"{a} + {b} == {a + b}";
Операции случайного выбора
Существует две операции: унарная ^
позволяет выполнить выбор, унарная *
позволяет получить генератор (ленивую бесконечную последовательность).
- random_num = ^1..100;
- using linq;
- random_nums = *1..100 |> linq.take(10) |> linq.jts();
Обе операции выбора можно выполнять для:
- Словарей вида Dictionary<T, double>, где значение это вес ключа
- Диапазонов
- Любых последовательностей конечной длины
- d = *{
- "0": 10%,
- "1": 20%,
- "2": 30%,
- "3": 40%,
- };
- using linq;
- str = d |> linq.take(^10..20) |> linq.jts("");
Операции рефлексии
Получение типа
Для получения типа используется унарная операция @
- typeof = <T>() => @T;
- x = typeof<i64>();
Существует также операция @@
. Делает то же самое, за исключением того, что она высчитывается на этапе парсинга. Поэтому, работает только с полными именами типов и их синонимов.
Получение конкретной перегрузки
Поскольку SLThree — динамический ЯП, он не поддерживают перегрузку методов C# в полной мере. В случае, если вам нужна определённая перегрузка, вы можете воспользоваться следующим синтаксисом:
@Type
::MethodName
(argType1, argType2)
- writeln = <T>(arg) => (@System.Console::WriteLine(T))(arg);
- writeln<bool>(true);
- writeln<string>("str");
- writeln<tuple<i64, i64, i64>>((1, 2, 3));
Под тип any
(System.Object) можно подводить любые типы!
Выражение match
Выражение match
позволяет разветвлять программу на уровне выражений. Поскольку это выражение, когда вы пишете его в качестве утверждения, вы должны писать после }
точку с запятой!
- (() => {
- using console;
- console.write("Введите ваше имя: ");
- name = console.readln();
- access = match (name.ToLower()) {
- "борис", "ярослав" ==> true;
- "джо" ==> {
- using System.Environment;
- Environment.Exit(-1i32);
- };
- () ==> false;
- };
- })();
Операторы
Оператор-выражение
Самым главным оператором является выражение. Отличие от чистого выражения лишь в присутствии ;
- 2 + 2;
Блок
!Важно. Исходя из этой статьи, вы могли подумать, что блоки в SLThree точно такие же, как и в любых си-подобных языках. Но нет — важным отличием является то, что блок должен состоять как минимум из одного оператора!
- DICTIONARY = {};
- if (empty_block ?? true) {
- ;
- }
Условный оператор
- if (2 + 2 == 4)
- "true-block";
- else
- "false-block";
Циклы
Цикл while
Этот цикл не нуждается в представлении.
- i = 1;
- while ((i = i + 1) < 100)
- i -= ^[0, 1];
Цикл foreach
Цикл для перебора последовательностей вида IEnumerable.
- using console;
- foreach (x in [1, 2, 3])
- console.writeln(x);
Прерывание
Операторы прерывания также не нуждаются в представлении.
- foreach (x in [1, 2, 3]) {
- if (x == 1)
- continue;
- if (x < 3)
- break;
- }
Оператор return
Используется для возврата значений из методов. Впрочем, любой контекст может иметь возвращаемое значение, а потому вы можете написать return
в любом скрипте SLThree и достать его как свойство ReturnedValue. Строгости в возврате для методов, которые не отправлены в JIT нет. Если метод ничего не вернул, или был написан return
без выражения, то возвращаемое значение равно null
.
Оператор using
Аналог создания using, но в виде оператора. Эти две строки кода идентичны:
- x = new using slt;
- using slt as x;
Оператор context
Аналог создания context'а, но в виде оператора.
- a = new context a : i64 {
- x = 2;
- };
- context a : i64 {
- x = 2;
- }
Этот код только демонстрация. Разумеется, у типа i64 (System.Int64) нет никакого поля x.
Обработка исключений
- В блоке
try
находится код, который необходимо обработать - В блоке
catch
находится перехват ошибки. В переменную из скобок будет записано перехваченное исключение. - В блоке
finally
находится код, который выполнится в любом случае. - Выбрасывать же исключения можно с помощью
throw
.
- using console;
- try {
- if (^50%)
- throw new System.Exception();
- } catch (e) {
- console.writeln($"caught {e}");
- } finally {
- console.writeln("finally");
- }