Создание объектов

Область видимости объекта != время жизни объекта

У объекта имя есть всегда, даже тогда, когда сам объект физически еще не создан. Область видимости имени определяется его расположением в программном коде (блок кода, пространство имен, модуль и т.д.). Объект может существовать, но быть не доступен из текущей области видимости, например, перекрыт другим объектом с таким же именем.

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

Создания объектов

Для создания объектов в TrustLang используется несколько операторов:

  • :=” — создаёт или присваивает значение переменной/функции (объекту по значению). Если объекта с таким именем ещё нет, он создаётся; иначе значение переприсваивается. Пример: x := 5;, hello(str) := { ... };.

  • ::=” — объявление типа (алиаса или типизированного типа), например MyInt ::= :Int32;, Status ::= (OK=0, ERROR=1, BUSY=2,):Enum;. Применять “::=” для создания значений/функций нельзя — компилятор сообщает: “cannot assign a type … to a value variable; use ‘::=’ to declare a type alias”.

⚠️ НЕ РЕАЛИЗОВАНО: операторы `::-` и `:-` (чистые функции и значения, вычисляемые на этапе компиляции) в текущей версии НЕ реализованы

Операторы “::-” и “:-” в текущей версии не реализованы; ранее они предназначались для чистых функций и значений, вычисляемых на этапе компиляции.

Присвоение нового значения

Оператор “=” применяется для присвоения нового значения уже существующим объектам. Если объект с указанным именем отсутствует, то возникает ошибка компиляции.
Если переопределяется функция или метод класса, то старое значение не удаляется, а создается стек переопределенных имен (своего рода таблицу вирутальных имен времени компиляции), и к старому (предыдущему) объекту можно обратиться из новой функции по системному имени “$$”.*

Оператор обмена значениями

Оператор “:=:” (swap) не создает новых переменных, а только обменивает их значения между собой. Переменные должны иметь одинаковые/совместимые типы данных, либо иметь тип :None, т.е. "_".
Используется для реализации идиомы копирования и замены (copy-and-swap idiom), так как при выполнении оператора не может быть ошибок.

Оператор добавления элемента

Оператор “[]=” — аналог push_back: добавляет новый элемент к контейнеру слева, увеличивая его размер на единицу. Общее правило: X []= v равносильно X.push_back(v).

  • Для словаря (Dict, в т.ч. позиционные «массивы») безымянный append добавляет позиционный элемент: d []= 999.
  • Для строк добавляет символ/строку: s []= 'd's.append("d").
  • Правый объект оператора должен быть совместим с типом единичного элемента левого объекта (ширина строки: узкая '...' / широкая "..." должны совпадать с типом строкового контейнера).
  • Вложенный LHS (d['x'] []= v, d[0] []= v, d.field []= v) — пока не реализован (ошибка компиляции).
  • Слияние словаря через оператор распаковки ...: d []= ... dict2 или d []= ... (a=1, b=2,) добавляет в цель все элементы словаря-операнда (аналог extend/update); размер цели растёт на число элементов (для литерала и словаря с известным размером — статически контролируется).
    d := (1, two=2,);
    d []= 999;        # добавить позиционный элемент → size 3
    d []= 'x';        # добавить ещё один элемент
    d []= ... (a=100, b=200,);  # слияние литерала → size 5 (добавлены a и b)
    d2 := (three=3, 4,);
    d []= ... d2;     # слияние переменной-словаря → size 7

    s := 'abc';
    s []= 'd';        # строка стала "abcd"
    @print(s);         # > abcd

Добавление именованного элемента словаря выполняется оператором присваивания d[name] = value (установка/добавление по ключу).

Например, при определении класса :NewClass2:

    :NewClass := :Class() { # Базовый класс
        @::static := 0; # Однократная инициализация статического объекта класса
        filed1 := 1; # Создание поля класса
        filed2 := 2; # Создание поля класса
        method1() := {}; # Регистрация метода
        method2() := {}; # Регистрация метода
    };

    :NewClass2 := :NewClass() {
        filed1 := 2; # Будет ошибка, т.к. field1 уже есть в базовом классе
        filed2 := 2; # Используется поле field2 базового класса (или создается новое)

        method1() := {}; # Будет ошибка, т.к. method1 уже есть в базовом классе
        method1() = {}; # Аналог override, т.к. method должен существовать в базовом классе
    };
    $var := 99; # Создать переменную var
    {
        $var := 100; # Новая переменная перекрывает var более высокого уровня
        @print('%d', $var)  # > 100
    }
    @print('%d', $var)  # > 99

Присваивание значения сразу нескольким переменным и оператор распаковки словаря

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

С правой стороны от оператора присвоения может находится только одно значение, а для обмена занчениями двух переменных, вместо записи a,b = b,a; нужно использовать оператор обмена:

    a :=: b;

В качестве правого операнда в операторе присвоения допускается использовать оператор распаковки словаря (многоточие), который можно использовать и при передаче аргументов в функцию.

    args := (arg1=1, arg2=2, 3, 4,);
    call(arg=0, ... args);

    # Что равносильно вызову
    call(arg=0, arg1=1, arg2=2, 3, 4);

Словарь может быть указан и с левой стороны от оператора присвоения — это деструктуризация. Без маркера — точная привязка: каждая цель получает ровно один элемент, число целей должно совпадать с числом элементов (для статически-известного размера). Суффикс ... у имени цели (rest..., C++-pack) связывает оставшиеся элементы; _... — извлечь элементы, остаток отбросить; одиночный _ — пропустить один элемент. Каждая цель типизируется runtime-типом своего элемента (Int → Integer, Float → Double, Bool, StrChar…); внутри цикла тип расширяется до максимального среди элементов (Bool/Int8 → Integer, float → Double), а для гетерогенного словаря используется runtime-конвертер — арифметика над элементом работает как над Integer/Double. Присваивание в существующие переменные — оператор = (a, b = ... d; или a, b = t;): цели должны быть объявлены ранее (:=), новые переменные не создаются; иначе — ошибка компиляции. Присваивание в константную цель (^) — ошибка. Цель может иметь явную аннотацию типа (a:Int32, b := ... d; / a:Int32, b = t;) — она фиксирует тип объявляемой переменной: для кортежа вместо auto генерируется int32_t c_a; для словаря переменная объявляется int32_t, а элемент извлекается по runtime-типу хранения (std::any_cast<int64_t>(...), т.к. Dict нормализует целые в int64_t). Без аннотации тип выводится. Вложенная деструктуризация (a, (b, c) := t;) не поддерживается — только отдельные переменные. Также не поддерживаются: деструктуризация по именованным ключам ({a: x, b: y} = d — раскрытие только позиционное, по порядку вставки), «rest в середине» (a, rest..., c := seq — маркер rest... допустим только последним) и аннотация типа на rest-цели (rest:MyDict... / rest:MyTuple... — тип остатка всегда выводится: для словаря — Dict, для кортежа — под-кортеж; аннотация → ошибка компиляции). Для кортежа rest-цель не может переиспользовать имя уже существующей переменной (в т.ч. сам источник — a, t... := t): в отличие от spread-словаря, где item, dict... := ... dict — легитимная идиома мутации источника, кортежный rest не мутация и такое переиспользование — ошибка компиляции. При runtime-недостатке элементов (источник с неизвестным на этапе сборки размером) выполнение завершается контролируемым abort с диагностикой — никакого тихого дефолта (None/нуля) для любых типов целей (включая Any). Если тип элемента словаря не выводится (например, словарь-параметр), цель вне цикла типизируется std::any с предупреждением (симметрично цикловому widening) — не молча.

Пример цикла foreach для суммирования всех элементов словаря (или одномерного тензора) с использованием оператора раскрытия словаря (списка):

    summa := 0;
    dictionary := (1,2,3,4,5,);
    @while( dictionary ) {
        # Первый элемент словаря перемещается в item; суффикс `dictionary...` связывает
        # оставшиеся элементы, мутируя источник (цикл завершается, когда словарь опустеет)
        item, dictionary... := ... dictionary; 
        summa += item;
    };

Заполнение размерных тензороов

⚠️ НЕ РЕАЛИЗОВАНО: описанные здесь способы заполнения размерных тензоров (`:Tensor[10](...)`, `...`-повторение, заполнение функцией/диапазоном) не реализованы — многомерные тензоры в текущей версии не реализованы

При заполнении данными размерных тензоров существует краткий способ указания их начальных значений. Для этого используется многоточие в следующих вариантах:

  • :Tensor[10](2,3, ...) - повторение всех предыдущих данных до полного заполнения тензора, что равносильно записи :Tensor[10](2,3, 2,3, 2,3, 2,3, 2,3).

  • :Tensor[10](2, 3, ... 42 ...) - заполнение значением 42 до конца тензора, что равносильно записи :Tensor[10](2,3, 42,42,42,42,42,42,42,42).

  • :Tensor[10](2, 3, ... rand() ...) - заполнение до конца тензора значением, которое будет возвращать функция. Указанная функция будет вызываться для каждого элемента до полного заполнения тензора (8 раз).

  • :Tensor[10]( rand(), rand(), ...) - заполнение тензора двумя произвольными заначениями, то есть функция rand() будет вызвана дважды только для двух первых элементов, после чего эти значения будут использоваться до полного заполнения тензора (как в первом примере).

  • :Tensor[10]( ... 0..0.99..0.1 ) - заполнение тензора элементами диапазона, что равносильно записи
    :Tensor[10](0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9). Раскрытие диапазона создает фиксированное количество значений. Поэтому в данном примере размерность тензора можно не указывать, так как она будет создана автоматически.

  • :Tensor[15]( ... 0..5 , ... ) - заполнение тензора элементами диапазона и их повторение до заданного размера тензора: :Tensor(0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4)

Заполнение данными в словарях и аргументах функций

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

Операторы раскрытия словаря или диапазона создают ограниченное количество значений, но за счет этого их можно использовать не только для инициализации размерных тензоров, но и как начальные значений у словарей или аргументы при вызове функций, причем в произвольном месте, а не только в конце списка инициализации:

  • :Tensor( ... 0..0.99..0.1 ) - заполнение тензора без указания его размера элементами из диапазона будет равносильно записи :Tensor(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)

  • funtcion( 0, ... dict, 42 ) - раскрытие словаря как индивидуальных не именованных аргументов функции, к примеру funtcion( 0, 1, 2, 3, 42), если словарь dict содержал три элемента 1, 2 и 3.

  • funtcion( ... ... dict, end=4) - раскрытие словаря как индивидуальных именованных аргументов функции. Если словарь dict будет содержать элементы (1, two=2, three=3,), то это будет равносильно вызову функции funtcion( 1, two=2, three=3, end=4).

  • funtcion( ... ... dict, end=4) - раскрытие словаря как индивидуальных именованных аргументов функции. Если словарь dict будет содержать элементы (1, two=2, three=3,), то это будет равносильно вызову функции funtcion( 1, two=2, three=3, end=4).

  • funtcion( ... :Tensor[5](... rand() ...) ) - раскрытие тензора с пятью случайными значениями в качестве аргументов при вызове функции.