Васянский перевод Using Cast Result Type Inference с Zig вики на GitHub

Васянский перевод Using Cast Result Type Inference с Zig вики на GitHub

@BratishkaErik, админ https://t.me/ziglang_ru

(на момент ревизии https://github.com/ziglang/zig/wiki/Using-Cast-Result-Type-Inference/4c0b15c196a12ee2caaeb858954ca6f8167f9352)

Недавно (сегодня утром), предложение #5909 было реализовано в компиляторе Zig (см. запрос на слияние). Это изменение улучщит читабельность и безопасность типов, но требует небольшого пояснения, чтобы понять, как правильно его использовать.

Result Locations and Types (Расположение и типы результатов)

Если вы уже давно используете Zig, то, скорее всего, уже встречались с семантикой расположения результата (Result Location Semantics, RLS). Идея заключается в том, что когда вы пишете выражение типа:

ptr.* = .{ .x = 1, .y = 2 };

временная анонимная структура справа (struct literal as rvalue) на самом деле не строится в памяти: вместо этого, выражение преобразовывается в:

ptr.*.x = 1;
ptr.*.y = 2;

Это преобразование является активной областью экспериментов в компиляторе Zig и служит для двух целей: оптимизация производительности и создание значений по конкретному адресу памяти (см. также связанное предложение).

В Zig также существует связанная с этим идея "выведения конечного типа выражения". Грубо говоря, идея заключается в том, что из окружающего контекста мы можем узнать, к какому типу нужно привести выражение. Например:

 1. const x: f32 = a + b;

Конечный тип выражения "a + b" равен равен типу "f32" ("a + b" будет приведено к T).

 2. @as(f32, a + b);

 Всё то же самое.

 3. return a + b;

Конечный тип выражения "a + b" равен типу результата функции (к примеру, для pub fn mul(a: u16, b: u16) u32 это будет u32).

 4. f(expr);

Конечный тип выражения "expr" равен типу первого параметра функции (для предыдущей функции это u16).

 5. x >> y;

Конечный тип y будет равен log2(кол-во битов в типе X) без дробной части. То есть, если x имеет тип u32, то он может вместить 32 бита и тип y будет равен log2(32) = 5 => u5.

 6. SomeStruct{ .field = expr };

Конеяный тип выражения "expr" будет равен типу поля "field".

Существует намного больше мест и выражений (к примеру "const x = if (boolean) 2 else 3"), где выражается эта "семантика расположения и типа результата выражения", но из-за частых, ломающих совместимость изменений это не документировано должным образом, но в будущем она будет размещена здесь. А пока что, любые вопросы по RLS или типам результатов можно задать в одном из многочисленных сообществ Zig.

Cast Builtins (встроенные функции для конвертации типов)

  • Приведение указателей: @addrSpaceCast, @alignCast и @ptrCast.
  • Приведение значений одного класса: @errSetCast, @floatCast и @intCast.
  • Конвертация из одного класса типов в другой: @intFromFloat, @enumFromInt, @floatFromInt и @ptrFromInt.
  • Прочие приведения: @truncate и @bitCast

Обратите внимание, что функция @as не затронута.

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

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


$ zig test foo.zigfoo.zig:2:15: error: @enumFromInt must have a known result type
    const x = @enumFromInt(123);
              ^
~
~
~
~
foo.zig:2:15: note: use @as to provide explicit result type


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

const x = @as(MyEnum, @enumFromInt(123));

Хоть это и работает как предыдущая версия @enumFromInt, строку довольно тяжело читать. Обычно, вам этот способ и не потребуется — конечный тип можно вывести разными способами, как показано в первом списке. Например, используя первый вариант (явно аннотировать тип для x):

const x: MyEnum = @enumFromInt(123);

Намного лучше! Это не длиннее и не сложнее, чем старый синтаксис, и стало немного понятнее, какой тип имеет сама константа x.

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

const S = struct { x: u32 };
var y: u64 = something();
const s: S = .{ .x = @intCast(u32, y) };

Проблема заключается в том, что последнюю строчку легко не заметить при рефакторинге: если тип поля x изменится (к примеру, с u32 на u48), а мы забудем изменить первый параметр для @intCast, это может вызвать целочисленное переполнение там, где (в теории) нам хватило бы размерности (т.е. мы конвертируем в u32, хотя могли бы и в u48). А так выглядит последняя строчка с новым синтаксисом:

const s: S = .{ .x = @intCast(y) };

Намного лучше! Мы всё еще явно приводим тип к менее ёмкому, как мы и хотели, но нам не нужно дублировать тип результата (т.к. он уже выведен из типа поля x). Это делает код немного чище и более устойчивым к рефакторингу.

По замыслу, большинство выражений должны выглядеть так: либо просто аннотировать тип для const или var, либо не указывать его вообще (вместо этого выводя его из окружающего контекста). (прим. переводчика: а в меньшинстве случаев использовать вместе с @as).

Pointer Casts (приведение указателей)

При изменении синтаксиса, одной из проблемных частей был первый параметр у встроенных функций @alignCast и @addrSpaceCast — вместо передачи полного типа, им передавались только численное значение выравнивания или адресное пространство. Более того, чаще всего они использовались не отдельно, а в цепочке вида@ptrCast(*T, @alignCast(@alignOf(T), ptr)). Как же нам изменить их без добавления излишних усложнений?

Для этого было решено изменить поведение функций для приведения указателей, так что они теперь немного отличаются от остальных приведений. Любое сочетание @ptrCast, @alignCast, @addrSpaceCast, @constCast и @volatileCast считаются компилятором одной операцией, с одним конечным типом. Например, вместо:

const ptr: *anyopaque = something();
const self = @ptrCast(*Self, @alignCast(@alignOf(Self), ptr));

Следует писать:

const ptr: *anyopaque = something();
const self: *Self = @ptrCast(@alignCast(ptr));

Считайте это не самой конвертацией, а разрешением конвертации: напр. @constCast(@alignCast(ptr)) означает "разрешается изменить выравнивание и убрать неизменяемость ptr"

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

zig fmt

Так как это изменение является одним из фундаментальных в языке и ломает большое количество кода на Zig, в zig fmt было добавлено простое автоматическое обновление старого синтаксиса в новый, если возможно. Обновление происходит очень просто, используя @as. Эта строка:

const y = @intCast(u8, x);

будет заменена на:

const y = @as(u8, @intCast(x));

Это выглядит довольно захламлённо и вы вряд ли захотите писать так сами, но зато код будет работать как прежде. Если ваш код довольно маленький и/или в нём редко используются задетые встроенные функции, вы можете обновиться вручную, используя более ясные варианты (напр. const x: u8).

В большинстве случаев автоматического обновления достаточно, но есть небольшие исключения:

@alignCast и @addrSpaceCast

Их невозможно обновить автоматически по причинам, указанным выше, поэтому zig fmt не затронет их. Вам придётся исправить их вручную (ошибка компиляции укажет на нужную строку)

Выравнивание в @ptrCast

У @ptrCast была одна примечательная особенность. До этого обновления, @ptrCast(*T, ptr) мог неявно сохранить увеличенное выравнивание у ptr, так что конечный тип был равен не *T, а *T align(увеличенный).

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

@truncate

У этой встроенной функции тоже была маленькая неизвестная особенность: при обрезании вектора, первым параметром передавался не тип самого вектора, а тип элемента вектора. Например:

const x: @Vector(2, u32) = .{ 1, 2 };
const y: @Vector(2, u8) = @truncate(u8, x);

Эти случаи для zig fmt эквивалентны простому обрезанию обычного скалярного типа, поэтому обновление ошибочно сконвертирует строку в @as(u8, @truncate(x)). Это гарантированно вызовет ошибку компиляции, поэтому вам придётся вручную исправить эти строки на нужный тип. Это поведение было выбрано потому, что такое обрезание встречается очень редко, поэтому в большинстве случаев это не будет проблемой.

Report Page