Контроль ссылок

Контроль ссылок - одна из ключевых особенностей Ü, позволяющих обеспечить отсутствие ошибок в программах. Данный механизм позволяет на этапе компиляции обнаружить ошибки обращения к освобождённой памяти, ошибки алиазинга, ошибки с висящими ссылками и т. д.

Правила контроля ссылок

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

Производные ссылки

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

fn Foo()
{
    var i32 x = 0;
    var i32 &y= x; // "у" - производная ссылка от "x"
    var i32 &z= y; // "z" - производная ссылка от "y"
}

Ссылка на член массива считается производной ссылкой от ссылки/переменной массива.

fn Foo()
{
    var [ f64, 4 ] a= zero_init;
    var f64 &a_ref= a[2]; // "a_ref" - производная ссылка от "a"
}

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

fn Pass( f32 &mut x ) : f32 &mut
{
    return x;
}

fn Min( i32 & a, i32 & b ) : i32 &
{
    if( a < b ) { return a; }
    return b;
}

fn Foo()
{
    var f32 mut f= 0.5f;
    var f32 &mut f_ref= Pass(f); // "f_ref" - производная ссылка от "f"

    var i32 a= 8, b= 7;
    var i32 &ab_ref= Min(a, b); // "ab_ref" - производная ссылка сразу от двух переменных - "a" и "b"
}

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

struct S{ i32& r; }
fn Foo()
{
    var i32 x= 0;
    var S s{ .r= x }; // "s.r" - производная ссылка от "x"
    var i32& r2= s.r; // "r2" - производная ссылка от ссылки "s.r"
}

Дочерние ссылки

Концепт дочерних ссылок несколько отличается от производных ссылок. Дочерние ссылки - это ссылки на нессылочные поля структур и классов и элементы кортежей. Главное отличие дочерних ссылок от производных заключается в том, что можно иметь более одной изменяемой ссылки на одну переменную, если это дочерние ссылки на разные её члены (поля или элементы для кортежей). Это позволяет, например, одновременно менять разные поля одного и того же экземпляра структуры.

struct S{ i32 x; i32 y; }
fn Swap( i32 &mut a, i32 &mut b );
fn Foo()
{
    var S mut s= zero_init;
    var tup[i32, i32] mut t= zero_init;
    var i32 &mut x_ref= s.x; // Создана первая дочерняя изменяемая ссылка на поле "x" структуры.
    var i32 &mut y_ref= s.y; // Ok - создана вторая изменяемая тот же экземпляр структуры, но на другое поле "y".
    Swap( t[0], t[1] ); // Одновременно изменяем разные элементы одного и того же экземпляра кортежа.
}

Управление производными ссылками в функциях

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

После указания ссылочного модификатора возвращаемого значения можно указать символ @ с последующим выражением в (). Выражение должно быть constexpr и быть массивом элементов типа [ char8, 2 ]. Каждый элемент этого массива - это указание входной ссылки функции в специальном формате. Первое значение - символ от 0 до 9, обозначающий номер параметра функции. Второе значение - или _ для обозначение ссылки на ссылочный аргумент или символ в диапазоне от a до z, обозначающий соответствующий внутренний ссылочный тег типа соответствующего параметра. Весь массив, указанный в выражении возвращаемой ссылки, тем самым обозначает потенциальный набор ссылок, которые возвращает функция.

var [ [ char8, 2 ], 1 ] return_references_foo[ "0_" ];
fn Foo( i32 & a, i32 & b ) : i32 & @(return_references_foo); // Данная функция возвращает производную ссылку только от аргумента "a"
var [ [ char8, 2 ], 2 ] return_references_bar[ "0_", "2_" ];
fn Bar( f32 & a, f32 & b, f32 & c ) : f32 & @(return_references_bar); // Данная функция возвращает производную ссылку только от аргументов "a" и "c"

fn Baz()
{
    var i32 i0= 0, i1= 0;
    var f32 f0= 0.0f, f1= 0.0f, f2= 0.0f;
    var i32 &i_ref= Foo(i0, i1); // "i_ref" - производная ссылка от переменной "i0", но не от "i1"
    var f32 &f_ref= Bar(f0, f1, f2); // "f_ref" - производная ссылка от "f0" и "f2", но не от "f1"
}

Компилятор проверяет, что возвращаются только разрешённые ссылки:

var [ [ char8, 2 ], 1 ] return_references[ "0_" ];
fn Foo( i32 & a, i32 & b ) : i32 & @(return_references)
{
   return b; // Будет порождена ошибка - возвращение недозволенной ссылки
}

Через указание выражения внутри @() после имени типа можно указать возвращаемые внутренние ссылки. Выражение для этого должно быть кортежем массивов [ char8, 2 ]. Каждый элемент кортежа обозначает набор ссылок для соответствующего внутреннего ссылочного тега возвращаемого типа.

struct S{ i32& r; }

var tup[ [ [ char8, 2 ], 2 ] ] return_inner_references[ [ "0_", "1a" ] ];
fn Foo( i32 & a, S s, i32 & z ) : S @(return_inner_references)
{
    if( a > s.r && z != 0 )
    {
        var S ret{ .r= a };
        return ret;
    }
    else
    {
        var S ret{ .r= s.r };
        return ret;
    }
}

Связывание ссылок

Некоторые функции могут создавать производные ссылки от своих аргументов внутри других аргументов. Это называется связыванием ссылок. Для функции, осуществляющей связывание ссылок, надо указать выражения связывания через @() - сразу после списка аргументов. Выражение должно являться constexpr массивом элементов типа [ [ char8, 2 ], 2 ]. Каждый элемент это пара описаний ссылок функции - для назначения и для источника. Ссылки функции обозначаются так же, как и в нотации возвращаемых ссылок.

struct S{ i32& r; }
var [ [ [ char8, 2 ], 2 ], 1 ] pollution[ [ "0a", "1_" ] ];
fn Foo( S &mut s, i32& r ) @(pollution); // Функция создаёт производную ссылку от аргумента "r" внутри аргумента "s".

fn Bar()
{
    var i32 x= 0, y= 0;
    var S mut s{ .r= x }; // "s.r" является производной ссылкой от "x"
    Foo( s, y ); // Теперь "s.r" является производной ссылкой ещё и от "y"
}

Если функция фактически осуществляет связывание ссылок для своих аргументов, но в её заголовке это не указано - компилятор породит ошибку.

struct S{ i32& r; }
var [ [ [ char8, 2 ], 2 ], 1 ] pollution[ [ "0a", "1_" ] ];
fn Foo( S &mut s, i32& r ) @(pollution); // Функция создаёт производную ссылку от аргумента "r" внутри аргумента "s".

fn Bar( S &mut s, i32 & r )
{
    Foo(s, r); // Будет порождена ошибка - недозволенное связывание ссылок
}

Для конструкторов копирования и операторов копирующего присваивания нотацию связывания ссылок указывать нельзя. Компилятор самостоятельно генерирует для них эту нотацию, в соответствии с семантикой копирования.

Ссылочная нотация для полей

Структуры и классы могут также хранить внутри себя ссылки. И эти ссылки надо как-то статически отслеживать. Поэтому компилятор внутри себя для переменных таких типов создаёт логические ссылки (т. н. ссылочные теги).

Структура без ссылочных полей и без полей, у которых есть внутри ссылочные поля, имеет 0 внутренних ссылочных тегов. Структура, у которой есть одно ссылочное поле, имеет внутри себя 1 ссылочный тег. Структура, содержащая внутри себя одно поле с количеством ссылочных тегов N отличным от нуля, сама имеет N ссылочных тегов.

Сложнее дело обстоит со структурами, содержащими несколько ссылочных полей и/или полей со ссылками внутри. Для того, чтобы осуществлять отображение этих ссылок на ссылочные теги структуры, существует специальная нотация. Она даже необходима в таком случае, компилятор породит ошибку, если она отсутствует.

Для ссылочных полей после модификатора ссылочности можно указать выражение в @(). Выражение должно быть constexpr и иметь тип char8. Допустимые значения - символы от a до z - нумерующие соответствующий ссылочный тег структуры. Данное выражение позволяет проассоциировать ссылочное поле с ссылочными тегами структуры.

Для полей можно указать выражение @() после имени типа поля. Выражение должно быть constexpr и быть массивом элементов типа char8. Допустимые значения - символы от a до z - нумерующие соотвествующий ссылочный тег структуры. Данное выражение позволяет сопоставить внутренние ссылки поля с ссылочными тегами структуры.

В итоге структура будет иметь количество ссылочных тегов на 1 большее номера максимального используемого тега. Но при этом пропуск тэгов не допускается.

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

struct S
{
    i32& @("a"c8) x; // Ссылка указывает на тег "a" (#0).
    i32& @("b"c8) y; // Ссылка указывает на тег "b" (#1).
}
static_assert( typeinfo</S/>.reference_tag_count == 2s );

struct T
{
    f32 &mut @("a"c8) f;
    bool& @("b"c8) b;
    // Отображаем теги "c" (#3) и "d" (#4) на внутренние ссылки "S".
    S @("cd") s;
    // Отображаем тег "e" (#5) на две разные ссылки.
    u64& @("e"c8) i0;
    i64& @("e"c8) i1;
}
static_assert( typeinfo</T/>.reference_tag_count == 5s );

// Функция возвращает структуру, разные внутренние ссылки которой указывают на разные ссылочные аргументы.
// Ссылка "x", помеченная тегом "a" (#0), будет указывать на ссылочный аргумент "x".
// Ссылка "y", помеченная тегом "b" (#1), будет указывать на ссылочный аргумент "y".
var tup[ [ [ char8, 2 ], 1 ], [ [ char8, 2 ], 1 ] ] return_inner_references[ [ "0_" ], [ "1_" ] ];
fn MakeS( i32& x, i32& y ) : S @(return_inner_references)
{
    var S s{ .x= x, .y= y };
    return s;
}

// Функция записывает ссылку на аргумент "i" в ссылочный тег "e" аргумента "t".
// Этому тегу соответствует ссылочное поле "i0".
var [ [ [ char8, 2 ], 2 ], 1 ] pollution_seti0[ [ "0e", "1_" ] ];
fn Seti0( T &mut t, u64& i ) @(pollution_seti0);

// Функция записывает ссылку на аргумент "i" в ссылочный тег "d" аргумента "t".
// Этому тегу соответствует тег "b" (#1) поля "s", которому, в свою очередь, соответствует ссылочное поле "y" структуры "S".
var [ [ [ char8, 2 ], 2 ], 1 ] pollution_setsy[ [ "0d", "1_" ] ];
fn SetSy( T &mut t, u32& i ) @(pollution_setsy);

Обнаружение нарушения правила контроля ссылок

В примерах ниже отражено, как правило контроля ссылок осуществляется на практике.

fn Foo()
{
    var i32 mut x= 0;
    var i32 &mut r0= x; // "r0" - изменяемая производная ссылка от "x"
    var i32 &imut r1= x; // Создание производной от "x" ссылки, когда уже существует производная изменяемая ссылка. Будет порождена ошибка.
}
fn Foo()
{
    var f32 mut x= 0.0f;
    var f32 &imut r0= x; // "r0" - неизменяемая производная ссылка от "x"
    var f32 &mut r1= x; // Создание изменяемой производной от "x" ссылки, когда уже существует производная ссылка. Будет порождена ошибка.
}
fn MutateArgs( f64 &mut a, f64 &mut b );

fn Foo()
{
    var f64 mut x= 0.0;
    MutateArgs( x, x ); // Будет порождена ошибка. Для вызова функции одновременно создаются две производные от переменной "x" ссылки.
}

Обнаружение нарушения времени жизни

Контроль ссылок также позволяет отловить ошибки висящих ссылок, когда время жизни переменной истекло.

struct S{ i32& r; }
var [ [ [ char8, 2 ], 2 ], 1 ] pollution[ [ "0a", "1_" ] ];
fn Foo( S &mut s, i32& r ) @(pollution); // Функция создаёт производную ссылку от аргумента "r" внутри аргумента "s".

fn Bar()
{
    var i32 x= 0;
    var S mut s{ .r= x };
    {
        var i32 y= 0;
        Foo( s, y );
    } // Будет порождена ошибка - переменная "y" всё ещё имеет ссылки на себя при разрушении.
}

Контроль ссылок не позволяет возвращать ссылки на локальные переменные.

fn Foo( i32& arg ) : i32 &
{
    var i32 x= 0;
    return x; // Будет порождена ошибка - переменная "x" всё ищё имеет ссылки на себя при разрушении.
}