Spec-Zone.ru › Zig

Справочник по языку Zig

Введение

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

Надёжность
Корректная работа даже в крайних случаях, например при нехватке памяти.
Оптимальность
Написание программ так, чтобы они работали и выполнялись наилучшим образом.
Повторное использование
Один и тот же код работает в разных средах с различными ограничениями.
Удобство сопровождения
Точная передача намерений компилятору и другим программистам. Язык требует мало усилий для чтения кода и устойчив к изменению требований и сред.

Часто самый эффективный способ узнать что-то новое — посмотреть примеры, поэтому в этой документации показано, как использовать каждую возможность Zig. Документация размещена на одной странице, чтобы вы могли выполнять поиск средствами браузера.

Примеры кода в этом документе компилируются и проверяются в составе основного набора тестов Zig.

Этот HTML-документ не зависит от внешних файлов, поэтому его можно использовать автономно.

Стандартная библиотека Zig

У стандартной библиотеки Zig есть собственная документация.

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

Кроме того, документация стандартной библиотеки Zig поставляется с каждым дистрибутивом Zig. Её можно отобразить с помощью локального веб-сервера:

Оболочка
zig std

Привет, мир

hello.zig
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, World!\n");
}
Оболочка
$ zig build-exe hello.zig
$ ./hello
Hello, World!

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

hello_again.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, {s}!\n", .{"World"});
}
Оболочка
$ zig build-exe hello_again.zig
$ ./hello_again
Hello, World!

В этом случае ! можно опустить из типа возвращаемого значения main, поскольку функция не возвращает ошибок.

См. также:

  • Значения
  • Кортежи
  • @import
  • Ошибки
  • Точка входа
  • Кодировка исходного кода
  • try

Комментарии

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

Генерация документации всё ещё является экспериментальной возможностью; её можно запустить с помощью:

Оболочка
zig test -femit-docs main.zig
comments.zig
const print = @import("std").debug.print;

pub fn main() void {
    // Comments in Zig start with "//" and end at the next LF byte (end of line).
    // The line below is a comment and won't be executed.

    //print("Hello?", .{});

    print("Hello, world!\n", .{}); // another comment
}
Оболочка
$ zig build-exe comments.zig
$ ./comments
Hello, world!

В Zig нет многострочных комментариев (например, комментариев /* */ в C). Благодаря этому каждую строку кода в Zig можно токенизировать вне контекста.

Комментарии документации

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

doc_comments.zig
/// A structure for storing a timestamp, with nanosecond precision (this is a
/// multiline doc comment).
const Timestamp = struct {
    /// The number of seconds since the epoch (this is also a doc comment).
    seconds: i64, // signed so we can represent pre-1970 (not a doc comment)
    /// The number of nanoseconds past the second (doc comment again).
    nanos: u32,

    /// Returns a `Timestamp` struct representing the Unix epoch; that is, the
    /// moment of 1970 Jan 1 00:00:00 UTC (this is a doc comment too).
    pub fn unixEpoch() Timestamp {
        return Timestamp{
            .seconds = 0,
            .nanos = 0,
        };
    }
};

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

invalid_doc-comment.zig
/// doc-comment
//! top-level doc-comment
const std = @import("std");
Оболочка
$ zig build-obj invalid_doc-comment.zig
/home/andy/src/zig/doc/langref/invalid_doc-comment.zig:1:16: error: expected type expression, found 'a document comment'
/// doc-comment
               ^

unattached_doc-comment.zig
pub fn main() void {}

/// End of file
Оболочка
$ zig build-obj unattached_doc-comment.zig
/home/andy/src/zig/doc/langref/unattached_doc-comment.zig:3:1: error: unattached documentation comment
/// End of file
^~~~~~~~~~~~~~~

Комментарии документации могут чередоваться с обычными комментариями, которые игнорируются.

Комментарии документации верхнего уровня

Комментарий документации верхнего уровня начинается с двух косых черт и восклицательного знака: //!; он документирует текущий модуль.

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

tldoc_comments.zig
//! This module provides functions for retrieving the current date and
//! time with varying degrees of precision and accuracy. It does not
//! depend on libc, but will use functions from it if available.

const S = struct {
    //! Top level comments are allowed inside a container other than a module,
    //! but it is not very useful.  Currently, when producing the package
    //! documentation, these comments are ignored.
};

Идентификаторы

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

Синтаксис строковых идентификаторов

Если требуется имя, не соответствующее этим требованиям, например для связывания с внешними библиотеками, можно использовать синтаксис @"".

identifiers.zig
const @"identifier with spaces in it" = 0xff;
const @"1SmallStep4Man" = 112358;

const c = @import("std").c;
pub extern "c" fn @"error"() void;
pub extern "c" fn @"fstat$INODE64"(fd: c.fd_t, buf: *c.Stat) c_int;

const Color = enum {
    red,
    @"really red",
};
const color: Color = .@"really red";

Значения

values.zig
// Top-level declarations are order-independent:
const print = std.debug.print;
const std = @import("std");
const os = std.os;
const assert = std.debug.assert;

// Custom error set definition:
const ExampleErrorSet = error{
    ExampleErrorVariant,
};

pub fn main() void {
    // integers
    const one_plus_one: i32 = 1 + 1;
    print("1 + 1 = {}\n", .{one_plus_one});

    // floats
    const seven_div_three: f32 = 7.0 / 3.0;
    print("7.0 / 3.0 = {}\n", .{seven_div_three});

    // boolean
    print("{}\n{}\n{}\n", .{
        true and false,
        true or false,
        !true,
    });

    // optional
    var optional_value: ?[]const u8 = null;
    assert(optional_value == null);

    print("\noptional 1\ntype: {}\nvalue: {?s}\n", .{
        @TypeOf(optional_value), optional_value,
    });

    optional_value = "hi";
    assert(optional_value != null);

    print("\noptional 2\ntype: {}\nvalue: {?s}\n", .{
        @TypeOf(optional_value), optional_value,
    });

    // error union
    var number_or_error: ExampleErrorSet!i32 = ExampleErrorSet.ExampleErrorVariant;

    print("\nerror union 1\ntype: {}\nvalue: {!}\n", .{
        @TypeOf(number_or_error),
        number_or_error,
    });

    number_or_error = 1234;

    print("\nerror union 2\ntype: {}\nvalue: {!}\n", .{
        @TypeOf(number_or_error), number_or_error,
    });
}
Оболочка
$ zig build-exe values.zig
$ ./values
1 + 1 = 2
7.0 / 3.0 = 2.3333333
false
true
false

optional 1
type: ?[]const u8
value: null

optional 2
type: ?[]const u8
value: hi

error union 1
type: error{ExampleErrorVariant}!i32
value: error.ExampleErrorVariant

error union 2
type: error{ExampleErrorVariant}!i32
value: 1234

Примитивные типы

Примитивные типы
Тип Эквивалент в C Описание
i8 int8_t знаковое 8-битное целое число
u8 uint8_t беззнаковое 8-битное целое число
i16 int16_t знаковое 16-битное целое число
u16 uint16_t беззнаковое 16-битное целое число
i32 int32_t знаковое 32-битное целое число
u32 uint32_t беззнаковое 32-битное целое число
i64 int64_t знаковое 64-битное целое число
u64 uint64_t беззнаковое 64-битное целое число
i128 __int128 знаковое 128-битное целое число
u128 unsigned __int128 беззнаковое 128-битное целое число
isize intptr_t знаковое целое число размером с указатель
usize uintptr_t, size_t беззнаковое целое число размером с указатель. См. также #5185
c_char char для совместимости ABI с C
c_short short для совместимости ABI с C
c_ushort unsigned short для совместимости ABI с C
c_int int для совместимости ABI с C
c_uint unsigned int для совместимости ABI с C
c_long long для совместимости ABI с C
c_ulong unsigned long для совместимости ABI с C
c_longlong long long для совместимости ABI с C
c_ulonglong unsigned long long для совместимости ABI с C
c_longdouble long double для совместимости ABI с C
f16 _Float16 16-битное число с плавающей точкой (мантисса 10 бит), IEEE-754-2008 binary16
f32 float 32-битное число с плавающей точкой (мантисса 23 бита), IEEE-754-2008 binary32
f64 double 64-битное число с плавающей точкой (мантисса 52 бита), IEEE-754-2008 binary64
f80 long double 80-битное число с плавающей точкой (мантисса 64 бита), расширенная точность IEEE-754-2008, 80 бит
f128 _Float128 128-битное число с плавающей точкой (мантисса 112 бит), IEEE-754-2008 binary128
bool bool true или false
anyopaque void Используется для указателей со стёртым типом.
void (нет) Всегда имеет значение void{}
noreturn (нет) тип break, continue, return, unreachable и while (true) {}
type (нет) тип типов
anyerror (нет) код ошибки
comptime_int (нет) Допускается только для значений, известных во время comptime. Тип целочисленных литералов.
comptime_float (нет) Допускается только для значений, известных во время comptime. Тип литералов с плавающей точкой.

Помимо перечисленных выше целочисленных типов, можно использовать целые числа произвольной разрядности: для этого указывается идентификатор i или u, за которым следуют цифры. Например, идентификатор i7 обозначает знаковое 7-битное целое число. Максимальная допустимая разрядность целочисленного типа — 65535.

См. также:

  • Целые числа
  • Числа с плавающей точкой
  • void
  • Ошибки
  • @Int

Примитивные значения

Примитивные значения
Имя Описание
true и false значения bool
null используется для установки необязательного типа в null
undefined используется, чтобы оставить значение неопределённым

См. также:

  • Необязательные типы
  • undefined

Строковые литералы и литералы кодовых точек Unicode

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

Поскольку исходный код Zig имеет кодировку UTF-8, любые не-ASCII-байты в строковом литерале исходного кода сохраняют своё значение UTF-8 в строке программы на Zig; компилятор не изменяет эти байты. В строковый литерал можно включить байты, не являющиеся UTF-8, с помощью записи \xNN.

При индексировании строки, содержащей не-ASCII-байты, возвращаются отдельные байты независимо от того, образуют ли они допустимую последовательность UTF-8.

Литералы кодовых точек Unicode имеют тип comptime_int, как и целочисленные литералы. Все escape-последовательности допустимы как в строковых литералах, так и в литералах кодовых точек Unicode.

string_literals.zig
const print = @import("std").debug.print;
const mem = @import("std").mem; // will be used to compare bytes

pub fn main() void {
    const bytes = "hello";
    print("{}\n", .{@TypeOf(bytes)}); // *const [5:0]u8
    print("{d}\n", .{bytes.len}); // 5
    print("{c}\n", .{bytes[1]}); // 'e'
    print("{d}\n", .{bytes[5]}); // 0
    print("{}\n", .{'e' == '\x65'}); // true
    print("{d}\n", .{'\u{1f4a9}'}); // 128169
    print("{d}\n", .{'💯'}); // 128175
    print("{u}\n", .{'⚡'});
    print("{}\n", .{mem.eql(u8, "hello", "h\x65llo")}); // true
    print("{}\n", .{mem.eql(u8, "💯", "\xf0\x9f\x92\xaf")}); // also true
    const invalid_utf8 = "\xff\xfe"; // non-UTF-8 strings are possible with \xNN notation.
    print("0x{x}\n", .{invalid_utf8[1]}); // indexing them returns individual bytes...
    print("0x{x}\n", .{"💯"[1]}); // ...as does indexing part-way through non-ASCII characters
}
Оболочка
$ zig build-exe string_literals.zig
$ ./string_literals
*const [5:0]u8
5
e
0
true
128169
128175
⚡
true
true
0xfe
0x9f

См. также:

  • Массивы
  • Кодировка исходного кода

Escape-последовательности

Escape-последовательности
Escape-последовательность Название
\n Перевод строки
\r Возврат каретки
\t Табуляция
\\ Обратная косая черта
\' Одинарная кавычка
\" Двойная кавычка
\xNN шестнадцатеричное значение 8-битного байта (2 цифры)
\u{NNNNNN} шестнадцатеричное значение скалярной величины Unicode в кодировке UTF-8 (1 или более цифр)

Обратите внимание: максимальное допустимое значение скалярной величины Unicode — 0x10ffff.

Многострочные строковые литералы

В многострочных строковых литералах нет escape-последовательностей, и они могут занимать несколько строк. Чтобы начать многострочный строковый литерал, используйте токен \\. Как и комментарий, строковый литерал продолжается до конца строки. Конец строки не включается в строковый литерал. Однако если следующая строка начинается с \\, к строке добавляется символ перевода строки, и строковый литерал продолжается.

multiline_string_literals.zig
const hello_world_in_c =
    \\#include <stdio.h>
    \\
    \\int main(int argc, char **argv) {
    \\    printf("hello world\n");
    \\    return 0;
    \\}
;

См. также:

  • @embedFile

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

Используйте ключевое слово const, чтобы присвоить значение идентификатору:

constant_identifier_cannot_change.zig
const x = 1234;

fn foo() void {
    // It works at file scope as well as inside functions.
    const y = 5678;

    // Once assigned, an identifier cannot be changed.
    y += 1;
}

pub fn main() void {
    foo();
}
Оболочка
$ zig build-exe constant_identifier_cannot_change.zig
/home/andy/src/zig/doc/langref/constant_identifier_cannot_change.zig:8:5: error: cannot assign to constant
    y += 1;
    ^
referenced by:
    main: /home/andy/src/zig/doc/langref/constant_identifier_cannot_change.zig:12:8
    callMain [inlined]: /home/andy/src/zig/lib/std/start.zig:685:59
    callMainWithArgs [inlined]: /home/andy/src/zig/lib/std/start.zig:637:20
    posixCallMainAndExit: /home/andy/src/zig/lib/std/start.zig:590:38
    2 reference(s) hidden; use '-freference-trace=6' to see all references

const распространяется на все байты, на которые непосредственно указывает идентификатор. У указателей есть собственное свойство const.

Если нужна переменная, значение которой можно изменять, используйте ключевое слово var:

mutable_var.zig
const print = @import("std").debug.print;

pub fn main() void {
    var y: i32 = 5678;

    y += 1;

    print("{d}", .{y});
}
Оболочка
$ zig build-exe mutable_var.zig
$ ./mutable_var
5679

Переменные необходимо инициализировать:

var_must_be_initialized.zig
pub fn main() void {
    var x: i32;

    x = 1;
}
Оболочка
$ zig build-exe var_must_be_initialized.zig
/home/andy/src/zig/doc/langref/var_must_be_initialized.zig:2:15: error: expected '=', found ';'
    var x: i32;
              ^

undefined

Используйте undefined, чтобы оставить переменные неинициализированными:

assign_undefined.zig
const print = @import("std").debug.print;

pub fn main() void {
    var x: i32 = undefined;
    x = 1;
    print("{d}", .{x});
}
Оболочка
$ zig build-exe assign_undefined.zig
$ ./assign_undefined
1

undefined можно привести к любому типу. После этого уже невозможно определить, что значение равно undefined. undefined означает, что значение может быть любым, даже бессмысленным с точки зрения типа. В переводе на русский undefined означает: «Это не имеющее смысла значение. Использование этого значения будет ошибкой. Значение не будет использовано или будет перезаписано до использования».

В режимах Debug и ReleaseSafe Zig записывает байты 0xaa в неопределённую память. Это позволяет рано обнаруживать ошибки и помогает выявлять использование неопределённой памяти в отладчике. Однако такое поведение является лишь особенностью реализации, а не семантикой языка, поэтому код не гарантированно сможет его обнаружить.

Деструктуризация

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

destructuring_to_existing.zig
const print = @import("std").debug.print;

pub fn main() void {
    var x: u32 = undefined;
    var y: u32 = undefined;
    var z: u32 = undefined;

    const tuple = .{ 1, 2, 3 };

    x, y, z = tuple;

    print("tuple: x = {}, y = {}, z = {}\n", .{x, y, z});

    const array = [_]u32{ 4, 5, 6 };

    x, y, z = array;

    print("array: x = {}, y = {}, z = {}\n", .{x, y, z});

    const vector: @Vector(3, u32) = .{ 7, 8, 9 };

    x, y, z = vector;

    print("vector: x = {}, y = {}, z = {}\n", .{x, y, z});
}
Оболочка
$ zig build-exe destructuring_to_existing.zig
$ ./destructuring_to_existing
tuple: x = 1, y = 2, z = 3
array: x = 4, y = 5, z = 6
vector: x = 7, y = 8, z = 9

Выражение деструктуризации может находиться только внутри блока (то есть не на уровне контейнера). Левая часть присваивания должна состоять из списка элементов, разделённых запятыми; каждый элемент может быть lvalue (например, существующей переменной `var`) или объявлением переменной:

destructuring_mixed.zig
const print = @import("std").debug.print;

pub fn main() void {
    var x: u32 = undefined;

    const tuple = .{ 1, 2, 3 };

    x, var y : u32, const z = tuple;

    print("x = {}, y = {}, z = {}\n", .{x, y, z});

    // y is mutable
    y = 100;

    // You can use _ to throw away unwanted values.
    _, x, _ = tuple;

    print("x = {}", .{x});
}
Оболочка
$ zig build-exe destructuring_mixed.zig
$ ./destructuring_mixed
x = 1, y = 2, z = 3
x = 2

Перед деструктуризацией можно поставить ключевое слово comptime; в этом случае всё выражение деструктуризации вычисляется во время comptime. Все объявленные var будут comptime var, а все выражения (как расположения результатов, так и выражение присваиваемого значения) вычисляются во время comptime.

См. также:

  • Деструктуризация кортежей
  • Деструктуризация массивов
  • Деструктуризация векторов

Тестирование Zig

Код, написанный в одном или нескольких объявлениях test, можно использовать для проверки соответствия поведения ожиданиям:

testing_introduction.zig
const std = @import("std");

test "expect addOne adds one to 41" {

    // The Standard Library contains useful functions to help create tests.
    // `expect` is a function that verifies its argument is true.
    // It will return an error if its argument is false to indicate a failure.
    // `try` is used to return an error to the test runner to notify it that the test failed.
    try std.testing.expect(addOne(41) == 42);

    // However, in most cases it is more convenient to use a more specific function like `expectEqual`.
    // This gives you much clearer and more helpful error messages when a test fails.
    try std.testing.expectEqual(42, addOne(41));
}

test addOne {
    // A test name can also be written using an identifier.
    // This is a doctest, and serves as documentation for `addOne`.
    try std.testing.expectEqual(42, addOne(41));
}

/// The function `addOne` adds one to the number given as its argument.
fn addOne(number: i32) i32 {
    return number + 1;
}
Оболочка
$ zig test testing_introduction.zig
1/2 testing_introduction.test.expect addOne adds one to 41...OK
2/2 testing_introduction.decltest.addOne...OK
All 2 tests passed.

Пример кода testing_introduction.zig проверяет функцию addOne, чтобы убедиться, что при входном значении 41 она возвращает 42. С точки зрения этого теста функция addOne называется тестируемым кодом.

zig test — это инструмент для создания и запуска тестовой сборки. По умолчанию он собирает и запускает исполняемую программу, используя в качестве её основной точки входа стандартный исполнитель тестов, предоставляемый стандартной библиотекой Zig. Во время сборки объявления test, обнаруженные при разрешении указанного исходного файла Zig, включаются в сборку, чтобы стандартный исполнитель тестов мог выполнить их и вывести результаты.

В этой документации рассматриваются возможности стандартного исполнителя тестов, предоставляемого стандартной библиотекой Zig. Его исходный код находится в lib/compiler/test_runner.zig.

В приведённом выше выводе оболочки после команды zig test отображаются две строки. Стандартный исполнитель тестов выводит их в поток стандартной ошибки:

1/2 testing_introduction.test.expect addOne adds one to 41...
Такие строки показывают, какой тест из общего числа тестов выполняется в данный момент. В данном случае 1/2 означает, что выполняется первый из двух тестов. Обратите внимание: если стандартный поток ошибок программы-исполнителя тестов выводится в терминал, эти строки очищаются при успешном прохождении теста.
2/2 testing_introduction.decltest.addOne...
Если имя теста является идентификатором, стандартный исполнитель тестов использует текст decltest вместо test.
All 2 tests passed.
Эта строка показывает общее количество успешно пройденных тестов.

Объявления тестов

Объявления тестов содержат ключевое слово test, за которым следует необязательное имя в виде строкового литерала или идентификатора, а затем блок с любым допустимым кодом Zig, который разрешён в функции.

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

Объявления тестов похожи на функции: у них есть тип возвращаемого значения и блок кода. Неявный тип возвращаемого значения test — это объединение ошибок anyerror!void, и изменить его нельзя. Если исходный файл Zig собирается не с помощью инструмента zig test, объявления тестов исключаются из сборки.

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

См. также:

  • Глобальный набор ошибок
  • Грамматика

Документирующие тесты

Объявления тестов, названные с помощью идентификатора, называются документирующими тестами. Идентификатор должен ссылаться на другое объявление в текущей области видимости. Документирующий тест, как и комментарий документации, служит документацией для связанного объявления и отображается в сгенерированной документации этого объявления.

Эффективный документирующий тест должен быть самодостаточным и сосредоточенным на проверяемом объявлении, отвечать на вопросы, которые могут возникнуть у нового пользователя о его интерфейсе или предполагаемом использовании, и при этом не содержать лишних или запутывающих подробностей. Документирующий тест не заменяет комментарий документации, а дополняет его проверяемым примером, основанным на коде и выполняемым командой zig test.

Сбой теста

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

testing_failure.zig
const std = @import("std");

test "expect this to fail" {
    try std.testing.expect(false);
}

test "expect this to succeed" {
    try std.testing.expect(true);
}
Оболочка
$ zig test testing_failure.zig
1/2 testing_failure.test.expect this to fail...FAIL (TestUnexpectedResult)
/home/andy/src/zig/lib/std/testing.zig:615:14: 0x1234e39 in expect (std.zig)
    if (!ok) return error.TestUnexpectedResult;
             ^
/home/andy/src/zig/doc/langref/testing_failure.zig:4:5: 0x1234f2e in test.expect this to fail (testing_failure.zig)
    try std.testing.expect(false);
    ^
2/2 testing_failure.test.expect this to succeed...OK
1 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/8b3fa8da11c9a50847bac61c047fa567/test --seed=0xf0359694

Пропуск тестов

Один из способов пропустить тесты — отфильтровать их с помощью параметра командной строки --test-filter [text] команды zig test. При этом в тестовую сборку включаются только тесты, имя которых содержит указанный текст фильтра. Обратите внимание: безымянные тесты запускаются даже при использовании параметра командной строки --test-filter [text].

Чтобы программно пропустить тест, верните из test ошибку error.SkipZigTest — стандартный исполнитель тестов сочтёт тест пропущенным. После выполнения всех тестов будет сообщено общее количество пропущенных тестов.

testing_skip.zig
test "this will be skipped" {
    return error.SkipZigTest;
}
Оболочка
$ zig test testing_skip.zig
1/1 testing_skip.test.this will be skipped...SKIP
0 passed; 1 skipped; 0 failed.

Обнаружение утечек памяти

Если код выделяет память с помощью тестового аллокатора стандартной библиотеки Zig — std.testing.allocator, стандартный исполнитель тестов сообщит об обнаруженных утечках, связанных с использованием тестового аллокатора:

testing_detect_leak.zig
const std = @import("std");

test "detect leak" {
    const gpa = std.testing.allocator;
    var list: std.ArrayList(u21) = .empty;
    // missing `defer list.deinit(gpa);`
    try list.append(gpa, '☔');

    try std.testing.expectEqual(1, list.items.len);
}
Оболочка
$ zig test testing_detect_leak.zig
1/1 testing_detect_leak.test.detect leak...OK
[DebugAllocator] (err): memory address 0x7f1aed660000 leaked:
/home/andy/src/zig/lib/std/array_list.zig:1235:56: 0x1235ef7 in ensureTotalCapacityPrecise (std.zig)
                const new_memory = try gpa.alignedAlloc(T, alignment, new_capacity);
                                                       ^
/home/andy/src/zig/lib/std/array_list.zig:1211:51: 0x1235b89 in ensureTotalCapacity (std.zig)
            return self.ensureTotalCapacityPrecise(gpa, growCapacity(new_capacity));
                                                  ^
/home/andy/src/zig/lib/std/array_list.zig:1265:41: 0x12351bd in addOne (std.zig)
            try self.ensureTotalCapacity(gpa, newlen);
                                        ^
/home/andy/src/zig/lib/std/array_list.zig:904:49: 0x1235057 in append (std.zig)
            const new_item_ptr = try self.addOne(gpa);
                                                ^
/home/andy/src/zig/doc/langref/testing_detect_leak.zig:7:20: 0x1234ef1 in test.detect leak (testing_detect_leak.zig)
    try list.append(gpa, '☔');
                   ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^

All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/62b2a76cc0e431025399bbc6c22d6f72/test --seed=0x75bfba04

См. также:

  • defer
  • Память

Определение тестовой сборки

Используйте переменную компиляции @import("builtin").is_test, чтобы определить тестовую сборку:

testing_detect_test.zig
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;

test "builtin.is_test" {
    try expect(isATest());
}

fn isATest() bool {
    return builtin.is_test;
}
Оболочка
$ zig test testing_detect_test.zig
1/1 testing_detect_test.test.builtin.is_test...OK
All 1 tests passed.

Вывод тестов и ведение журнала

Стандартный исполнитель тестов и пространство имён тестирования стандартной библиотеки Zig выводят сообщения в поток стандартной ошибки.

Пространство имён Testing

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

testing_namespace.zig
const std = @import("std");

test "expectEqual demo" {
    const expected: i32 = 42;
    const actual = 42;

    // The first argument to `expectEqual` is the known, expected, result.
    // The second argument is the result of some expression.
    // The actual's type is casted to the type of expected.
    try std.testing.expectEqual(expected, actual);
}

test "expectError demo" {
    const expected_error = error.DemoError;
    const actual_error_union: anyerror!void = error.DemoError;

    // `expectError` will fail when the actual error is different than
    // the expected error.
    try std.testing.expectError(expected_error, actual_error_union);
}
Оболочка
$ zig test testing_namespace.zig
1/2 testing_namespace.test.expectEqual demo...OK
2/2 testing_namespace.test.expectError demo...OK
All 2 tests passed.

В стандартной библиотеке Zig также есть функции для сравнения срезов, строк и других значений. Другие доступные функции можно найти в пространстве имён std.testing стандартной библиотеки Zig.

Документация инструмента тестирования

У zig test есть несколько параметров командной строки, влияющих на компиляцию. Полный список см. в выводе команды zig test --help.

Переменные

Переменная — это область хранения в памяти.

При объявлении переменной обычно предпочтительнее использовать const, а не var. Это облегчает чтение кода как людям, так и компьютерам и создаёт больше возможностей для оптимизации.

Переменным запрещено затенять идентификаторы из внешней области видимости.

Для связывания с переменной, экспортированной из другого объектного файла, можно использовать ключевое слово extern или встроенную функцию @extern. Чтобы сделать переменную доступной другим объектным файлам на этапе компоновки, можно использовать ключевое слово export или встроенную функцию @export. В обоих случаях тип переменной должен быть совместим с ABI C.

См. также:

  • Экспорт библиотеки C

Переменные уровня контейнера

Переменные уровня контейнера имеют статическое время жизни, не зависят от порядка объявления и анализируются отложенно. Начальное значение переменных уровня контейнера неявно вычисляется во время comptime. Если переменная уровня контейнера имеет значение const, её значение известно во время comptime; в противном случае оно известно только во время выполнения.

test_container_level_variables.zig
var y: i32 = add(10, x);
const x: i32 = add(12, 34);

test "container level variables" {
    try expectEqual(46, x);
    try expectEqual(56, y);
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}

const std = @import("std");
const expectEqual = std.testing.expectEqual;
Оболочка
$ zig test test_container_level_variables.zig
1/1 test_container_level_variables.test.container level variables...OK
All 1 tests passed.

Переменные уровня контейнера можно объявлять внутри struct, union, enum или opaque:

test_namespaced_container_level_variable.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "namespaced container level variable" {
    try expectEqual(1235, foo());
    try expectEqual(1236, foo());
}

const S = struct {
    var x: i32 = 1234;
};

fn foo() i32 {
    S.x += 1;
    return S.x;
}
Оболочка
$ zig test test_namespaced_container_level_variable.zig
1/1 test_namespaced_container_level_variable.test.namespaced container level variable...OK
All 1 tests passed.

Статические локальные переменные

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

test_static_local_variable.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "static local variable" {
    try expectEqual(1235, foo());
    try expectEqual(1236, foo());
}

fn foo() i32 {
    const S = struct {
        var x: i32 = 1234;
    };
    S.x += 1;
    return S.x;
}
Оболочка
$ zig test test_static_local_variable.zig
1/1 test_static_local_variable.test.static local variable...OK
All 1 tests passed.

Локальные переменные потока

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

test_thread_local_variables.zig
const std = @import("std");
const assert = std.debug.assert;

threadlocal var x: i32 = 1234;

test "thread local storage" {
    const thread1 = try std.Thread.spawn(.{}, testTls, .{});
    const thread2 = try std.Thread.spawn(.{}, testTls, .{});
    testTls();
    thread1.join();
    thread2.join();
}

fn testTls() void {
    assert(x == 1234);
    x += 1;
    assert(x == 1235);
}
Оболочка
$ zig test test_thread_local_variables.zig
1/1 test_thread_local_variables.test.thread local storage...OK
All 1 tests passed.

В однопоточных сборках все локальные переменные потока рассматриваются как обычные переменные уровня контейнера.

Локальные переменные потока не могут быть const.

Локальные переменные

Локальные переменные встречаются внутри функций, блоков comptime и блоков @cImport.

Если локальная переменная является const, это означает, что после инициализации значение переменной не изменится. Если значение инициализации переменной const известно на этапе comptime, то переменная также известна на этапе comptime.

Локальная переменная может быть объявлена с ключевым словом comptime. Это приводит к тому, что значение переменной становится известным на этапе comptime, а все чтения и записи переменной выполняются во время семантического анализа программы, а не во время выполнения. Все переменные, объявленные в выражении comptime, неявно являются переменными comptime.

test_comptime_variables.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "comptime vars" {
    var x: i32 = 1;
    comptime var y: i32 = 1;

    x += 1;
    y += 1;

    try expectEqual(2, x);
    try expectEqual(2, y);

    if (y != 2) {
        // This compile error never triggers because y is a comptime variable,
        // and so `y != 2` is a comptime value, and this if is statically evaluated.
        @compileError("wrong y value");
    }
}
Командная оболочка
$ zig test test_comptime_variables.zig
1/1 test_comptime_variables.test.comptime vars...OK
All 1 tests passed.

Целые числа

Целочисленные литералы

integer_literals.zig
const decimal_int = 98222;
const hex_int = 0xff;
const another_hex_int = 0xFF;
const octal_int = 0o755;
const binary_int = 0b11110000;

// underscores may be placed between two digits as a visual separator
const one_billion = 1_000_000_000;
const binary_mask = 0b1_1111_1111;
const permissions = 0o7_5_5;
const big_address = 0xFF80_0000_0000_0000;

Целочисленные значения времени выполнения

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

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

runtime_vs_comptime.zig
fn divide(a: i32, b: i32) i32 {
    return a / b;
}

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

Такие операторы, как + и -, приводят к недопустимому поведению при переполнении целого числа. Для всех целевых платформ предусмотрены альтернативные операторы для арифметики с циклическим переполнением и насыщением. +% и -% выполняют арифметику с циклическим переполнением, а +| и -| — арифметику с насыщением.

Zig поддерживает целые числа с произвольной разрядностью. Для ссылки на них используется идентификатор i или u, за которым следуют цифры. Например, идентификатор i7 обозначает знаковое 7-битное целое число. Максимальная допустимая разрядность целочисленного типа — 65535. Для знаковых целочисленных типов Zig использует представление в дополнительном коде.

См. также:

  • Операции с циклическим переполнением

Числа с плавающей запятой

Zig поддерживает следующие типы чисел с плавающей запятой:

  • f16 — IEEE-754-2008 binary16
  • f32 — IEEE-754-2008 binary32
  • f64 — IEEE-754-2008 binary64
  • f80 — IEEE-754-2008, расширенная точность 80 бит
  • f128 — IEEE-754-2008 binary128
  • c_longdouble — соответствует long double для целевого ABI языка C

Литералы с плавающей запятой

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

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

float_literals.zig
const floating_point = 123.0E+77;
const another_float = 123.0;
const yet_another = 123.0e+77;

const hex_floating_point = 0x103.70p-5;
const another_hex_float = 0x103.70;
const yet_another_hex_float = 0x103.70P-5;

// underscores may be placed between two digits as a visual separator
const lightspeed = 299_792_458.000_000;
const nanosecond = 0.000_000_001;
const more_hex = 0x1234_5678.9ABC_CDEFp-10;

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

float_special_values.zig
const std = @import("std");

const inf = std.math.inf(f32);
const negative_inf = -std.math.inf(f64);
const nan = std.math.nan(f128);

Операции с плавающей запятой

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

float_mode_obj.zig
const std = @import("std");
const big = @as(f64, 1 << 40);

export fn foo_strict(x: f64) f64 {
    return x + big - big;
}

export fn foo_optimized(x: f64) f64 {
    @setFloatMode(.optimized);
    return x + big - big;
}
Командная оболочка
$ zig build-obj float_mode_obj.zig -O ReleaseFast

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

float_mode_exe.zig
const print = @import("std").debug.print;

extern fn foo_strict(x: f64) f64;
extern fn foo_optimized(x: f64) f64;

pub fn main() void {
    const x = 0.001;
    print("optimized = {}\n", .{foo_optimized(x)});
    print("strict = {}\n", .{foo_strict(x)});
}

См. также:

  • @setFloatMode
  • Деление на ноль

Операторы

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

Таблица операторов

Название Синтаксис Типы Примечания Пример
Сложение
a + b
a += b
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к переполнению.
  • Для операндов применяется согласование типов операндов.
  • См. также @addWithOverflow.
2 + 5 == 7
Сложение с циклическим переполнением
a +% b
a +%= b
  • Целые числа
  • Поведение с циклическим переполнением в дополнительном коде.
  • Для операндов применяется согласование типов операндов.
  • См. также @addWithOverflow.
@as(u32, 0xffffffff) +% 1 == 0
Сложение с насыщением
a +| b
a +|= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
@as(u8, 255) +| 1 == @as(u8, 255)
Вычитание
a - b
a -= b
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к переполнению.
  • Для операндов применяется согласование типов операндов.
  • См. также @subWithOverflow.
2 - 5 == -3
Вычитание с циклическим переполнением
a -% b
a -%= b
  • Целые числа
  • Поведение с циклическим переполнением в дополнительном коде.
  • Для операндов применяется согласование типов операндов.
  • См. также @subWithOverflow.
@as(u8, 0) -% 1 == 255
Вычитание с насыщением
a -| b
a -|= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
@as(u32, 0) -| 1 == 0
Отрицание
-a
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к переполнению.
-1 == 0 - 1
Отрицание с циклическим переполнением
-%a
  • Целые числа
  • Поведение с циклическим переполнением в дополнительном коде.
-%@as(i8, -128) == -128
Умножение
a * b
a *= b
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к переполнению.
  • Для операндов применяется согласование типов операндов.
  • См. также @mulWithOverflow.
2 * 5 == 10
Умножение с циклическим переполнением
a *% b
a *%= b
  • Целые числа
  • Поведение с циклическим переполнением в дополнительном коде.
  • Для операндов применяется согласование типов операндов.
  • См. также @mulWithOverflow.
@as(u8, 200) *% 2 == 144
Умножение с насыщением
a *| b
a *|= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
@as(u8, 200) *| 2 == 255
Деление
a / b
a /= b
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к переполнению.
  • Для целых чисел может привести к делению на ноль.
  • Для чисел с плавающей запятой может привести к делению на ноль в режиме FloatMode.optimized.
  • Знаковые целочисленные операнды должны быть известны на этапе comptime и быть положительными. В остальных случаях используйте вместо этого @divTrunc, @divFloor или @divExact.
  • Для операндов применяется согласование типов операндов.
10 / 5 == 2
Деление с остатком
a % b
a %= b
  • Целые числа
  • Числа с плавающей запятой
  • Для целых чисел может привести к делению на ноль.
  • Для чисел с плавающей запятой может привести к делению на ноль в режиме FloatMode.optimized.
  • Знаковые операнды и операнды с плавающей запятой должны быть известны на этапе comptime и быть положительными. В остальных случаях используйте вместо этого @rem или @mod.
  • Для операндов применяется согласование типов операндов.
10 % 3 == 1
Сдвиг битов влево
a << b
a <<= b
  • Целые числа
  • Сдвигает все биты влево, заполняя младшие биты нулями.
  • b должно быть известно на этапе comptime либо иметь тип, количество битов которого равно log2 от a.
  • См. также @shlExact.
  • См. также @shlWithOverflow.
0b1 << 8 == 0b100000000
Сдвиг битов влево с насыщением
a <<| b
a <<|= b
  • Целые числа
  • См. также @shlExact.
  • См. также @shlWithOverflow.
@as(u8, 1) <<| 8 == 255
Сдвиг битов вправо
a >> b
a >>= b
  • Целые числа
  • Сдвигает все биты вправо, заполняя старшие биты нулями.
  • b должно быть известно на этапе comptime либо иметь тип, количество битов которого равно log2 от a.
  • См. также @shrExact.
0b1010 >> 1 == 0b101
Побитовое И
a & b
a &= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
0b011 & 0b101 == 0b001
Побитовое ИЛИ
a | b
a |= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
0b010 | 0b100 == 0b110
Побитовое исключающее ИЛИ
a ^ b
a ^= b
  • Целые числа
  • Для операндов применяется согласование типов операндов.
0b011 ^ 0b101 == 0b110
Побитовое НЕ
~a
  • Целые числа
~@as(u8, 0b10101111) == 0b01010000
Распаковка опционального значения со значением по умолчанию
a orelse b
  • Опциональные типы
Если a равно null, возвращает b («значение по умолчанию»), иначе возвращает распакованное значение a. Обратите внимание, что b может быть значением типа noreturn.
const value: ?u32 = null;
const unwrapped = value orelse 1234;
unwrapped == 1234
Распаковка опционального значения
a.?
  • Опциональные типы
Эквивалентно:
a orelse unreachable
const value: ?u32 = 5678;
value.? == 5678
Распаковка ошибки со значением по умолчанию
a catch b
a catch |err| b
  • Объединения ошибок
Если a является error, возвращает b («значение по умолчанию»), иначе возвращает распакованное значение a. Обратите внимание, что b может быть значением типа noreturn. err — это error, доступное в области видимости выражения b.
const value: anyerror!u32 = error.Broken;
const unwrapped = value catch 1234;
unwrapped == 1234
Логическое И
a and b
  • bool
Если a равно false, возвращает false, не вычисляя b. Иначе возвращает b.
(false and true) == false
Логическое ИЛИ
a or b
  • bool
Если a равно true, возвращает true, не вычисляя b. Иначе возвращает b.
(false or true) == true
Логическое НЕ
!a
  • bool
!false == true
Равенство
a == b
  • Целые числа
  • Числа с плавающей запятой
  • bool
  • type
  • упакованные структуры
Возвращает true, если a и b равны, иначе возвращает false. Для операндов применяется согласование типов операндов.
(1 == 1) == true
Проверка на null
a == null
  • Опциональные типы
Возвращает true, если a равно null, иначе возвращает false.
const value: ?u32 = null;
(value == null) == true
Неравенство
a != b
  • Целые числа
  • Числа с плавающей запятой
  • bool
  • type
Возвращает false, если a и b равны, иначе возвращает true. Для операндов применяется согласование типов операндов.
(1 != 1) == false
Проверка на ненулевое значение
a != null
  • Опциональные типы
Возвращает false, если a равно null, иначе возвращает true.
const value: ?u32 = null;
(value != null) == false
Больше
a > b
  • Целые числа
  • Числа с плавающей запятой
Возвращает true, если a больше b, иначе возвращает false. Для операндов применяется согласование типов операндов.
(2 > 1) == true
Больше или равно
a >= b
  • Целые числа
  • Числа с плавающей запятой
Возвращает true, если a больше или равно b, иначе возвращает false. Для операндов применяется согласование типов операндов.
(2 >= 1) == true
Меньше
a < b
  • Целые числа
  • Числа с плавающей запятой
Возвращает true, если a меньше b, иначе возвращает false. Для операндов применяется согласование типов операндов.
(1 < 2) == true
Меньше или равно
a <= b
  • Целые числа
  • Числа с плавающей запятой
Возвращает true, если a меньше или равно b, иначе возвращает false. Для операндов применяется согласование типов операндов.
(1 <= 2) == true
Объединение массивов
a ++ b
  • Массивы
  • Доступно только в том случае, если длины обоих массивов a и b известны во время компиляции.
const mem = @import("std").mem;
const array1 = [_]u32{1,2};
const array2 = [_]u32{3,4};
const together = array1 ++ array2;
mem.eql(u32, &together, &[_]u32{1,2,3,4})
Повторение массива
a ** b
  • Массивы
  • Доступно только в том случае, если длина a и b известна во время компиляции.
const mem = @import("std").mem;
const pattern = "ab" ** 3;
mem.eql(u8, pattern, "ababab")
Разыменование указателя
a.*
  • Указатели
Разыменование указателя.
const x: u32 = 1234;
const ptr = &x;
ptr.* == 1234
Взятие адреса
&a
Все типы
const x: u32 = 1234;
const ptr = &x;
ptr.* == 1234
Объединение множеств ошибок
a || b
  • Тип множества ошибок
Объединение множеств ошибок
const A = error{One};
const B = error{Two};
(A || B) == error{One, Two}

Приоритет

x() x[] x.y x.* x.?
a!b
x{}
!x -x -%x ~x &x ?x
* / % ** *% *| ||
+ - ++ +% -% +| -|
<< >> <<|
& ^ | orelse catch
== != < > <= >=
and
or
= *= *%= *|= /= %= += +%= +|= -= -%= -|= <<= <<|= >>= &= ^= |=

Массивы

test_arrays.zig
const expectEqual = @import("std").testing.expectEqual;
const assert = @import("std").debug.assert;
const mem = @import("std").mem;

// array literal
const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };

// alternative initialization using result location
const alt_message: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' };

comptime {
    assert(mem.eql(u8, &message, &alt_message));
}

// get the size of an array
comptime {
    assert(message.len == 5);
}

// A string literal is a single-item pointer to an array.
const same_message = "hello";

comptime {
    assert(mem.eql(u8, &message, same_message));
}

test "iterate over an array" {
    var sum: usize = 0;
    for (message) |byte| {
        sum += byte;
    }
    try expectEqual('h' + 'e' + 'l' * 2 + 'o', sum);
}

// modifiable array
var some_integers: [100]i32 = undefined;

test "modify an array" {
    for (&some_integers, 0..) |*item, i| {
        item.* = @intCast(i);
    }
    try expectEqual(10, some_integers[10]);
    try expectEqual(99, some_integers[99]);
}

// array concatenation works if the values are known
// at compile time
const part_one = [_]i32{ 1, 2, 3, 4 };
const part_two = [_]i32{ 5, 6, 7, 8 };
const all_of_it = part_one ++ part_two;
comptime {
    assert(mem.eql(i32, &all_of_it, &[_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }));
}

// remember that string literals are arrays
const hello = "hello";
const world = "world";
const hello_world = hello ++ " " ++ world;
comptime {
    assert(mem.eql(u8, hello_world, "hello world"));
}

// ** does repeating patterns
const pattern = "ab" ** 3;
comptime {
    assert(mem.eql(u8, pattern, "ababab"));
}

// initialize an array to zero
const all_zero = [_]u16{0} ** 10;

comptime {
    assert(all_zero.len == 10);
    assert(all_zero[5] == 0);
}

// use compile-time code to initialize an array
var fancy_array = init: {
    var initial_value: [10]Point = undefined;
    for (&initial_value, 0..) |*pt, i| {
        pt.* = Point{
            .x = @intCast(i),
            .y = @intCast(i * 2),
        };
    }
    break :init initial_value;
};
const Point = struct {
    x: i32,
    y: i32,
};

test "compile-time array initialization" {
    try expectEqual(4, fancy_array[4].x);
    try expectEqual(8, fancy_array[4].y);
}

// call a function to initialize an array
var more_points = [_]Point{makePoint(3)} ** 10;
fn makePoint(x: i32) Point {
    return Point{
        .x = x,
        .y = x * 2,
    };
}
test "array initialization with function calls" {
    try expectEqual(3, more_points[4].x);
    try expectEqual(6, more_points[4].y);
    try expectEqual(10, more_points.len);
}
Командная оболочка
$ zig test test_arrays.zig
1/4 test_arrays.test.iterate over an array...OK
2/4 test_arrays.test.modify an array...OK
3/4 test_arrays.test.compile-time array initialization...OK
4/4 test_arrays.test.array initialization with function calls...OK
All 4 tests passed.

См. также:

  • for
  • Срезы

Многомерные массивы

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

test_multidimensional_arrays.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const mat4x5 = [4][5]f32{
    [_]f32{ 1.0, 0.0, 0.0, 0.0, 0.0 },
    [_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 },
    [_]f32{ 0.0, 0.0, 1.0, 0.0, 0.0 },
    [_]f32{ 0.0, 0.0, 0.0, 1.0, 9.9 },
};
test "multidimensional arrays" {
    // mat4x5 itself is a one-dimensional array of arrays.
    try expectEqual(mat4x5[1], [_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 });

    // Access the 2D array by indexing the outer array, and then the inner array.
    try expectEqual(9.9, mat4x5[3][4]);

    // Here we iterate with for loops.
    for (mat4x5, 0..) |row, row_index| {
        for (row, 0..) |cell, column_index| {
            if (row_index == column_index) {
                try expectEqual(1.0, cell);
            }
        }
    }

    // Initialize a multidimensional array to zeros.
    const all_zero: [4][5]f32 = .{.{0} ** 5} ** 4;
    try expectEqual(0, all_zero[0][0]);
}
Командная оболочка
$ zig test test_multidimensional_arrays.zig
1/1 test_multidimensional_arrays.test.multidimensional arrays...OK
All 1 tests passed.

Массивы с завершающим sentinel-значением

Синтаксис [N:x]T описывает массив с sentinel-элементом, значение которого равно x, по индексу, соответствующему длине N.

test_null_terminated_array.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "0-terminated sentinel array" {
    const array = [_:0]u8{ 1, 2, 3, 4 };

    try expectEqual([4:0]u8, @TypeOf(array));
    try expectEqual(4, array.len);
    try expectEqual(0, array[4]);
}

test "extra 0s in 0-terminated sentinel array" {
    // The sentinel value may appear earlier, but does not influence the compile-time 'len'.
    const array = [_:0]u8{ 1, 0, 0, 4 };

    try expectEqual([4:0]u8, @TypeOf(array));
    try expectEqual(4, array.len);
    try expectEqual(0, array[4]);
}
Командная оболочка
$ zig test test_null_terminated_array.zig
1/2 test_null_terminated_array.test.0-terminated sentinel array...OK
2/2 test_null_terminated_array.test.extra 0s in 0-terminated sentinel array...OK
All 2 tests passed.

См. также:

  • Указатели с завершающим sentinel-значением
  • Срезы с завершающим sentinel-значением

Деструктуризация массивов

Массивы можно деструктурировать:

destructuring_arrays.zig
const print = @import("std").debug.print;

fn swizzleRgbaToBgra(rgba: [4]u8) [4]u8 {
    // readable swizzling by destructuring
    const r, const g, const b, const a = rgba;
    return .{ b, g, r, a };
}

pub fn main() void {
    const pos = [_]i32{ 1, 2 };
    const x, const y = pos;
    print("x = {}, y = {}\n", .{x, y});

    const orange: [4]u8 = .{ 255, 165, 0, 255 };
    print("{any}\n", .{swizzleRgbaToBgra(orange)});
}
Командная оболочка
$ zig build-exe destructuring_arrays.zig
$ ./destructuring_arrays
x = 1, y = 2
{ 0, 165, 255, 255 }

См. также:

  • Деструктуризация
  • Деструктуризация кортежей
  • Деструктуризация векторов

Векторы

Вектор — это группа логических значений, целых чисел, чисел с плавающей запятой или указателей, над которыми выполняются операции параллельно с использованием SIMD-инструкций, если это возможно. Типы векторов создаются встроенной функцией @Vector.

Векторы, как правило, поддерживают те же встроенные операторы, что и их базовые типы. Единственное исключение — ключевые слова `and` и `or` для векторов логических значений: эти операторы влияют на поток управления, что для векторов недопустимо. Все остальные операции выполняются поэлементно и возвращают вектор той же длины, что и входные векторы. К ним относятся:

  • Арифметические операции (+, -, /, *, @divFloor, @sqrt, @ceil, @log и т. д.)
  • Побитовые операторы (>>, <<, &, |, ~ и т. д.)
  • Операторы сравнения (<, >, == и т. д.)
  • Логическое НЕ (!)

Запрещено применять математический оператор к сочетанию скаляров (отдельных чисел) и векторов. В Zig есть встроенная функция @splat, позволяющая легко преобразовать скаляры в векторы; для преобразования векторов в скаляры предусмотрены @reduce и синтаксис индексирования массивов. Векторы также поддерживают присваивание из массивов фиксированной длины и обратно, если длина известна на этапе comptime.

Для перестановки элементов внутри векторов и между ними в Zig предусмотрены функции @shuffle и @select.

Операции над векторами, длина которых меньше собственного SIMD-размера целевой машины, обычно компилируются в одну SIMD-инструкцию; векторы, длина которых больше собственного SIMD-размера целевой машины, компилируются в несколько SIMD-инструкций. Если для данной операции в целевой архитектуре нет поддержки SIMD, компилятор по умолчанию обрабатывает элементы вектора по одному. Zig поддерживает любую длину вектора, известную на этапе comptime, вплоть до 2^32-1, хотя чаще всего используются небольшие степени двойки (2–64). Обратите внимание: чрезмерно большая длина вектора (например, 2^20) может приводить к сбоям компилятора в текущих версиях Zig.

test_vector.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "Basic vector usage" {
    // Vectors have a compile-time known length and base type.
    const a = @Vector(4, i32){ 1, 2, 3, 4 };
    const b = @Vector(4, i32){ 5, 6, 7, 8 };

    // Math operations take place element-wise.
    const c = a + b;

    // Individual vector elements can be accessed using array indexing syntax.
    try expectEqual(6, c[0]);
    try expectEqual(8, c[1]);
    try expectEqual(10, c[2]);
    try expectEqual(12, c[3]);
}

test "Conversion between vectors, arrays, and slices" {
    // Vectors can be coerced to arrays, and vice versa.
    const arr1: [4]f32 = [_]f32{ 1.1, 3.2, 4.5, 5.6 };
    const vec: @Vector(4, f32) = arr1;
    const arr2: [4]f32 = vec;
    try expectEqual(arr1, arr2);

    // You can also assign from a slice with comptime-known length to a vector using .*
    const vec2: @Vector(2, f32) = arr1[1..3].*;

    const slice: []const f32 = &arr1;
    var offset: u32 = 1; // var to make it runtime-known
    _ = &offset; // suppress 'var is never mutated' error
    // To extract a comptime-known length from a runtime-known offset,
    // first extract a new slice from the starting offset, then an array of
    // comptime-known length
    const vec3: @Vector(2, f32) = slice[offset..][0..2].*;
    try expectEqual(slice[offset], vec2[0]);
    try expectEqual(slice[offset + 1], vec2[1]);
    try expectEqual(vec2, vec3);
}
Командная оболочка
$ zig test test_vector.zig
1/2 test_vector.test.Basic vector usage...OK
2/2 test_vector.test.Conversion between vectors, arrays, and slices...OK
All 2 tests passed.

TODO: рассказать о взаимодействии с C ABI
TODO: рассмотреть возможность рекомендовать std.MultiArrayList

См. также:

  • @splat
  • @shuffle
  • @select
  • @reduce

Связь с массивами

Векторы и массивы имеют чётко определённое битовое расположение, поэтому поддерживают взаимное преобразование с помощью @bitCast. Приведение типов неявно выполняет @bitCast.

Байтовое расположение массивов определено, а векторов — нет, поэтому применение @ptrCast для преобразования между ними приводит к недопустимому поведению.

Деструктуризация векторов

Векторы можно деструктурировать:

destructuring_vectors.zig
const print = @import("std").debug.print;

// emulate punpckldq
pub fn unpack(x: @Vector(4, f32), y: @Vector(4, f32)) @Vector(4, f32) {
    const a, const c, _, _ = x;
    const b, const d, _, _ = y;
    return .{ a, b, c, d };
}

pub fn main() void {
    const x: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
    const y: @Vector(4, f32) = .{ 5.0, 6.0, 7.0, 8.0 };
    print("{}", .{unpack(x, y)});
}
Командная оболочка
$ zig build-exe destructuring_vectors.zig
$ ./destructuring_vectors
{ 1, 5, 2, 6 }

См. также:

  • Деструктуризация
  • Деструктуризация кортежей
  • Деструктуризация массивов

Указатели

В Zig есть два вида указателей: на один элемент и на несколько элементов.

  • *T — указатель ровно на один элемент.
    • Поддерживает синтаксис разыменования: ptr.*
    • Поддерживает синтаксис среза: ptr[0..1]
    • Поддерживает вычитание указателей: ptr - ptr
  • [*]T — указатель на неизвестное количество элементов.
    • Поддерживает синтаксис индексирования: ptr[i]
    • Поддерживает синтаксис среза: ptr[start..end] и ptr[start..]
    • Поддерживает арифметику указателей и целых чисел: ptr + int, ptr - int
    • Поддерживает вычитание указателей: ptr - ptr
    T должен иметь известный размер, а значит, не может быть anyopaque или любым другим непрозрачным типом.

Эти типы тесно связаны с массивами и срезами:

  • *[N]T — указатель на N элементов, эквивалентный указателю на один элемент — массив.
    • Поддерживает синтаксис индексирования: array_ptr[i]
    • Поддерживает синтаксис среза: array_ptr[start..end]
    • Поддерживает свойство len: array_ptr.len
    • Поддерживает вычитание указателей: array_ptr - array_ptr
  • []T — это срез (толстый указатель, содержащий указатель типа [*]T и длину).
    • Поддерживает синтаксис индексирования: slice[i]
    • Поддерживает синтаксис среза: slice[start..end]
    • Поддерживает свойство len: slice.len

Используйте &x, чтобы получить указатель на один элемент:

test_single_item_pointer.zig
const expectEqual = @import("std").testing.expectEqual;

test "address of syntax" {
    // Get the address of a variable:
    const x: i32 = 1234;
    const x_ptr = &x;

    // Dereference a pointer:
    try expectEqual(1234, x_ptr.*);

    // When you get the address of a const variable, you get a const single-item pointer.
    try expectEqual(*const i32, @TypeOf(x_ptr));

    // If you want to mutate the value, you'd need an address of a mutable variable:
    var y: i32 = 5678;
    const y_ptr = &y;
    try expectEqual(*i32, @TypeOf(y_ptr));
    y_ptr.* += 1;
    try expectEqual(5679, y_ptr.*);
}

test "pointer array access" {
    // Taking an address of an individual element gives a
    // single-item pointer. This kind of pointer
    // does not support pointer arithmetic.
    var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    const ptr = &array[2];
    try expectEqual(*u8, @TypeOf(ptr));

    try expectEqual(3, array[2]);
    ptr.* += 1;
    try expectEqual(4, array[2]);
}

test "slice syntax" {
    // Get a pointer to a variable:
    var x: i32 = 1234;
    const x_ptr = &x;

    // Convert to array pointer using slice syntax:
    const x_array_ptr = x_ptr[0..1];
    try expectEqual(*[1]i32, @TypeOf(x_array_ptr));

    // Coerce to many-item pointer:
    const x_many_ptr: [*]i32 = x_array_ptr;
    try expectEqual(1234, x_many_ptr[0]);
}
Командная оболочка
$ zig test test_single_item_pointer.zig
1/3 test_single_item_pointer.test.address of syntax...OK
2/3 test_single_item_pointer.test.pointer array access...OK
3/3 test_single_item_pointer.test.slice syntax...OK
All 3 tests passed.

Zig поддерживает арифметику указателей. Лучше присвоить указатель переменной [*]T и увеличивать значение этой переменной. Например, прямое увеличение указателя, полученного из среза, повредит его.

test_pointer_arithmetic.zig
const expectEqual = @import("std").testing.expectEqual;

test "pointer arithmetic with many-item pointer" {
    const array = [_]i32{ 1, 2, 3, 4 };
    var ptr: [*]const i32 = &array;

    try expectEqual(1, ptr[0]);
    ptr += 1;
    try expectEqual(2, ptr[0]);

    // slicing a many-item pointer without an end is equivalent to
    // pointer arithmetic: `ptr[start..] == ptr + start`
    try expectEqual(ptr[1..], ptr + 1);

    // subtraction between any two pointers except slices based on element size is supported
    try expectEqual(1, &ptr[1] - &ptr[0]);
}

test "pointer arithmetic with slices" {
    var array = [_]i32{ 1, 2, 3, 4 };
    var length: usize = 0; // var to make it runtime-known
    _ = &length; // suppress 'var is never mutated' error
    var slice = array[length..array.len];

    try expectEqual(1, slice[0]);
    try expectEqual(4, slice.len);

    slice.ptr += 1;
    // now the slice is in an bad state since len has not been updated

    try expectEqual(2, slice[0]);
    try expectEqual(4, slice.len);
}
Командная оболочка
$ zig test test_pointer_arithmetic.zig
1/2 test_pointer_arithmetic.test.pointer arithmetic with many-item pointer...OK
2/2 test_pointer_arithmetic.test.pointer arithmetic with slices...OK
All 2 tests passed.

В Zig обычно предпочтительнее использовать срезы, а не указатели с завершающим sentinel-значением. Массив или указатель можно преобразовать в срез с помощью синтаксиса среза.

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

test_slice_bounds.zig
const expectEqual = @import("std").testing.expectEqual;

test "pointer slicing" {
    var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    var start: usize = 2; // var to make it runtime-known
    _ = &start; // suppress 'var is never mutated' error
    const slice = array[start..4];
    try expectEqual(2, slice.len);

    try expectEqual(4, array[3]);
    slice[1] += 1;
    try expectEqual(5, array[3]);
}
Командная оболочка
$ zig test test_slice_bounds.zig
1/1 test_slice_bounds.test.pointer slicing...OK
All 1 tests passed.

Указатели работают и во время компиляции, если код не зависит от неопределённого расположения в памяти:

test_comptime_pointers.zig
const expectEqual = @import("std").testing.expectEqual;

test "comptime pointers" {
    comptime {
        var x: i32 = 1;
        const ptr = &x;
        ptr.* += 1;
        x += 1;
        try expectEqual(3, ptr.*);
    }
}
Командная оболочка
$ zig test test_comptime_pointers.zig
1/1 test_comptime_pointers.test.comptime pointers...OK
All 1 tests passed.

Чтобы преобразовать целочисленный адрес в указатель, используйте @ptrFromInt. Чтобы преобразовать указатель в целое число, используйте @intFromPtr:

test_integer_pointer_conversion.zig
const expectEqual = @import("std").testing.expectEqual;

test "@intFromPtr and @ptrFromInt" {
    const ptr: *i32 = @ptrFromInt(0xdeadbee0);
    const addr = @intFromPtr(ptr);
    try expectEqual(usize, @TypeOf(addr));
    try expectEqual(0xdeadbee0, addr);
}
Командная оболочка
$ zig test test_integer_pointer_conversion.zig
1/1 test_integer_pointer_conversion.test.@intFromPtr and @ptrFromInt...OK
All 1 tests passed.

Zig может сохранять адреса памяти в коде comptime, если указатель никогда не разыменовывается:

test_comptime_pointer_conversion.zig
const expectEqual = @import("std").testing.expectEqual;

test "comptime @ptrFromInt" {
    comptime {
        // Zig is able to do this at compile-time, as long as
        // ptr is never dereferenced.
        const ptr: *i32 = @ptrFromInt(0xdeadbee0);
        const addr = @intFromPtr(ptr);
        try expectEqual(usize, @TypeOf(addr));
        try expectEqual(0xdeadbee0, addr);
    }
}
Командная оболочка
$ zig test test_comptime_pointer_conversion.zig
1/1 test_comptime_pointer_conversion.test.comptime @ptrFromInt...OK
All 1 tests passed.

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

test_pointer_casting.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "pointer casting" {
    const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x12, 0x12, 0x12 };
    const u32_ptr: *const u32 = @ptrCast(&bytes);
    try expectEqual(0x12121212, u32_ptr.*);

    // Even this example is contrived - there are better ways to do the above than
    // pointer casting. For example, using a slice narrowing cast:
    const u32_value = std.mem.bytesAsSlice(u32, bytes[0..])[0];
    try expectEqual(0x12121212, u32_value);

    // And even another way, the most straightforward way to do it:
    try expectEqual(0x12121212, @as(u32, @bitCast(bytes)));
}

test "pointer child type" {
    // pointer types have a `child` field which tells you the type they point to.
    try expectEqual(u32, @typeInfo(*u32).pointer.child);
}
Командная оболочка
$ zig test test_pointer_casting.zig
1/2 test_pointer_casting.test.pointer casting...OK
2/2 test_pointer_casting.test.pointer child type...OK
All 2 tests passed.

См. также:

  • Опциональные указатели
  • @ptrFromInt
  • @intFromPtr
  • Указатели C

volatile

Предполагается, что чтение и запись не имеют побочных эффектов. Если определённая операция чтения или записи должна иметь побочные эффекты, например при вводе-выводе, отображённом на память (MMIO), используйте volatile. В следующем коде гарантируется выполнение всех операций чтения и записи с mmio_ptr в том же порядке, что и в исходном коде:

test_volatile.zig
const expectEqual = @import("std").testing.expectEqual;

test "volatile" {
    const mmio_ptr: *volatile u8 = @ptrFromInt(0x12345678);
    try expectEqual(*volatile u8, @TypeOf(mmio_ptr));
}
Командная оболочка
$ zig test test_volatile.zig
1/1 test_volatile.test.volatile...OK
All 1 tests passed.

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

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

У каждого типа есть выравнивание — число байтов, на которое должен без остатка делиться адрес памяти при загрузке значения этого типа из памяти или его сохранении в памяти. Чтобы узнать это значение для любого типа, можно использовать @alignOf.

Выравнивание зависит от архитектуры процессора, но всегда является степенью двойки и меньше 1 << 29.

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

test_variable_alignment.zig
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

test "variable alignment" {
    var x: i32 = 1234;

    try expectEqual(*i32, @TypeOf(&x));

    try expect(@intFromPtr(&x) % @alignOf(i32) == 0);

    // The implicitly-aligned pointer can be coerced to be explicitly-aligned to
    // the alignment of the underlying type `i32`:
    const ptr: *align(@alignOf(i32)) i32 = &x;

    try expectEqual(1234, ptr.*);
}
Оболочка
$ zig test test_variable_alignment.zig
1/1 test_variable_alignment.test.variable alignment...OK
All 1 tests passed.

Так же, как *i32 может быть приведён к *const i32, указатель с большим выравниванием может быть неявно преобразован в указатель с меньшим выравниванием, но не наоборот.

Для переменных и функций можно задать выравнивание. В этом случае указатели на них получат указанное выравнивание:

test_variable_func_alignment.zig
const expectEqual = @import("std").testing.expectEqual;

var foo: u8 align(4) = 100;

test "global variable alignment" {
    try expectEqual(4, @typeInfo(@TypeOf(&foo)).pointer.alignment);
    try expectEqual(*align(4) u8, @TypeOf(&foo));
    const as_pointer_to_array: *align(4) [1]u8 = &foo;
    const as_slice: []align(4) u8 = as_pointer_to_array;
    const as_unaligned_slice: []u8 = as_slice;
    try expectEqual(100, as_unaligned_slice[0]);
}

fn derp() align(@sizeOf(usize) * 2) i32 {
    return 1234;
}
fn noop1() align(1) void {}
fn noop4() align(4) void {}

test "function alignment" {
    try expectEqual(1234, derp());
    try expectEqual(fn () i32, @TypeOf(derp));
    try expectEqual(*align(@sizeOf(usize) * 2) const fn () i32, @TypeOf(&derp));

    noop1();
    try expectEqual(fn () void, @TypeOf(noop1));
    try expectEqual(*align(1) const fn () void, @TypeOf(&noop1));

    noop4();
    try expectEqual(fn () void, @TypeOf(noop4));
    try expectEqual(*align(4) const fn () void, @TypeOf(&noop4));
}
Оболочка
$ zig test test_variable_func_alignment.zig
1/2 test_variable_func_alignment.test.global variable alignment...OK
2/2 test_variable_func_alignment.test.function alignment...OK
All 2 tests passed.

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

test_incorrect_pointer_alignment.zig
const std = @import("std");

test "pointer alignment safety" {
    var array align(4) = [_]u32{ 0x11111111, 0x11111111 };
    const bytes = std.mem.sliceAsBytes(array[0..]);
    try std.testing.expectEqual(0x11111111, foo(bytes));
}
fn foo(bytes: []u8) u32 {
    const slice4 = bytes[1..5];
    const int_slice = std.mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
    return int_slice[0];
}
Оболочка
$ zig test test_incorrect_pointer_alignment.zig
1/1 test_incorrect_pointer_alignment.test.pointer alignment safety...thread 932714 panic: incorrect alignment
/home/andy/src/zig/doc/langref/test_incorrect_pointer_alignment.zig:10:68: 0x1235076 in foo (test_incorrect_pointer_alignment.zig)
    const int_slice = std.mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
                                                                   ^
/home/andy/src/zig/doc/langref/test_incorrect_pointer_alignment.zig:6:48: 0x1234ede in test.pointer alignment safety (test_incorrect_pointer_alignment.zig)
    try std.testing.expectEqual(0x11111111, foo(bytes));
                                               ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/7f8d19ec74bf7dbcd82b039049234c1a/test --seed=0x2210595e

allowzero

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

test_allowzero.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "allowzero" {
    var zero: usize = 0; // var to make to runtime-known
    _ = &zero; // suppress 'var is never mutated' error
    const ptr: *allowzero i32 = @ptrFromInt(zero);
    try expectEqual(0, @intFromPtr(ptr));
}
Оболочка
$ zig test test_allowzero.zig
1/1 test_allowzero.test.allowzero...OK
All 1 tests passed.

Указатели с завершителем

Синтаксис [*:x]T описывает указатель, длина которого определяется значением-завершителем. Это обеспечивает защиту от переполнения буфера и чтения за его пределами.

sentinel-terminated_pointer.zig
const std = @import("std");

// This is also available as `std.c.printf`.
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;

pub fn main() anyerror!void {
    _ = printf("Hello, world!\n"); // OK

    const msg = "Hello, world!\n";
    const non_null_terminated_msg: [msg.len]u8 = msg.*;
    _ = printf(&non_null_terminated_msg);
}
Оболочка
$ zig build-exe sentinel-terminated_pointer.zig -lc
/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: error: expected type '[*:0]const u8', found '*const [14]u8'
    _ = printf(&non_null_terminated_msg);
               ^~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: note: destination pointer requires '0' sentinel
/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:4:34: note: parameter type declared here
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
                                 ^~~~~~~~~~~~~
referenced by:
    callMain [inlined]: /home/andy/src/zig/lib/std/start.zig:685:59
    callMainWithArgs [inlined]: /home/andy/src/zig/lib/std/start.zig:637:20
    main: /home/andy/src/zig/lib/std/start.zig:662:28
    1 reference(s) hidden; use '-freference-trace=4' to see all references

См. также:

  • Срезы с завершителем
  • Массивы с завершителем

Срезы

Срез — это указатель и длина. Массив отличается от среза тем, что длина массива является частью типа и известна во время компиляции, тогда как длина среза известна во время выполнения. Доступ к обоим можно получить через поле len.

test_basic_slices.zig
const expectEqual = @import("std").testing.expectEqual;
const expectEqualSlices = @import("std").testing.expectEqualSlices;

test "basic slices" {
    var array = [_]i32{ 1, 2, 3, 4 };
    var known_at_runtime_zero: usize = 0;
    _ = &known_at_runtime_zero;
    const slice = array[known_at_runtime_zero..array.len];

    // alternative initialization using result location
    const alt_slice: []const i32 = &.{ 1, 2, 3, 4 };

    try expectEqualSlices(i32, slice, alt_slice);

    try expectEqual([]i32, @TypeOf(slice));
    try expectEqual(&array[0], &slice[0]);
    try expectEqual(array.len, slice.len);

    // If you slice with comptime-known start and end positions, the result is
    // a pointer to an array, rather than a slice.
    const array_ptr = array[0..array.len];
    try expectEqual(*[array.len]i32, @TypeOf(array_ptr));

    // You can perform a slice-by-length by slicing twice. This allows the compiler
    // to perform some optimisations like recognising a comptime-known length when
    // the start position is only known at runtime.
    var runtime_start: usize = 1;
    _ = &runtime_start;
    const length = 2;
    const array_ptr_len = array[runtime_start..][0..length];
    try expectEqual(*[length]i32, @TypeOf(array_ptr_len));

    // Using the address-of operator on a slice gives a single-item pointer.
    try expectEqual(*i32, @TypeOf(&slice[0]));
    // Using the `ptr` field gives a many-item pointer.
    try expectEqual([*]i32, @TypeOf(slice.ptr));
    try expectEqual(@intFromPtr(slice.ptr), @intFromPtr(&slice[0]));

    // Slices have array bounds checking. If you try to access something out
    // of bounds, you'll get a safety check failure:
    slice[10] += 1;

    // Note that `slice.ptr` does not invoke safety checking, while `&slice[0]`
    // asserts that the slice has len > 0.

    // Empty slices can be created like this:
    const empty1 = &[0]u8{};
    // If the type is known you can use this short hand:
    const empty2: []u8 = &.{};
    try expectEqual(0, empty1.len);
    try expectEqual(0, empty2.len);

    // A zero-length initialization can always be used to create an empty slice, even if the slice is mutable.
    // This is because the pointed-to data is zero bits long, so its immutability is irrelevant.
}
Оболочка
$ zig test test_basic_slices.zig
1/1 test_basic_slices.test.basic slices...thread 927257 panic: index out of bounds: index 10, len 4
/home/andy/src/zig/doc/langref/test_basic_slices.zig:41:10: 0x1236eb4 in test.basic slices (test_basic_slices.zig)
    slice[10] += 1;
         ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/5351e7c63c9dd9ef396b7b8b54eb8d1c/test --seed=0x92b0e709

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

test_slices.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const fmt = std.fmt;

test "using slices for strings" {
    // Zig has no concept of strings. String literals are const pointers
    // to null-terminated arrays of u8, and by convention parameters
    // that are "strings" are expected to be UTF-8 encoded slices of u8.
    // Here we coerce *const [5:0]u8 and *const [6:0]u8 to []const u8
    const hello: []const u8 = "hello";
    const world: []const u8 = "世界";

    var all_together: [100]u8 = undefined;
    // You can use slice syntax with at least one runtime-known index on an
    // array to convert an array into a slice.
    var start: usize = 0;
    _ = &start;
    const all_together_slice = all_together[start..];
    // String concatenation example.
    const hello_world = try fmt.bufPrint(all_together_slice, "{s} {s}", .{ hello, world });

    // Generally, you can use UTF-8 and not worry about whether something is a
    // string. If you don't need to deal with individual characters, no need
    // to decode.
    try expectEqualStrings("hello 世界", hello_world);
}

test "slice pointer" {
    var array: [10]u8 = undefined;
    const ptr = &array;
    try expectEqual(*[10]u8, @TypeOf(ptr));

    // A pointer to an array can be sliced just like an array:
    var start: usize = 0;
    var end: usize = 5;
    _ = .{ &start, &end };
    const slice = ptr[start..end];
    // The slice is mutable because we sliced a mutable pointer.
    try expectEqual([]u8, @TypeOf(slice));
    slice[2] = 3;
    try expectEqual(3, array[2]);

    // Again, slicing with comptime-known indexes will produce another pointer
    // to an array:
    const ptr2 = slice[2..3];
    try expectEqual(1, ptr2.len);
    try expectEqual(3, ptr2[0]);
    try expectEqual(*[1]u8, @TypeOf(ptr2));
}
Оболочка
$ zig test test_slices.zig
1/2 test_slices.test.using slices for strings...OK
2/2 test_slices.test.slice pointer...OK
All 2 tests passed.

См. также:

  • Указатели
  • for
  • Массивы

Срезы с завершителем

Синтаксис [:x]T обозначает срез, длина которого известна во время выполнения и в котором гарантируется наличие значения-завершителя по индексу, равному длине. Тип не гарантирует, что до этого значения нет других элементов-завершителей. Срезы с завершителем позволяют обращаться к элементу по индексу len.

test_null_terminated_slice.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "0-terminated slice" {
    const slice: [:0]const u8 = "hello";

    try expectEqual(5, slice.len);
    try expectEqual(0, slice[5]);
}
Оболочка
$ zig test test_null_terminated_slice.zig
1/1 test_null_terminated_slice.test.0-terminated slice...OK
All 1 tests passed.

Срезы с завершителем также можно создавать с помощью варианта синтаксиса среза data[start..end :x], где data — указатель на множество элементов, массив или срез, а x — значение-завершитель.

test_null_terminated_slicing.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "0-terminated slicing" {
    var array = [_]u8{ 3, 2, 1, 0, 3, 2, 1, 0 };
    var runtime_length: usize = 3;
    _ = &runtime_length;
    const slice = array[0..runtime_length :0];

    try expectEqual([:0]u8, @TypeOf(slice));
    try expectEqual(3, slice.len);
}
Оболочка
$ zig test test_null_terminated_slicing.zig
1/1 test_null_terminated_slicing.test.0-terminated slicing...OK
All 1 tests passed.

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

test_sentinel_mismatch.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "sentinel mismatch" {
    var array = [_]u8{ 3, 2, 1, 0 };

    // Creating a sentinel-terminated slice from the array with a length of 2
    // will result in the value `1` occupying the sentinel element position.
    // This does not match the indicated sentinel value of `0` and will lead
    // to a runtime panic.
    var runtime_length: usize = 2;
    _ = &runtime_length;
    const slice = array[0..runtime_length :0];

    _ = slice;
}
Оболочка
$ zig test test_sentinel_mismatch.zig
1/1 test_sentinel_mismatch.test.sentinel mismatch...thread 929349 panic: sentinel mismatch: expected 0, found 1
/home/andy/src/zig/doc/langref/test_sentinel_mismatch.zig:13:24: 0x1234ecf in test.sentinel mismatch (test_sentinel_mismatch.zig)
    const slice = array[0..runtime_length :0];
                       ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/c6d75cc8280b6dc18f9d56dd44b174a2/test --seed=0x1972e607

См. также:

  • Указатели с завершителем
  • Массивы с завершителем

struct

test_structs.zig
// Declare a struct.
// Zig gives no guarantees about the order of fields and the size of
// the struct but the fields are guaranteed to be ABI-aligned.
const Point = struct {
    x: f32,
    y: f32,
};

// Declare an instance of a struct.
const p: Point = .{
    .x = 0.12,
    .y = 0.34,
};

// Functions in the struct's namespace can be called with dot syntax.
const Vec3 = struct {
    x: f32,
    y: f32,
    z: f32,

    pub fn init(x: f32, y: f32, z: f32) Vec3 {
        return Vec3{
            .x = x,
            .y = y,
            .z = z,
        };
    }

    pub fn dot(self: Vec3, other: Vec3) f32 {
        return self.x * other.x + self.y * other.y + self.z * other.z;
    }
};

test "dot product" {
    const v1 = Vec3.init(1.0, 0.0, 0.0);
    const v2 = Vec3.init(0.0, 1.0, 0.0);
    try expectEqual(0.0, v1.dot(v2));

    // Other than being available to call with dot syntax, struct methods are
    // not special. You can reference them as any other declaration inside
    // the struct:
    try expectEqual(0.0, Vec3.dot(v1, v2));
}

// Structs can have declarations.
// Structs can have 0 fields.
const Empty = struct {
    pub const PI = 3.14;
};
test "struct namespaced variable" {
    try expectEqual(3.14, Empty.PI);
    try expectEqual(0, @sizeOf(Empty));

    // Empty structs can be instantiated the same as usual.
    const does_nothing: Empty = .{};

    _ = does_nothing;
}

// Struct field order is determined by the compiler, however, a base pointer
// can be computed from a field pointer:
fn setYBasedOnX(x: *f32, y: f32) void {
    const point: *Point = @fieldParentPtr("x", x);
    point.y = y;
}
test "field parent pointer" {
    var point = Point{
        .x = 0.1234,
        .y = 0.5678,
    };
    setYBasedOnX(&point.x, 0.9);
    try expectEqual(0.9, point.y);
}

// Structs can be returned from functions.
fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last: ?*Node,
        len: usize,
    };
}

test "linked list" {
    // Functions called at compile-time are memoized.
    try expectEqual(LinkedList(i32), LinkedList(i32));

    const list = LinkedList(i32){
        .first = null,
        .last = null,
        .len = 0,
    };
    try expectEqual(0, list.len);

    // Since types are first class values you can instantiate the type
    // by assigning it to a variable:
    const ListOfInts = LinkedList(i32);
    try expectEqual(LinkedList(i32), ListOfInts);

    var node = ListOfInts.Node{
        .prev = null,
        .next = null,
        .data = 1234,
    };
    const list2 = LinkedList(i32){
        .first = &node,
        .last = &node,
        .len = 1,
    };

    // When using a pointer to a struct, fields can be accessed directly,
    // without explicitly dereferencing the pointer.
    // So you can do
    try expectEqual(1234, list2.first.?.data);
    // instead of try expectEqual(1234, list2.first.?.*.data);
}

const expectEqual = @import("std").testing.expectEqual;
Оболочка
$ zig test test_structs.zig
1/4 test_structs.test.dot product...OK
2/4 test_structs.test.struct namespaced variable...OK
3/4 test_structs.test.field parent pointer...OK
4/4 test_structs.test.linked list...OK
All 4 tests passed.

Значения полей по умолчанию

Для каждого поля структуры можно задать выражение, определяющее значение поля по умолчанию. Такие выражения выполняются во время компиляции и позволяют не указывать поле в литеральном выражении структуры:

struct_default_field_values.zig
const Foo = struct {
    a: i32 = 1234,
    b: i32,
};

test "default struct initialization fields" {
    const x: Foo = .{
        .b = 5,
    };
    if (x.a + x.b != 1239) {
        comptime unreachable;
    }
}
Оболочка
$ zig test struct_default_field_values.zig
1/1 struct_default_field_values.test.default struct initialization fields...OK
All 1 tests passed.

Ошибочные значения полей по умолчанию

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

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

bad_default_value.zig
const Threshold = struct {
    minimum: f32 = 0.25,
    maximum: f32 = 0.75,

    const Category = enum { low, medium, high };

    fn categorize(t: Threshold, value: f32) Category {
        assert(t.maximum >= t.minimum);
        if (value < t.minimum) return .low;
        if (value > t.maximum) return .high;
        return .medium;
    }
};

pub fn main() !void {
    var threshold: Threshold = .{
        .maximum = 0.20,
    };
    const category = threshold.categorize(0.90);
    std.log.info("category: {t}", .{category});
}

const std = @import("std");
const assert = std.debug.assert;
Оболочка
$ zig build-exe bad_default_value.zig
$ ./bad_default_value
thread 932709 panic: reached unreachable code
/home/andy/src/zig/lib/std/debug.zig:421:14: 0x1025eb9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/home/andy/src/zig/doc/langref/bad_default_value.zig:8:15: 0x11d611a in categorize (bad_default_value.zig)
        assert(t.maximum >= t.minimum);
              ^
/home/andy/src/zig/doc/langref/bad_default_value.zig:19:42: 0x11d4bce in main (bad_default_value.zig)
    const category = threshold.categorize(0.90);
                                         ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d51ac in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

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

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

struct_default_value.zig
const Threshold = struct {
    minimum: f32,
    maximum: f32,

    const default: Threshold = .{
        .minimum = 0.25,
        .maximum = 0.75,
    };
};

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

extern struct

Структура extern struct имеет расположение в памяти, соответствующее ABI C для целевой платформы.

Если строго определённое расположение в памяти не требуется, лучше использовать struct, поскольку он накладывает меньше ограничений на компилятор.

См. packed struct — структуру, ABI которой соответствует базовому целому числу; она может быть полезна для моделирования флагов.

См. также:

  • extern union
  • extern enum

packed struct

Структуры packed, как и enum, основаны на концепции различной интерпретации целых чисел. У всех упакованных структур есть базовое целое число, которое определяется неявно на основе общего числа битов полей или задаётся явно. Упакованные структуры имеют чётко определённое расположение в памяти — ABI у них в точности совпадает с ABI базового целого числа.

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

  • Поле типа целое число использует ровно столько битов, какова его разрядность. Например, для u5 будут использоваться 5 битов базового целого числа.
  • Поле типа bool использует ровно 1 бит.
  • Поле типа enum использует ровно столько битов, какова разрядность его целочисленного типа тега.
  • Поле типа packed union использует столько битов, какова разрядность поля объединения с наибольшей разрядностью.
  • Поле packed struct использует биты своего базового целого числа.

Это означает, что packed struct можно использовать в @bitCast или @ptrCast для переинтерпретации памяти. Это работает даже во время компиляции:

test_packed_structs.zig
const std = @import("std");
const native_endian = @import("builtin").target.cpu.arch.endian();
const expectEqual = std.testing.expectEqual;

const Full = packed struct {
    number: u16,
};
const Divided = packed struct {
    half1: u8,
    quarter3: u4,
    quarter4: u4,
};

test "@bitCast between packed structs" {
    try doTheTest();
    try comptime doTheTest();
}

fn doTheTest() !void {
    try expectEqual(2, @sizeOf(Full));
    try expectEqual(2, @sizeOf(Divided));
    const full = Full{ .number = 0x1234 };
    const divided: Divided = @bitCast(full);
    try expectEqual(0x34, divided.half1);
    try expectEqual(0x2, divided.quarter3);
    try expectEqual(0x1, divided.quarter4);

    const ordered: [2]u8 = @bitCast(full);
    switch (native_endian) {
        .big => {
            try expectEqual(0x12, ordered[0]);
            try expectEqual(0x34, ordered[1]);
        },
        .little => {
            try expectEqual(0x34, ordered[0]);
            try expectEqual(0x12, ordered[1]);
        },
    }
}
Оболочка
$ zig test test_packed_structs.zig
1/1 test_packed_structs.test.@bitCast between packed structs...OK
All 1 tests passed.

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

test_missized_packed_struct.zig
test "missized packed struct" {
    const S = packed struct(u32) { a: u16, b: u8 };
    _ = S{ .a = 4, .b = 2 };
}
Оболочка
$ zig test test_missized_packed_struct.zig
/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:22: error: backing integer bit width does not match total bit width of fields
    const S = packed struct(u32) { a: u16, b: u8 };
              ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:29: note: backing integer 'u32' has bit width '32'
    const S = packed struct(u32) { a: u16, b: u8 };
                            ^~~
/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:22: note: struct fields have total bit width '24'
referenced by:
    test.missized packed struct: /home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:3:17

В Zig можно получить адрес поля, не выровненного по границе байта:

test_pointer_to_non-byte_aligned_field.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const BitField = packed struct {
    a: u3,
    b: u3,
    c: u2,
};

var foo = BitField{
    .a = 1,
    .b = 2,
    .c = 3,
};

test "pointer to non-byte-aligned field" {
    const ptr = &foo.b;
    try expectEqual(2, ptr.*);
}
Оболочка
$ zig test test_pointer_to_non-byte_aligned_field.zig
1/1 test_pointer_to_non-byte_aligned_field.test.pointer to non-byte-aligned field...OK
All 1 tests passed.

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

test_misaligned_pointer.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const BitField = packed struct {
    a: u3,
    b: u3,
    c: u2,
};

var bit_field = BitField{
    .a = 1,
    .b = 2,
    .c = 3,
};

test "pointer to non-byte-aligned field" {
    try expectEqual(2, bar(&bit_field.b));
}

fn bar(x: *const u3) u3 {
    return x.*;
}
Оболочка
$ zig test test_misaligned_pointer.zig
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: error: expected type '*const u3', found '*align(1:3:1) u3'
    try expectEqual(2, bar(&bit_field.b));
                           ^~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer host size '1' cannot cast into pointer host size '0'
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:20:11: note: parameter type declared here
fn bar(x: *const u3) u3 {
          ^~~~~~~~~

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

Указатели на поля, не выровненные по ABI, имеют тот же адрес, что и другие поля внутри содержащего их целого числа:

test_packed_struct_field_address.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const BitField = packed struct {
    a: u3,
    b: u3,
    c: u2,
};

var bit_field = BitField{
    .a = 1,
    .b = 2,
    .c = 3,
};

test "pointers of sub-byte-aligned fields share addresses" {
    try expectEqual(@intFromPtr(&bit_field.a), @intFromPtr(&bit_field.b));
    try expectEqual(@intFromPtr(&bit_field.a), @intFromPtr(&bit_field.c));
}
Оболочка
$ zig test test_packed_struct_field_address.zig
1/1 test_packed_struct_field_address.test.pointers of sub-byte-aligned fields share addresses...OK
All 1 tests passed.

Это можно наблюдать с помощью @bitOffsetOf и offsetOf:

test_bitOffsetOf_offsetOf.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const BitField = packed struct {
    a: u3,
    b: u3,
    c: u2,
};

test "offsets of non-byte-aligned fields" {
    comptime {
        try expectEqual(0, @bitOffsetOf(BitField, "a"));
        try expectEqual(3, @bitOffsetOf(BitField, "b"));
        try expectEqual(6, @bitOffsetOf(BitField, "c"));

        try expectEqual(0, @offsetOf(BitField, "a"));
        try expectEqual(0, @offsetOf(BitField, "b"));
        try expectEqual(0, @offsetOf(BitField, "c"));
    }
}
Оболочка
$ zig test test_bitOffsetOf_offsetOf.zig
1/1 test_bitOffsetOf_offsetOf.test.offsets of non-byte-aligned fields...OK
All 1 tests passed.

Упакованные структуры имеют такое же выравнивание, как и их базовое целое число, однако указатели на упакованные структуры с увеличенным выравниванием могут переопределить его:

test_overaligned_packed_struct.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const S = packed struct {
    a: u32,
    b: u32,
};
test "overaligned pointer to packed struct" {
    var foo: S align(4) = .{ .a = 1, .b = 2 };
    const ptr: *align(4) S = &foo;
    const ptr_to_b = &ptr.b;
    try expectEqual(2, ptr_to_b.*);
}
Оболочка
$ zig test test_overaligned_packed_struct.zig
1/1 test_overaligned_packed_struct.test.overaligned pointer to packed struct...OK
All 1 tests passed.

Можно также задавать выравнивание полей структуры:

test_aligned_struct_fields.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "aligned struct fields" {
    const S = struct {
        a: u32 align(2),
        b: u32 align(64),
    };
    var foo = S{ .a = 1, .b = 2 };

    try expectEqual(64, @alignOf(S));
    try expectEqual(*align(2) u32, @TypeOf(&foo.a));
    try expectEqual(*align(64) u32, @TypeOf(&foo.b));
}
Оболочка
$ zig test test_aligned_struct_fields.zig
1/1 test_aligned_struct_fields.test.aligned struct fields...OK
All 1 tests passed.

При сравнении упакованных структур сравниваются базовые целые числа; это работает только для операторов == и !=.

test_packed_struct_equality.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "packed struct equality" {
    const S = packed struct {
        a: u4,
        b: u4,
    };
    const x: S = .{ .a = 1, .b = 2 };
    const y: S = .{ .b = 2, .a = 1 };
    try expectEqual(x, y);
}
Оболочка
$ zig test test_packed_struct_equality.zig
1/1 test_packed_struct_equality.test.packed struct equality...OK
All 1 tests passed.

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

packed_struct_mmio.zig
pub const GpioRegister = packed struct(u8) {
    GPIO0: bool,
    GPIO1: bool,
    GPIO2: bool,
    GPIO3: bool,
    reserved: u4 = 0,
};

const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);

pub fn writeToGpio(new_states: GpioRegister) void {
    // Example of what not to do:
    // BAD! gpio.GPIO0 = true; BAD!

    // Instead, do this:
    gpio.* = new_states;
}

Именование структур

Поскольку все структуры являются анонимными, Zig выводит имя типа по нескольким правилам.

  • Если структура находится в выражении инициализации переменной, она получает имя этой переменной.
  • Если структура находится в выражении return, она получает имя функции, из которой возвращается, а значения параметров сериализуются.
  • В противном случае структура получает имя, например (filename.funcname__struct_ID).
  • Если структура объявлена внутри другой структуры, её имя состоит из имени родительской структуры и имени, выведенного по предыдущим правилам, разделённых точкой.
struct_name.zig
const std = @import("std");

pub fn main() void {
    const Foo = struct {};
    std.debug.print("variable: {s}\n", .{@typeName(Foo)});
    std.debug.print("anonymous: {s}\n", .{@typeName(struct {})});
    std.debug.print("function: {s}\n", .{@typeName(List(i32))});
}

fn List(comptime T: type) type {
    return struct {
        x: T,
    };
}
Оболочка
$ zig build-exe struct_name.zig
$ ./struct_name
variable: struct_name.main.Foo
anonymous: struct_name.main__struct_32474
function: struct_name.List(i32)

Анонимные литералы структур

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

test_struct_result.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Point = struct { x: i32, y: i32 };

test "anonymous struct literal" {
    const pt: Point = .{
        .x = 13,
        .y = 67,
    };
    try expectEqual(13, pt.x);
    try expectEqual(67, pt.y);
}
Оболочка
$ zig test test_struct_result.zig
1/1 test_struct_result.test.anonymous struct literal...OK
All 1 tests passed.

Тип структуры может быть выведен. Здесь место результата не включает тип, поэтому Zig выводит его:

test_anonymous_struct.zig
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

test "fully anonymous struct" {
    try check(.{
        .int = @as(u32, 1234),
        .float = @as(f64, 12.34),
        .b = true,
        .s = "hi",
    });
}

fn check(args: anytype) !void {
    try expectEqual(1234, args.int);
    try expectEqual(12.34, args.float);
    try expect(args.b);
    try expectEqual('h', args.s[0]);
    try expectEqual('i', args.s[1]);
}
Оболочка
$ zig test test_anonymous_struct.zig
1/1 test_anonymous_struct.test.fully anonymous struct...OK
All 1 tests passed.

Кортежи

Анонимные структуры можно создавать без указания имён полей; такие структуры называются «кортежами». Пустой кортеж выглядит как .{}; он встречается в одном из примеров Hello World.

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

Как и массивы, кортежи имеют поле .len, могут индексироваться (если индекс известен во время компиляции) и поддерживают операторы ++ и **. Кроме того, по ним можно выполнять итерацию с помощью inline for.

test_tuples.zig
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

test "tuple" {
    const values = .{
        @as(u32, 1234),
        @as(f64, 12.34),
        true,
        "hi",
    } ++ .{false} ** 2;
    try expectEqual(1234, values[0]);
    try expectEqual(false, values[4]);
    inline for (values, 0..) |v, i| {
        if (i != 2) continue;
        try expect(v);
    }
    try expectEqual(6, values.len);
    try expectEqual('h', values.@"3"[0]);
}
Оболочка
$ zig test test_tuples.zig
1/1 test_tuples.test.tuple...OK
All 1 tests passed.

Деструктуризация кортежей

Кортежи можно деструктурировать.

Деструктуризация кортежей полезна для возврата нескольких значений из блока:

destructuring_block.zig
const print = @import("std").debug.print;

pub fn main() void {
    const digits = [_]i8 { 3, 8, 9, 0, 7, 4, 1 };

    const min, const max = blk: {
        var min: i8 = 127;
        var max: i8 = -128;

        for (digits) |digit| {
            if (digit < min) min = digit;
            if (digit > max) max = digit;
        }

        break :blk .{ min, max };
    };

    print("min = {}\n", .{ min });
    print("max = {}\n", .{ max });
}
Оболочка
$ zig build-exe destructuring_block.zig
$ ./destructuring_block
min = 0
max = 9

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

destructuring_return_value.zig
const print = @import("std").debug.print;

fn divmod(numerator: u32, denominator: u32) struct { u32, u32 } {
    return .{ numerator / denominator, numerator % denominator };
}

pub fn main() void {
    const div, const mod = divmod(10, 3);

    print("10 / 3 = {}\n", .{div});
    print("10 % 3 = {}\n", .{mod});
}
Оболочка
$ zig build-exe destructuring_return_value.zig
$ ./destructuring_return_value
10 / 3 = 3
10 % 3 = 1

См. также:

  • Деструктуризация
  • Деструктуризация массивов
  • Деструктуризация векторов

См. также:

  • comptime
  • @fieldParentPtr

enum

test_enums.zig
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
const expectEqualStrings = @import("std").testing.expectEqualStrings;
const mem = @import("std").mem;

// Declare an enum.
const Type = enum {
    ok,
    not_ok,
};

// Declare a specific enum field.
const c = Type.ok;

// If you want access to the ordinal value of an enum, you
// can specify the tag type.
const Value = enum(u2) {
    zero,
    one,
    two,
};
// Now you can cast between u2 and Value.
// The ordinal value starts from 0, counting up by 1 from the previous member.
test "enum ordinal value" {
    try expectEqual(0, @intFromEnum(Value.zero));
    try expectEqual(1, @intFromEnum(Value.one));
    try expectEqual(2, @intFromEnum(Value.two));
}

// You can override the ordinal value for an enum.
const Value2 = enum(u32) {
    hundred = 100,
    thousand = 1000,
    million = 1000000,
};
test "set enum ordinal value" {
    try expectEqual(100, @intFromEnum(Value2.hundred));
    try expectEqual(1000, @intFromEnum(Value2.thousand));
    try expectEqual(1000000, @intFromEnum(Value2.million));
}

// You can also override only some values.
const Value3 = enum(u4) {
    a,
    b = 8,
    c,
    d = 4,
    e,
};
test "enum implicit ordinal values and overridden values" {
    try expectEqual(0, @intFromEnum(Value3.a));
    try expectEqual(8, @intFromEnum(Value3.b));
    try expectEqual(9, @intFromEnum(Value3.c));
    try expectEqual(4, @intFromEnum(Value3.d));
    try expectEqual(5, @intFromEnum(Value3.e));
}

// Enums can have methods, the same as structs and unions.
// Enum methods are not special, they are only namespaced
// functions that you can call with dot syntax.
const Suit = enum {
    clubs,
    spades,
    diamonds,
    hearts,

    pub fn isClubs(self: Suit) bool {
        return self == Suit.clubs;
    }
};
test "enum method" {
    const p = Suit.spades;
    try expect(!p.isClubs());
}

// An enum can be switched upon.
const Foo = enum {
    string,
    number,
    none,
};
test "enum switch" {
    const p = Foo.number;
    const what_is_it = switch (p) {
        Foo.string => "this is a string",
        Foo.number => "this is a number",
        Foo.none => "this is a none",
    };
    try expectEqualStrings(what_is_it, "this is a number");
}

// @typeInfo can be used to access the integer tag type of an enum.
const Small = enum {
    one,
    two,
    three,
    four,
};
test "std.meta.Tag" {
    try expectEqual(u2, @typeInfo(Small).@"enum".tag_type);
}

// @typeInfo tells us the field count and the fields names:
test "@typeInfo" {
    try expectEqual(4, @typeInfo(Small).@"enum".fields.len);
    try expectEqualStrings(@typeInfo(Small).@"enum".fields[1].name, "two");
}

// @tagName gives a [:0]const u8 representation of an enum value:
test "@tagName" {
    try expectEqualStrings(@tagName(Small.three), "three");
}
Оболочка
$ zig test test_enums.zig
1/8 test_enums.test.enum ordinal value...OK
2/8 test_enums.test.set enum ordinal value...OK
3/8 test_enums.test.enum implicit ordinal values and overridden values...OK
4/8 test_enums.test.enum method...OK
5/8 test_enums.test.enum switch...OK
6/8 test_enums.test.std.meta.Tag...OK
7/8 test_enums.test.@typeInfo...OK
8/8 test_enums.test.@tagName...OK
All 8 tests passed.

См. также:

  • @typeInfo
  • @tagName
  • @sizeOf

extern enum

По умолчанию перечисления не гарантированно совместимы с ABI C:

enum_export_error.zig
const Foo = enum { a, b, c };
export fn entry(foo: Foo) void {
    _ = foo;
}
Оболочка
$ zig build-obj enum_export_error.zig -target x86_64-linux
/home/andy/src/zig/doc/langref/enum_export_error.zig:2:17: error: parameter of type 'enum_export_error.Foo' not allowed in function with calling convention 'x86_64_sysv'
export fn entry(foo: Foo) void {
                ^~~~~~~~
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: integer tag type of enum is inferred
const Foo = enum { a, b, c };
            ^~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: consider explicitly specifying the integer tag type
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: enum declared here
referenced by:
    root: /home/andy/src/zig/lib/std/start.zig:13:22
    comptime: /home/andy/src/zig/lib/std/start.zig:20:9
    2 reference(s) hidden; use '-freference-trace=4' to see all references

Чтобы перечисление было совместимо с ABI C, укажите для него явный тип тега:

enum_export.zig
const Foo = enum(c_int) { a, b, c };
export fn entry(foo: Foo) void {
    _ = foo;
}
Оболочка
$ zig build-obj enum_export.zig

Литералы перечислений

Литералы перечислений позволяют указывать имя поля перечисления, не указывая тип перечисления:

test_enum_literals.zig
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

const Color = enum {
    auto,
    off,
    on,
};

test "enum literals" {
    const color1: Color = .auto;
    const color2 = Color.auto;
    try expectEqual(color1, color2);
}

test "switch using enum literals" {
    const color = Color.on;
    const result = switch (color) {
        .auto => false,
        .on => true,
        .off => false,
    };
    try expect(result);
}
Оболочка
$ zig test test_enum_literals.zig
1/2 test_enum_literals.test.enum literals...OK
2/2 test_enum_literals.test.switch using enum literals...OK
All 2 tests passed.

Неполное перечисление

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

При использовании @enumFromInt с неполным перечислением применяются правила безопасности @intCast для приведения к целочисленному типу тега, но в остальном результатом всегда будет корректное значение перечисления.

В операторе switch для неполного перечисления можно использовать ветвь _ вместо ветви else. При наличии ветви _ компилятор выдаст ошибку, если в switch не обработаны все известные имена тегов.

test_switch_non-exhaustive.zig
const std = @import("std");
const expect = std.testing.expect;

const Number = enum(u8) {
    one,
    two,
    three,
    _,
};

test "switch on non-exhaustive enum" {
    const number = Number.one;
    const result = switch (number) {
        .one => true,
        .two, .three => false,
        _ => false,
    };
    try expect(result);
    const is_one = switch (number) {
        .one => true,
        else => false,
    };
    try expect(is_one);
}
Оболочка
$ zig test test_switch_non-exhaustive.zig
1/1 test_switch_non-exhaustive.test.switch on non-exhaustive enum...OK
All 1 tests passed.

union

Простое union задаёт набор возможных типов значения в виде списка полей. Одновременно может быть активно только одно поле. Представление простых объединений в памяти не гарантируется. Простые объединения нельзя использовать для переинтерпретации памяти. Для этого используйте @ptrCast, extern union или packed union, расположение которых в памяти гарантировано. Доступ к неактивному полю является проверяемым на безопасность недопустимым поведением:

test_wrong_union_access.zig
const Payload = union {
    int: i64,
    float: f64,
    boolean: bool,
};
test "simple union" {
    var payload = Payload{ .int = 1234 };
    payload.float = 12.34;
}
Оболочка
$ zig test test_wrong_union_access.zig
1/1 test_wrong_union_access.test.simple union...thread 933934 panic: access of union field 'float' while field 'int' is active
/home/andy/src/zig/doc/langref/test_wrong_union_access.zig:8:12: 0x1234e62 in test.simple union (test_wrong_union_access.zig)
    payload.float = 12.34;
           ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/a910feedacc78c90cf0c411c52cd47d5/test --seed=0x39660e11

Чтобы активировать другое поле, присвойте значение всему объединению:

test_simple_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Payload = union {
    int: i64,
    float: f64,
    boolean: bool,
};
test "simple union" {
    var payload = Payload{ .int = 1234 };
    try expectEqual(1234, payload.int);
    payload = Payload{ .float = 12.34 };
    try expectEqual(12.34, payload.float);
}
Оболочка
$ zig test test_simple_union.zig
1/1 test_simple_union.test.simple union...OK
All 1 tests passed.

Чтобы использовать switch с объединением, оно должно быть объединением с тегом.

Чтобы инициализировать объединение, если имя тега известно во время компиляции, см. @unionInit.

Объединение с тегом

Объединения можно объявить с типом тега-перечисления. Это превращает объединение в объединение с тегом, которое можно использовать в выражениях switch. При переключении по объединению с тегом значение тега можно получить с помощью дополнительного захвата. Объединения с тегом приводятся к своему типу тега: Приведение типов: объединения и перечисления.

test_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const ComplexTypeTag = enum {
    ok,
    not_ok,
};
const ComplexType = union(ComplexTypeTag) {
    ok: u8,
    not_ok: void,
};

test "switch on tagged union" {
    const c = ComplexType{ .ok = 42 };
    try expectEqual(ComplexTypeTag.ok, @as(ComplexTypeTag, c));

    switch (c) {
        .ok => |value| try expectEqual(42, value),
        .not_ok => unreachable,
    }

    switch (c) {
        .ok => |_, tag| {
            // Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok':
            comptime std.debug.assert(tag == .ok);
        },
        .not_ok => unreachable,
    }
}

test "get tag type" {
    try expectEqual(ComplexTypeTag, std.meta.Tag(ComplexType));
}
Оболочка
$ zig test test_tagged_union.zig
1/2 test_tagged_union.test.switch on tagged union...OK
2/2 test_tagged_union.test.get tag type...OK
All 2 tests passed.

Чтобы изменить полезную нагрузку объединения с тегом в выражении switch, поставьте * перед именем переменной, чтобы сделать её указателем:

test_switch_modify_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const ComplexTypeTag = enum {
    ok,
    not_ok,
};
const ComplexType = union(ComplexTypeTag) {
    ok: u8,
    not_ok: void,
};

test "modify tagged union in switch" {
    var c = ComplexType{ .ok = 42 };

    switch (c) {
        ComplexTypeTag.ok => |*value| value.* += 1,
        ComplexTypeTag.not_ok => unreachable,
    }

    try expectEqual(43, c.ok);
}
Оболочка
$ zig test test_switch_modify_tagged_union.zig
1/1 test_switch_modify_tagged_union.test.modify tagged union in switch...OK
All 1 tests passed.

Объединения могут выводить тип тега-перечисления. Кроме того, у объединений, как и у структур и перечислений, могут быть методы.

test_union_method.zig
const std = @import("std");
const expect = std.testing.expect;

const Variant = union(enum) {
    int: i32,
    boolean: bool,

    // void can be omitted when inferring enum tag type.
    none,

    fn truthy(self: Variant) bool {
        return switch (self) {
            Variant.int => |x_int| x_int != 0,
            Variant.boolean => |x_bool| x_bool,
            Variant.none => false,
        };
    }
};

test "union method" {
    var v1: Variant = .{ .int = 1 };
    var v2: Variant = .{ .boolean = false };
    var v3: Variant = .none;

    try expect(v1.truthy());
    try expect(!v2.truthy());
    try expect(!v3.truthy());
}
Оболочка
$ zig test test_union_method.zig
1/1 test_union_method.test.union method...OK
All 1 tests passed.

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

test_tagged_union_with_tag_values.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Tagged = union(enum(u32)) {
    int: i64 = 123,
    boolean: bool = 67,
};

test "tag values" {
    const int: Tagged = .{ .int = -40 };
    try expectEqual(123, @intFromEnum(int));

    const boolean: Tagged = .{ .boolean = false };
    try expectEqual(67, @intFromEnum(boolean));
}
Оболочка
$ zig test test_tagged_union_with_tag_values.zig
1/1 test_tagged_union_with_tag_values.test.tag values...OK
All 1 tests passed.

С помощью @tagName можно получить значение [:0]const u8 времени компиляции, представляющее имя поля:

test_tagName.zig
const std = @import("std");
const expectEqualSlices = std.testing.expectEqualSlices;

const Small2 = union(enum) {
    a: i32,
    b: bool,
    c: u8,
};
test "@tagName" {
    try expectEqualSlices(u8, "a", @tagName(Small2.a));
}
Оболочка
$ zig test test_tagName.zig
1/1 test_tagName.test.@tagName...OK
All 1 tests passed.

extern union

Объединение extern union имеет расположение в памяти, совместимость которого с ABI C целевой платформы гарантирована.

См. также:

  • extern struct

packed union

Объединение packed union имеет чётко определённое расположение в памяти и может использоваться в packed struct.

Все поля упакованного объединения должны иметь одинаковое значение @bitSizeOf.

При сравнении упакованных объединений сравниваются базовые целые числа; это работает только для операторов == и !=.

test_packed_union_equality.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "packed union equality" {
    const U = packed union {
        a: u4,
        b: i4,
    };
    const x: U = .{ .a = 3 };
    const y: U = .{ .b = 3 };
    try expectEqual(x, y);
}
Оболочка
$ zig test test_packed_union_equality.zig
1/1 test_packed_union_equality.test.packed union equality...OK
All 1 tests passed.

Анонимные литералы объединений

Для инициализации объединений без указания типа можно использовать синтаксис анонимных литералов структур:

test_anonymous_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Number = union {
    int: i32,
    float: f64,
};

test "anonymous union literal syntax" {
    const i: Number = .{ .int = 42 };
    const f = makeNumber();
    try expectEqual(42, i.int);
    try expectEqual(12.34, f.float);
}

fn makeNumber() Number {
    return .{ .float = 12.34 };
}
Оболочка
$ zig test test_anonymous_union.zig
1/1 test_anonymous_union.test.anonymous union literal syntax...OK
All 1 tests passed.

opaque

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

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

test_opaque.zig
const Derp = opaque {};
const Wat = opaque {};

extern fn bar(d: *Derp) void;
fn foo(w: *Wat) callconv(.c) void {
    bar(w);
}

test "call foo" {
    foo(undefined);
}
Оболочка
$ zig test test_opaque.zig
/home/andy/src/zig/doc/langref/test_opaque.zig:6:9: error: expected type '*test_opaque.Derp', found '*test_opaque.Wat'
    bar(w);
        ^
/home/andy/src/zig/doc/langref/test_opaque.zig:6:9: note: pointer type child 'test_opaque.Wat' cannot cast into pointer type child 'test_opaque.Derp'
/home/andy/src/zig/doc/langref/test_opaque.zig:2:13: note: opaque declared here
const Wat = opaque {};
            ^~~~~~~~~
/home/andy/src/zig/doc/langref/test_opaque.zig:1:14: note: opaque declared here
const Derp = opaque {};
             ^~~~~~~~~
/home/andy/src/zig/doc/langref/test_opaque.zig:4:18: note: parameter type declared here
extern fn bar(d: *Derp) void;
                 ^~~~~
referenced by:
    test.call foo: /home/andy/src/zig/doc/langref/test_opaque.zig:10:8

Блоки

Блоки используются для ограничения области видимости объявлений переменных:

test_blocks.zig
test "access variable after block scope" {
    {
        var x: i32 = 1;
        _ = &x;
    }
    x += 1;
}
Оболочка
$ zig test test_blocks.zig
/home/andy/src/zig/doc/langref/test_blocks.zig:6:5: error: use of undeclared identifier 'x'
    x += 1;
    ^

Блоки являются выражениями. Если блоку присвоена метка, для возврата из него значения можно использовать break:

test_labeled_break.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "labeled break from labeled block expression" {
    var y: i32 = 123;

    const x = blk: {
        y += 1;
        break :blk y;
    };
    try expectEqual(124, x);
    try expectEqual(124, y);
}
Оболочка
$ zig test test_labeled_break.zig
1/1 test_labeled_break.test.labeled break from labeled block expression...OK
All 1 tests passed.

Здесь blk может быть любым именем.

См. также:

  • while с меткой
  • for с меткой

Затенение

Идентификаторам никогда не разрешается «скрывать» другие идентификаторы, используя то же имя:

test_shadowing.zig
const pi = 3.14;

test "inside test block" {
    // Let's even go inside another block
    {
        var pi: i32 = 1234;
    }
}
Оболочка
$ zig test test_shadowing.zig
/home/andy/src/zig/doc/langref/test_shadowing.zig:6:13: error: local variable shadows declaration of 'pi'
        var pi: i32 = 1234;
            ^~
/home/andy/src/zig/doc/langref/test_shadowing.zig:1:1: note: declared here
const pi = 3.14;
^~~~~~~~~~~~~~~

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

test_scopes.zig
test "separate scopes" {
    {
        const pi = 3.14;
        _ = pi;
    }
    {
        var pi: bool = true;
        _ = &pi;
    }
}
Оболочка
$ zig test test_scopes.zig
1/1 test_scopes.test.separate scopes...OK
All 1 tests passed.

Пустые блоки

Пустой блок эквивалентен void{}:

test_empty_block.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test {
    const a = {};
    const b = void{};
    try expectEqual(void, @TypeOf(a));
    try expectEqual(void, @TypeOf(b));
    try expectEqual(a, b);
}
Оболочка
$ zig test test_empty_block.zig
1/1 test_empty_block.test_0...OK
All 1 tests passed.

switch

test_switch.zig
const std = @import("std");
const builtin = @import("builtin");
const expectEqual = std.testing.expectEqual;

test "switch simple" {
    const a: u64 = 10;
    const zz: u64 = 103;

    // All branches of a switch expression must be able to be coerced to a
    // common type.
    //
    // Branches cannot fallthrough. If fallthrough behavior is desired, combine
    // the cases and use an if.
    const b = switch (a) {
        // Multiple cases can be combined via a ','
        1, 2, 3 => 0,

        // Ranges can be specified using the ... syntax. These are inclusive
        // of both ends.
        5...100 => 1,

        // Branches can be arbitrarily complex.
        101 => blk: {
            const c: u64 = 5;
            break :blk c * 2 + 1;
        },

        // Switching on arbitrary expressions is allowed as long as the
        // expression is known at compile-time.
        zz => zz,
        blk: {
            const d: u32 = 5;
            const e: u32 = 100;
            break :blk d + e;
        } => 107,

        // The else branch catches everything not already captured.
        // Else branches are mandatory unless the entire range of values
        // is handled.
        else => 9,
    };

    try expectEqual(1, b);
}

// Switch expressions can be used outside a function:
const os_msg = switch (builtin.target.os.tag) {
    .linux => "we found a linux user",
    else => "not a linux user",
};

// Inside a function, switch statements implicitly are compile-time
// evaluated if the target expression is compile-time known.
test "switch inside function" {
    switch (builtin.target.os.tag) {
        .fuchsia => {
            // On an OS other than fuchsia, block is not even analyzed,
            // so this compile error is not triggered.
            // On fuchsia this compile error would be triggered.
            @compileError("fuchsia not supported");
        },
        else => {},
    }
}
Оболочка
$ zig test test_switch.zig
1/2 test_switch.test.switch simple...OK
2/2 test_switch.test.switch inside function...OK
All 2 tests passed.

switch можно использовать для захвата значений полей объединения с тегом. Чтобы изменить значения полей, поставьте * перед именем переменной захвата, превратив её в указатель.

test_switch_tagged_union.zig
const expectEqual = @import("std").testing.expectEqual;

test "switch on tagged union" {
    const Point = struct {
        x: u8,
        y: u8,
    };
    const Item = union(enum) {
        a: u32,
        c: Point,
        d,
        e: u32,
    };

    var a = Item{ .c = Point{ .x = 1, .y = 2 } };

    // Switching on more complex enums is allowed.
    const b = switch (a) {
        // A capture group is allowed on a match, and will return the enum
        // value matched. If the payload types of both cases are the same
        // they can be put into the same switch prong.
        Item.a, Item.e => |item| item,

        // A reference to the matched value can be obtained using `*` syntax.
        Item.c => |*item| blk: {
            item.*.x += 1;
            break :blk 6;
        },

        // No else is required if the types cases was exhaustively handled
        Item.d => 8,
    };

    try expectEqual(6, b);
    try expectEqual(2, a.c.x);
}
Оболочка
$ zig test test_switch_tagged_union.zig
1/1 test_switch_tagged_union.test.switch on tagged union...OK
All 1 tests passed.

См. также:

  • comptime
  • enum
  • @compileError
  • Переменные времени компиляции

Полный перебор в switch

Если в выражении switch отсутствует ветвь else, в нём необходимо перечислить все возможные значения. В противном случае возникает ошибка компиляции:

test_unhandled_enumeration_value.zig
const Color = enum {
    auto,
    off,
    on,
};

test "exhaustive switching" {
    const color = Color.off;
    switch (color) {
        Color.auto => {},
        Color.on => {},
    }
}
Оболочка
$ zig test test_unhandled_enumeration_value.zig
/home/andy/src/zig/doc/langref/test_unhandled_enumeration_value.zig:9:5: error: switch must handle all possibilities
    switch (color) {
    ^~~~~~
/home/andy/src/zig/doc/langref/test_unhandled_enumeration_value.zig:3:5: note: unhandled enumeration value: 'off'
    off,
    ^~~
/home/andy/src/zig/doc/langref/test_unhandled_enumeration_value.zig:1:15: note: enum 'test_unhandled_enumeration_value.Color' declared here
const Color = enum {
              ^~~~

Использование литералов перечислений в switch

Литералы перечислений можно использовать в switch, чтобы не указывать повторно типы перечислений или объединений:

test_exhaustive_switch.zig
const std = @import("std");
const expect = std.testing.expect;

const Color = enum {
    auto,
    off,
    on,
};

test "enum literals with switch" {
    const color = Color.off;
    const result = switch (color) {
        .auto => false,
        .on => false,
        .off => true,
    };
    try expect(result);
}
Оболочка
$ zig test test_exhaustive_switch.zig
1/1 test_exhaustive_switch.test.enum literals with switch...OK
All 1 tests passed.

Переключение по ошибкам

При переключении по ошибкам допускаются некоторые особые случаи, упрощающие шаблоны обобщённого программирования:

test_switch_on_errors.zig
const FileOpenError0 = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

fn openFile0() FileOpenError0 {
    return error.OutOfMemory;
}

test "unreachable else prong" {
    switch (openFile0()) {
        error.AccessDenied, error.FileNotFound => |e| return e,
        error.OutOfMemory => {},
        // 'openFile0' cannot return any more errors, so an 'else' prong would be
        // statically known to be unreachable. Nonetheless, in this case, adding
        // one does not raise an "unreachable else prong" compile error:
        else => unreachable,
    }

    // Allowed unreachable else prongs are:
    //    `else => unreachable,`
    //    `else => return,`
    //    `else => |e| return e,` (where `e` is any identifier)
}

const FileOpenError1 = error{
    AccessDenied,
    SystemResources,
    FileNotFound,
};

fn openFile1() FileOpenError1 {
    return error.SystemResources;
}

fn openFileGeneric(comptime kind: u1) switch (kind) {
    0 => FileOpenError0,
    1 => FileOpenError1,
} {
    return switch (kind) {
        0 => openFile0(),
        1 => openFile1(),
    };
}

test "comptime unreachable errors not in error set" {
    switch (openFileGeneric(1)) {
        error.AccessDenied, error.FileNotFound => |e| return e,
        error.OutOfMemory => comptime unreachable, // not in `FileOpenError1`!
        error.SystemResources => {},
    }
}
Оболочка
$ zig test test_switch_on_errors.zig
1/2 test_switch_on_errors.test.unreachable else prong...OK
2/2 test_switch_on_errors.test.comptime unreachable errors not in error set...OK
All 2 tests passed.

switch с меткой

Если оператору switch присвоена метка, на него можно ссылаться из break или continue. break вернёт значение из switch.

Оператор continue, ссылающийся на switch, должен иметь операнд. При выполнении он перейдёт к соответствующей ветви, как если бы switch был выполнен повторно, а continue заменил начальное значение switch.

test_switch_continue.zig
const std = @import("std");

test "switch continue" {
    sw: switch (@as(i32, 5)) {
        5 => continue :sw 4,

        // `continue` can occur multiple times within a single switch prong.
        2...4 => |v| {
            if (v > 3) {
                continue :sw 2;
            } else if (v == 3) {

                // `break` can target labeled loops.
                break :sw;
            }

            continue :sw 1;
        },

        1 => return,

        else => unreachable,
    }
}
Оболочка
$ zig test test_switch_continue.zig
1/1 test_switch_continue.test.switch continue...OK
All 1 tests passed.

Семантически это эквивалентно следующему циклу:

test_switch_continue_equivalent.zig
const std = @import("std");

test "switch continue, equivalent loop" {
    var sw: i32 = 5;
    while (true) {
        switch (sw) {
            5 => {
                sw = 4;
                continue;
            },
            2...4 => |v| {
                if (v > 3) {
                    sw = 2;
                    continue;
                } else if (v == 3) {
                    break;
                }

                sw = 1;
                continue;
            },
            1 => return,
            else => unreachable,
        }
    }
}
Оболочка
$ zig test test_switch_continue_equivalent.zig
1/1 test_switch_continue_equivalent.test.switch continue, equivalent loop...OK
All 1 tests passed.

Это может повысить ясность, например, конечных автоматов, где синтаксис continue :sw .next_state однозначен, явен и сразу понятен.

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

test_switch_dispatch_loop.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Instruction = enum {
    add,
    mul,
    end,
};

fn evaluate(initial_stack: []const i32, code: []const Instruction) !i32 {
    var buffer: [8]i32 = undefined;
    var stack = std.ArrayList(i32).initBuffer(&buffer);
    try stack.appendSliceBounded(initial_stack);
    var ip: usize = 0;

    return vm: switch (code[ip]) {
        // Because all code after `continue` is unreachable, this branch does
        // not provide a result.
        .add => {
            try stack.appendBounded(stack.pop().? + stack.pop().?);

            ip += 1;
            continue :vm code[ip];
        },
        .mul => {
            try stack.appendBounded(stack.pop().? * stack.pop().?);

            ip += 1;
            continue :vm code[ip];
        },
        .end => stack.pop().?,
    };
}

test "evaluate" {
    const result = try evaluate(&.{ 7, 2, -3 }, &.{ .mul, .add, .end });
    try expectEqual(1, result);
}
Оболочка
$ zig test test_switch_dispatch_loop.zig
1/1 test_switch_dispatch_loop.test.evaluate...OK
All 1 tests passed.

Если операнд для continue известен на этапе comptime, его можно преобразовать в безусловный переход к соответствующему варианту. Такой переход предсказывается абсолютно точно и поэтому обычно выполняется очень быстро.

Если операнд известен только во время выполнения, каждый continue может содержать встроенный условный переход (в идеале — через таблицу переходов), что позволяет процессору независимо предсказывать цель каждого варианта. При преобразовании в цикл все переходы проходили бы через одну точку диспетчеризации, что затруднило бы предсказание переходов.

Встроенные варианты switch

Варианты switch можно пометить как inline, чтобы сгенерировать тело варианта для каждого возможного значения, которое оно может принимать. Захваченное значение при этом будет известно на этапе comptime.

test_inline_switch.zig
const std = @import("std");
const expect = std.testing.expect;
const expectError = std.testing.expectError;

fn isFieldOptional(comptime T: type, field_index: usize) !bool {
    const fields = @typeInfo(T).@"struct".fields;
    return switch (field_index) {
        // This prong is analyzed twice with `idx` being a
        // comptime-known value each time.
        inline 0, 1 => |idx| @typeInfo(fields[idx].type) == .optional,
        else => return error.IndexOutOfBounds,
    };
}

const Struct1 = struct { a: u32, b: ?u32 };

test "using @typeInfo with runtime values" {
    var index: usize = 0;
    try expect(!try isFieldOptional(Struct1, index));
    index += 1;
    try expect(try isFieldOptional(Struct1, index));
    index += 1;
    try expectError(error.IndexOutOfBounds, isFieldOptional(Struct1, index));
}

// Calls to `isFieldOptional` on `Struct1` get unrolled to an equivalent
// of this function:
fn isFieldOptionalUnrolled(field_index: usize) !bool {
    return switch (field_index) {
        0 => false,
        1 => true,
        else => return error.IndexOutOfBounds,
    };
}
Оболочка
$ zig test test_inline_switch.zig
1/1 test_inline_switch.test.using @typeInfo with runtime values...OK
All 1 tests passed.

Ключевое слово inline также можно использовать с диапазонами:

inline_prong_range.zig
fn isFieldOptional(comptime T: type, field_index: usize) !bool {
    const fields = @typeInfo(T).@"struct".fields;
    return switch (field_index) {
        inline 0...fields.len - 1 => |idx| @typeInfo(fields[idx].type) == .optional,
        else => return error.IndexOutOfBounds,
    };
}

Варианты inline else можно использовать как типобезопасную альтернативу циклам inline for:

test_inline_else.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const SliceTypeA = extern struct {
    len: usize,
    ptr: [*]u32,
};
const SliceTypeB = extern struct {
    ptr: [*]SliceTypeA,
    len: usize,
};
const AnySlice = union(enum) {
    a: SliceTypeA,
    b: SliceTypeB,
    c: []const u8,
    d: []AnySlice,
};

fn withFor(any: AnySlice) usize {
    const Tag = @typeInfo(AnySlice).@"union".tag_type.?;
    inline for (@typeInfo(Tag).@"enum".fields) |field| {
        // With `inline for` the function gets generated as
        // a series of `if` statements relying on the optimizer
        // to convert it to a switch.
        if (field.value == @intFromEnum(any)) {
            return @field(any, field.name).len;
        }
    }
    // When using `inline for` the compiler doesn't know that every
    // possible case has been handled requiring an explicit `unreachable`.
    unreachable;
}

fn withSwitch(any: AnySlice) usize {
    return switch (any) {
        // With `inline else` the function is explicitly generated
        // as the desired switch and the compiler can check that
        // every possible case is handled.
        inline else => |slice| slice.len,
    };
}

test "inline for and inline else similarity" {
    const any = AnySlice{ .c = "hello" };
    try expectEqual(5, withFor(any));
    try expectEqual(5, withSwitch(any));
}
Оболочка
$ zig test test_inline_else.zig
1/1 test_inline_else.test.inline for and inline else similarity...OK
All 1 tests passed.

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

test_inline_switch_union_tag.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const U = union(enum) {
    a: u32,
    b: f32,
};

fn getNum(u: U) u32 {
    switch (u) {
        // Here `num` is a runtime-known value that is either
        // `u.a` or `u.b` and `tag` is `u`'s comptime-known tag value.
        inline else => |num, tag| {
            if (tag == .b) {
                return @intFromFloat(num);
            }
            return num;
        },
    }
}

test "test" {
    const u = U{ .b = 42 };
    try expectEqual(42, getNum(u));
}
Оболочка
$ zig test test_inline_switch_union_tag.zig
1/1 test_inline_switch_union_tag.test.test...OK
All 1 tests passed.

См. также:

  • inline while
  • inline for
  • Тегированное объединение

while

Цикл while используется для многократного выполнения выражения до тех пор, пока некоторое условие истинно.

test_while.zig
const expectEqual = @import("std").testing.expectEqual;

test "while basic" {
    var i: usize = 0;
    while (i < 10) {
        i += 1;
    }
    try expectEqual(10, i);
}
Оболочка
$ zig test test_while.zig
1/1 test_while.test.while basic...OK
All 1 tests passed.

Используйте break, чтобы досрочно выйти из цикла while.

test_while_break.zig
const expectEqual = @import("std").testing.expectEqual;

test "while break" {
    var i: usize = 0;
    while (true) {
        if (i == 10)
            break;
        i += 1;
    }
    try expectEqual(10, i);
}
Оболочка
$ zig test test_while_break.zig
1/1 test_while_break.test.while break...OK
All 1 tests passed.

Используйте continue, чтобы перейти к началу цикла.

test_while_continue.zig
const expectEqual = @import("std").testing.expectEqual;

test "while continue" {
    var i: usize = 0;
    while (true) {
        i += 1;
        if (i < 10)
            continue;
        break;
    }
    try expectEqual(10, i);
}
Оболочка
$ zig test test_while_continue.zig
1/1 test_while_continue.test.while continue...OK
All 1 tests passed.

Циклы while поддерживают выражение продолжения, которое выполняется при переходе к следующей итерации цикла. Ключевое слово continue учитывает это выражение.

test_while_continue_expression.zig
const expectEqual = @import("std").testing.expectEqual;
const expect = @import("std").testing.expect;

test "while loop continue expression" {
    var i: usize = 0;
    while (i < 10) : (i += 1) {}
    try expectEqual(10, i);
}

test "while loop continue expression, more complicated" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) : ({
        i *= 2;
        j *= 3;
    }) {
        const my_ij = i * j;
        try expect(my_ij < 2000);
    }
}
Оболочка
$ zig test test_while_continue_expression.zig
1/2 test_while_continue_expression.test.while loop continue expression...OK
2/2 test_while_continue_expression.test.while loop continue expression, more complicated...OK
All 2 tests passed.

Циклы while являются выражениями. Результатом выражения является результат ветви else цикла while, которая выполняется, если при проверке условие цикла while оказывается ложным.

break, как и return, принимает параметр-значение. Это результат выражения while. При выходе из цикла while с помощью break ветвь else не вычисляется.

test_while_else.zig
const expect = @import("std").testing.expect;

test "while else" {
    try expect(rangeHasNumber(0, 10, 5));
    try expect(!rangeHasNumber(0, 10, 15));
}

fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
    var i = begin;
    return while (i < end) : (i += 1) {
        if (i == number) {
            break true;
        }
    } else false;
}
Оболочка
$ zig test test_while_else.zig
1/1 test_while_else.test.while else...OK
All 1 tests passed.

Именованный while

Если циклу while присвоено имя, на него можно ссылаться из break или continue во вложенном цикле:

test_while_nested_break.zig
test "nested break" {
    outer: while (true) {
        while (true) {
            break :outer;
        }
    }
}

test "nested continue" {
    var i: usize = 0;
    outer: while (i < 10) : (i += 1) {
        while (true) {
            continue :outer;
        }
    }
}
Оболочка
$ zig test test_while_nested_break.zig
1/2 test_while_nested_break.test.nested break...OK
2/2 test_while_nested_break.test.nested continue...OK
All 2 tests passed.

while с optional-значениями

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

Если в выражении while присутствует синтаксис |x|, условие while должно иметь optional-тип.

При итерации по optional-значению допускается ветвь else. В этом случае она выполнится при первом встреченном значении null.

test_while_null_capture.zig
const expectEqual = @import("std").testing.expectEqual;

test "while null capture" {
    var sum1: u32 = 0;
    numbers_left = 3;
    while (eventuallyNullSequence()) |value| {
        sum1 += value;
    }
    try expectEqual(3, sum1);

    // null capture with an else block
    var sum2: u32 = 0;
    numbers_left = 3;
    while (eventuallyNullSequence()) |value| {
        sum2 += value;
    } else {
        try expectEqual(3, sum2);
    }

    // null capture with a continue expression
    var i: u32 = 0;
    var sum3: u32 = 0;
    numbers_left = 3;
    while (eventuallyNullSequence()) |value| : (i += 1) {
        sum3 += value;
    }
    try expectEqual(3, i);
}

var numbers_left: u32 = undefined;
fn eventuallyNullSequence() ?u32 {
    return if (numbers_left == 0) null else blk: {
        numbers_left -= 1;
        break :blk numbers_left;
    };
}
Оболочка
$ zig test test_while_null_capture.zig
1/1 test_while_null_capture.test.while null capture...OK
All 1 tests passed.

while с объединениями ошибок

Подобно выражениям if, циклы while могут принимать объединение ошибок в качестве условия и захватывать полезную нагрузку или код ошибки. Если условие возвращает код ошибки, вычисляется ветвь else и цикл завершается.

Если в выражении while присутствует синтаксис else |x|, условие while должно иметь тип объединения ошибок.

test_while_error_capture.zig
const expectEqual = @import("std").testing.expectEqual;

test "while error union capture" {
    var sum1: u32 = 0;
    numbers_left = 3;
    while (eventuallyErrorSequence()) |value| {
        sum1 += value;
    } else |err| {
        try expectEqual(error.ReachedZero, err);
    }
}

var numbers_left: u32 = undefined;

fn eventuallyErrorSequence() anyerror!u32 {
    return if (numbers_left == 0) error.ReachedZero else blk: {
        numbers_left -= 1;
        break :blk numbers_left;
    };
}
Оболочка
$ zig test test_while_error_capture.zig
1/1 test_while_error_capture.test.while error union capture...OK
All 1 tests passed.

inline while

Циклы while можно встраивать. В этом случае цикл разворачивается, что позволяет коду выполнять действия, возможные только на этапе компиляции, например использовать типы как значения первого класса.

test_inline_while.zig
const expectEqual = @import("std").testing.expectEqual;

test "inline while loop" {
    comptime var i = 0;
    var sum: usize = 0;
    inline while (i < 3) : (i += 1) {
        const T = switch (i) {
            0 => f32,
            1 => i8,
            2 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }
    try expectEqual(9, sum);
}

fn typeNameLength(comptime T: type) usize {
    return @typeName(T).len;
}
Оболочка
$ zig test test_inline_while.zig
1/1 test_inline_while.test.inline while loop...OK
All 1 tests passed.

Рекомендуется использовать циклы inline только по одной из следующих причин:

  • Для корректной работы семантики цикл должен выполняться на этапе comptime.
  • У вас есть результаты тестирования производительности, доказывающие, что принудительное разворачивание цикла таким способом заметно ускоряет его выполнение.

См. также:

  • if
  • Optional-значения
  • Ошибки
  • comptime
  • unreachable

for

test_for.zig
const expectEqual = @import("std").testing.expectEqual;

test "for basics" {
    const items = [_]i32{ 4, 5, 3, 4, 0 };
    var sum: i32 = 0;

    // For loops iterate over slices and arrays.
    for (items) |value| {
        // Break and continue are supported.
        if (value == 0) {
            continue;
        }
        sum += value;
    }
    try expectEqual(16, sum);

    // To iterate over a portion of a slice, reslice.
    for (items[0..1]) |value| {
        sum += value;
    }
    try expectEqual(20, sum);

    // To access the index of iteration, specify a second condition as well
    // as a second capture value.
    var sum2: i32 = 0;
    for (items, 0..) |_, i| {
        try expectEqual(usize, @TypeOf(i));
        sum2 += @as(i32, @intCast(i));
    }
    try expectEqual(10, sum2);

    // To iterate over consecutive integers, use the range syntax.
    // Unbounded range is always a compile error.
    var sum3: usize = 0;
    for (0..5) |i| {
        sum3 += i;
    }
    try expectEqual(10, sum3);
}

test "multi object for" {
    const items = [_]usize{ 1, 2, 3 };
    const items2 = [_]usize{ 4, 5, 6 };
    var count: usize = 0;

    // Iterate over multiple objects.
    // All lengths must be equal at the start of the loop, otherwise detectable
    // illegal behavior occurs.
    for (items, items2) |i, j| {
        count += i + j;
    }

    try expectEqual(21, count);
}

test "for reference" {
    var items = [_]i32{ 3, 4, 2 };

    // Iterate over the slice by reference by
    // specifying that the capture value is a pointer.
    for (&items) |*value| {
        value.* += 1;
    }

    try expectEqual(4, items[0]);
    try expectEqual(5, items[1]);
    try expectEqual(3, items[2]);
}

test "for else" {
    // For allows an else attached to it, the same as a while loop.
    const items = [_]?i32{ 3, 4, null, 5 };

    // For loops can also be used as expressions.
    // Similar to while loops, when you break from a for loop, the else branch is not evaluated.
    var sum: i32 = 0;
    const result = for (items) |value| {
        if (value != null) {
            sum += value.?;
        }
    } else blk: {
        try expectEqual(12, sum);
        break :blk sum;
    };
    try expectEqual(12, result);
}
Оболочка
$ zig test test_for.zig
1/4 test_for.test.for basics...OK
2/4 test_for.test.multi object for...OK
3/4 test_for.test.for reference...OK
4/4 test_for.test.for else...OK
All 4 tests passed.

Именованный for

Если циклу for присвоено имя, на него можно ссылаться из break или continue во вложенном цикле:

test_for_nested_break.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "nested break" {
    var count: usize = 0;
    outer: for (1..6) |_| {
        for (1..6) |_| {
            count += 1;
            break :outer;
        }
    }
    try expectEqual(1, count);
}

test "nested continue" {
    var count: usize = 0;
    outer: for (1..9) |_| {
        for (1..6) |_| {
            count += 1;
            continue :outer;
        }
    }

    try expectEqual(8, count);
}
Оболочка
$ zig test test_for_nested_break.zig
1/2 test_for_nested_break.test.nested break...OK
2/2 test_for_nested_break.test.nested continue...OK
All 2 tests passed.

inline for

Циклы for можно встраивать. В этом случае цикл разворачивается, что позволяет коду выполнять действия, возможные только на этапе компиляции, например использовать типы как значения первого класса. Значения захвата и итератора во встроенных циклах for известны во время компиляции.

test_inline_for.zig
const expectEqual = @import("std").testing.expectEqual;

test "inline for loop" {
    const nums = [_]i32{ 2, 4, 6 };
    var sum: usize = 0;
    inline for (nums) |i| {
        const T = switch (i) {
            2 => f32,
            4 => i8,
            6 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }
    try expectEqual(9, sum);
}

fn typeNameLength(comptime T: type) usize {
    return @typeName(T).len;
}
Оболочка
$ zig test test_inline_for.zig
1/1 test_inline_for.test.inline for loop...OK
All 1 tests passed.

Рекомендуется использовать циклы inline только по одной из следующих причин:

  • Для корректной работы семантики цикл должен выполняться на этапе comptime.
  • У вас есть результаты тестирования производительности, доказывающие, что принудительное разворачивание цикла таким способом заметно ускоряет его выполнение.

См. также:

  • while
  • comptime
  • Массивы
  • Срезы

if

test_if.zig
// If expressions have three uses, corresponding to the three types:
// * bool
// * ?T
// * anyerror!T

const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;

test "if expression" {
    // If expressions are used instead of a ternary expression.
    const a: u32 = 5;
    const b: u32 = 4;
    const result = if (a != b) 47 else 3089;
    try expectEqual(result, 47);
}

test "if boolean" {
    // If expressions test boolean conditions.
    const a: u32 = 5;
    const b: u32 = 4;
    if (a != b) {
        try expect(true);
    } else if (a == 9) {
        unreachable;
    } else {
        unreachable;
    }
}

test "if error union" {
    // If expressions test for errors.
    // Note the |err| capture on the else.

    const a: anyerror!u32 = 0;
    if (a) |value| {
        try expectEqual(value, 0);
    } else |err| {
        _ = err;
        unreachable;
    }

    const b: anyerror!u32 = error.BadValue;
    if (b) |value| {
        _ = value;
        unreachable;
    } else |err| {
        try expectEqual(err, error.BadValue);
    }

    // The else and |err| capture is strictly required.
    if (a) |value| {
        try expectEqual(value, 0);
    } else |_| {}

    // To check only the error value, use an empty block expression.
    if (b) |_| {} else |err| {
        try expectEqual(err, error.BadValue);
    }

    // Access the value by reference using a pointer capture.
    var c: anyerror!u32 = 3;
    if (c) |*value| {
        value.* = 9;
    } else |_| {
        unreachable;
    }

    if (c) |value| {
        try expectEqual(value, 9);
    } else |_| {
        unreachable;
    }
}
Оболочка
$ zig test test_if.zig
1/3 test_if.test.if expression...OK
2/3 test_if.test.if boolean...OK
3/3 test_if.test.if error union...OK
All 3 tests passed.

if с optional-значениями

test_if_optionals.zig
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;

test "if optional" {
    // If expressions test for null.

    const a: ?u32 = 0;
    if (a) |value| {
        try expectEqual(0, value);
    } else {
        unreachable;
    }

    const b: ?u32 = null;
    if (b) |_| {
        unreachable;
    } else {
        try expect(true);
    }

    // The else is not required.
    if (a) |value| {
        try expectEqual(0, value);
    }

    // To test against null only, use the binary equality operator.
    if (b == null) {
        try expect(true);
    }

    // Access the value by reference using a pointer capture.
    var c: ?u32 = 3;
    if (c) |*value| {
        value.* = 2;
    }

    if (c) |value| {
        try expectEqual(2, value);
    } else {
        unreachable;
    }
}

test "if error union with optional" {
    // If expressions test for errors before unwrapping optionals.
    // The |optional_value| capture's type is ?u32.

    const a: anyerror!?u32 = 0;
    if (a) |optional_value| {
        try expectEqual(0, optional_value.?);
    } else |err| {
        _ = err;
        unreachable;
    }

    const b: anyerror!?u32 = null;
    if (b) |optional_value| {
        try expectEqual(null, optional_value);
    } else |_| {
        unreachable;
    }

    const c: anyerror!?u32 = error.BadValue;
    if (c) |optional_value| {
        _ = optional_value;
        unreachable;
    } else |err| {
        try expectEqual(error.BadValue, err);
    }

    // Access the value by reference by using a pointer capture each time.
    var d: anyerror!?u32 = 3;
    if (d) |*optional_value| {
        if (optional_value.*) |*value| {
            value.* = 9;
        }
    } else |_| {
        unreachable;
    }

    if (d) |optional_value| {
        try expectEqual(9, optional_value.?);
    } else |_| {
        unreachable;
    }
}
Оболочка
$ zig test test_if_optionals.zig
1/2 test_if_optionals.test.if optional...OK
2/2 test_if_optionals.test.if error union with optional...OK
All 2 tests passed.

См. также:

  • Optional-значения
  • Ошибки

defer

Безусловно выполняет выражение при выходе из области видимости.

test_defer.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const print = std.debug.print;

fn deferExample() !usize {
    var a: usize = 1;

    {
        defer a = 2;
        a = 1;
    }
    try expectEqual(2, a);

    a = 5;
    return a;
}

test "defer basics" {
    try expectEqual(5, (try deferExample()));
}
Оболочка
$ zig test test_defer.zig
1/1 test_defer.test.defer basics...OK
All 1 tests passed.

Выражения defer вычисляются в обратном порядке.

defer_unwind.zig
const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    print("\n", .{});

    defer {
        print("1 ", .{});
    }
    defer {
        print("2 ", .{});
    }
    if (false) {
        // defers are not run if they are never executed.
        defer {
            print("3 ", .{});
        }
    }
}
Оболочка
$ zig build-exe defer_unwind.zig
$ ./defer_unwind

2 1

Внутри выражения defer оператор return не допускается.

test_invalid_defer.zig
fn deferInvalidExample() !void {
    defer {
        return error.DeferError;
    }

    return error.DeferError;
}
Оболочка
$ zig test test_invalid_defer.zig
/home/andy/src/zig/doc/langref/test_invalid_defer.zig:3:9: error: cannot return from defer expression
        return error.DeferError;
        ^~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_invalid_defer.zig:2:5: note: defer expression here
    defer {
    ^~~~~

См. также:

  • Ошибки

unreachable

В режимах Debug и ReleaseSafe unreachable генерирует вызов panic с сообщением reached unreachable code.

В режимах ReleaseFast и ReleaseSmall оптимизатор использует предположение, что код unreachable никогда не будет выполнен, для выполнения оптимизаций.

Основы

test_unreachable.zig
// unreachable is used to assert that control flow will never reach a
// particular location:
test "basic math" {
    const x = 1;
    const y = 2;
    if (x + y != 3) {
        unreachable;
    }
}
Оболочка
$ zig test test_unreachable.zig
1/1 test_unreachable.test.basic math...OK
All 1 tests passed.

Фактически, именно так реализован std.debug.assert:

test_assertion_failure.zig
// This is how std.debug.assert is implemented
fn assert(ok: bool) void {
    if (!ok) unreachable; // assertion failure
}

// This test will fail because we hit unreachable.
test "this will fail" {
    assert(false);
}
Оболочка
$ zig test test_assertion_failure.zig
1/1 test_assertion_failure.test.this will fail...thread 932713 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_assertion_failure.zig:3:14: 0x1234e59 in assert (test_assertion_failure.zig)
    if (!ok) unreachable; // assertion failure
             ^
/home/andy/src/zig/doc/langref/test_assertion_failure.zig:8:11: 0x1234e2e in test.this will fail (test_assertion_failure.zig)
    assert(false);
          ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/1209ea68e9a74969bb29b43362f9d1da/test --seed=0x3aa77c99

На этапе компиляции

test_comptime_unreachable.zig
const assert = @import("std").debug.assert;

test "type of unreachable" {
    comptime {
        // The type of unreachable is noreturn.

        // However this assertion will still fail to compile because
        // unreachable expressions are compile errors.

        assert(@TypeOf(unreachable) == noreturn);
    }
}
Оболочка
$ zig test test_comptime_unreachable.zig
/home/andy/src/zig/doc/langref/test_comptime_unreachable.zig:10:16: error: unreachable code
        assert(@TypeOf(unreachable) == noreturn);
               ^~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_comptime_unreachable.zig:10:24: note: control flow is diverted here
        assert(@TypeOf(unreachable) == noreturn);
                       ^~~~~~~~~~~

См. также:

  • Тестирование Zig
  • Режим сборки
  • comptime

noreturn

noreturn — это тип для:

  • break
  • continue
  • return
  • unreachable
  • while (true) {}

При согласовании типов, например в ветвях if или вариантах switch, тип noreturn совместим с любым другим типом. Рассмотрим пример:

test_noreturn.zig
fn foo(condition: bool, b: u32) void {
    const a = if (condition) b else return;
    _ = a;
    @panic("do something with a");
}
test "noreturn" {
    foo(false, 1);
}
Оболочка
$ zig test test_noreturn.zig
1/1 test_noreturn.test.noreturn...OK
All 1 tests passed.

Еще один вариант использования noreturn — функция exit:

test_noreturn_from_exit.zig
const std = @import("std");
const builtin = @import("builtin");
const native_arch = builtin.cpu.arch;
const expectEqual = std.testing.expectEqual;

const WINAPI: std.builtin.CallingConvention = if (native_arch == .x86) .{ .x86_stdcall = .{} } else .c;
extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(WINAPI) noreturn;

test "foo" {
    const value = bar() catch ExitProcess(1);
    try expectEqual(1234, value);
}

fn bar() anyerror!u32 {
    return 1234;
}
Оболочка
$ zig test test_noreturn_from_exit.zig -target x86_64-windows --test-no-exec

Функции

test_functions.zig
const std = @import("std");
const builtin = @import("builtin");
const native_arch = builtin.cpu.arch;
const expectEqual = std.testing.expectEqual;

// Functions are declared like this
fn add(a: i8, b: i8) i8 {
    if (a == 0) {
        return b;
    }

    return a + b;
}

// The export specifier makes a function externally visible in the generated
// object file, and makes it use the C ABI.
export fn sub(a: i8, b: i8) i8 {
    return a - b;
}

// The extern specifier is used to declare a function that will be resolved
// at link time, when linking statically, or at runtime, when linking
// dynamically. The quoted identifier after the extern keyword specifies
// the library that has the function. (e.g. "c" -> libc.so)
// The callconv specifier changes the calling convention of the function.
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;

// The @branchHint builtin can be used to tell the optimizer that a function is rarely called ("cold").
fn abort() noreturn {
    @branchHint(.cold);
    while (true) {}
}

// The naked calling convention makes a function not have any function prologue or epilogue.
// This can be useful when integrating with assembly.
fn _start() callconv(.naked) noreturn {
    abort();
}

// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
inline fn shiftLeftOne(a: u32) u32 {
    return a << 1;
}

// The pub specifier allows the function to be visible when importing.
// Another file can use @import and call sub2
pub fn sub2(a: i8, b: i8) i8 {
    return a - b;
}

// Function pointers are prefixed with `*const `.
const Call2Op = *const fn (a: i8, b: i8) i8;
fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
    return fnCall(op1, op2);
}

test "function" {
    try expectEqual(11, doOp(add, 5, 6));
    try expectEqual(-1, doOp(sub2, 5, 6));
}
Оболочка
$ zig test test_functions.zig
1/1 test_functions.test.function...OK
All 1 tests passed.

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

Параметры, передаваемые по значению

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

Структуры, объединения и массивы иногда эффективнее передавать по ссылке, поскольку стоимость копирования может быть очень высокой в зависимости от размера. При передаче таких типов в качестве параметров Zig может выбрать копирование и передачу по значению либо передачу по ссылке — в зависимости от того, какой способ, по мнению Zig, будет быстрее. Отчасти это возможно благодаря неизменяемости параметров.

test_pass_by_reference_or_value.zig
const Point = struct {
    x: i32,
    y: i32,
};

fn foo(point: Point) i32 {
    // Here, `point` could be a reference, or a copy. The function body
    // can ignore the difference and treat it as a value. Be very careful
    // taking the address of the parameter - it should be treated as if
    // the address will become invalid when the function returns.
    return point.x + point.y;
}

const expectEqual = @import("std").testing.expectEqual;

test "pass struct to function" {
    try expectEqual(3, foo(Point{ .x = 1, .y = 2 }));
}
Оболочка
$ zig test test_pass_by_reference_or_value.zig
1/1 test_pass_by_reference_or_value.test.pass struct to function...OK
All 1 tests passed.

Для внешних функций Zig следует соглашению C ABI при передаче структур и объединений по значению.

Вывод типов параметров функции

Вместо типа параметры функции можно объявлять с помощью anytype. В этом случае типы параметров будут выводиться при вызове функции. Используйте @TypeOf и @typeInfo, чтобы получить сведения о выведенном типе.

test_fn_type_inference.zig
const expectEqual = @import("std").testing.expectEqual;

fn addFortyTwo(x: anytype) @TypeOf(x) {
    return x + 42;
}

test "fn type inference" {
    try expectEqual(43, addFortyTwo(1));
    try expectEqual(comptime_int, @TypeOf(addFortyTwo(1)));
    const y: i64 = 2;
    try expectEqual(44, addFortyTwo(y));
    try expectEqual(i64, @TypeOf(addFortyTwo(y)));
}
Оболочка
$ zig test test_fn_type_inference.zig
1/1 test_fn_type_inference.test.fn type inference...OK
All 1 tests passed.

inline fn

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

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

inline_call.zig
const std = @import("std");

pub fn main() void {
    if (foo(1200, 34) != 1234) {
        @compileError("bad");
    }
}

inline fn foo(a: i32, b: i32) i32 {
    std.debug.print("runtime a = {} b = {}", .{ a, b });
    return a + b;
}
Оболочка
$ zig build-exe inline_call.zig
$ ./inline_call
runtime a = 1200 b = 34

Если удалить inline, тест завершится ошибкой компиляции вместо успешного выполнения.

Как правило, лучше позволить компилятору самостоятельно решать, когда встраивать функцию, за исключением следующих случаев:

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

Обратите внимание, что inline фактически ограничивает возможности компилятора. Это может отрицательно сказаться на размере двоичного файла, скорости компиляции и даже производительности во время выполнения.

Рефлексия функций

test_fn_reflection.zig
const std = @import("std");
const math = std.math;
const testing = std.testing;

test "fn reflection" {
    try testing.expectEqual(bool, @typeInfo(@TypeOf(testing.expect)).@"fn".params[0].type.?);
    try testing.expectEqual(testing.TmpDir, @typeInfo(@TypeOf(testing.tmpDir)).@"fn".return_type.?);

    try testing.expect(@typeInfo(@TypeOf(math.Log2Int)).@"fn".is_generic);
}
Оболочка
$ zig test test_fn_reflection.zig
1/1 test_fn_reflection.test.fn reflection...OK
All 1 tests passed.

Ошибки

Тип множества ошибок

Множество ошибок похоже на перечисление. Однако каждой ошибке с уникальным именем во всей компиляции присваивается беззнаковое целое число больше 0. Одно и то же имя ошибки можно объявлять несколько раз — в этом случае ему будет присвоено одно и то же целое значение.

По умолчанию тип множества ошибок — u16, однако если максимальное число различных значений ошибок указано с помощью параметра командной строки --error-limit [num], будет использоваться целочисленный тип с минимальным количеством битов, необходимым для представления всех значений ошибок.

Ошибку из подмножества можно привести к надмножеству:

test_coerce_error_subset_to_superset.zig
const std = @import("std");

const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

const AllocationError = error{
    OutOfMemory,
};

test "coerce subset to superset" {
    const err = foo(AllocationError.OutOfMemory);
    try std.testing.expectEqual(FileOpenError.OutOfMemory, err);
}

fn foo(err: AllocationError) FileOpenError {
    return err;
}
Оболочка
$ zig test test_coerce_error_subset_to_superset.zig
1/1 test_coerce_error_subset_to_superset.test.coerce subset to superset...OK
All 1 tests passed.

Но нельзя привести ошибку из надмножества к подмножеству:

test_coerce_error_superset_to_subset.zig
const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

const AllocationError = error{
    OutOfMemory,
};

test "coerce superset to subset" {
    foo(FileOpenError.OutOfMemory) catch {};
}

fn foo(err: FileOpenError) AllocationError {
    return err;
}
Оболочка
$ zig test test_coerce_error_superset_to_subset.zig
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: error: expected type 'error{OutOfMemory}', found 'error{AccessDenied,FileNotFound,OutOfMemory}'
    return err;
           ^~~
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.AccessDenied' not a member of destination error set
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.FileNotFound' not a member of destination error set
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:15:28: note: function return type declared here
fn foo(err: FileOpenError) AllocationError {
                           ^~~~~~~~~~~~~~~
referenced by:
    test.coerce superset to subset: /home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:12:8

Для объявления множества ошибок с единственным значением и получения этого значения предусмотрено сокращённое обозначение:

single_value_error_set_shortcut.zig
const err = error.FileNotFound;

Это эквивалентно следующему:

single_value_error_set.zig
const err = (error{FileNotFound}).FileNotFound;

Это удобно при использовании множеств ошибок с выведенным типом.

Глобальное множество ошибок

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

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

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

Тип объединения ошибок

Множество ошибок и обычный тип можно объединить с помощью бинарного оператора !, чтобы получить тип объединения ошибок. Вероятно, тип объединения ошибок вы будете использовать чаще, чем отдельный тип множества ошибок.

Ниже приведена функция для преобразования строки в 64-битное целое число:

error_union_parsing_u64.zig
const std = @import("std");
const maxInt = std.math.maxInt;

pub fn parseU64(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = charToDigit(c);

        if (digit >= radix) {
            return error.InvalidChar;
        }

        // x *= radix
        var ov = @mulWithOverflow(x, radix);
        if (ov[1] != 0) return error.OverFlow;

        // x += digit
        ov = @addWithOverflow(ov[0], digit);
        if (ov[1] != 0) return error.OverFlow;
        x = ov[0];
    }

    return x;
}

fn charToDigit(c: u8) u8 {
    return switch (c) {
        '0'...'9' => c - '0',
        'A'...'Z' => c - 'A' + 10,
        'a'...'z' => c - 'a' + 10,
        else => maxInt(u8),
    };
}

test "parse u64" {
    const result = try parseU64("1234", 10);
    try std.testing.expectEqual(1234, result);
}
Оболочка
$ zig test error_union_parsing_u64.zig
1/1 error_union_parsing_u64.test.parse u64...OK
All 1 tests passed.

Обратите внимание, что возвращаемый тип — !u64. Это означает, что функция возвращает либо беззнаковое 64-битное целое число, либо ошибку. Мы не указали множество ошибок слева от !, поэтому оно выводится автоматически.

В определении функции видны операторы return, возвращающие ошибку, а в конце — оператор return, возвращающий u64. Оба типа приводятся к anyerror!u64.

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

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

catch

Чтобы указать значение по умолчанию, можно использовать бинарный оператор catch:

catch.zig
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;

fn doAThing(str: []u8) void {
    const number = parseU64(str, 10) catch 13;
    _ = number; // ...
}

В этом коде number будет содержать успешно разобранную строку или значение по умолчанию 13. Тип правой части бинарного оператора catch должен совпадать с типом объединения ошибок после извлечения значения либо быть типа noreturn.

Если требуется указать значение по умолчанию с помощью catch после выполнения некоторой логики, можно сочетать catch с именованными блоками:

handle_error_with_catch_block.zig
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;

fn doAThing(str: []u8) void {
    const number = parseU64(str, 10) catch blk: {
        // do things
        break :blk 13;
    };
    _ = number; // number is now initialized
}

try

Предположим, что в случае ошибки вы хотите вернуть её, а иначе продолжить выполнение функции:

catch_err_return.zig
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;

fn doAThing(str: []u8) !void {
    const number = parseU64(str, 10) catch |err| return err;
    _ = number; // ...
}

Для этого есть сокращённая запись — выражение try:

try.zig
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;

fn doAThing(str: []u8) !void {
    const number = try parseU64(str, 10);
    _ = number; // ...
}

try вычисляет выражение объединения ошибок. Если оно содержит ошибку, выражение возвращает ту же ошибку из текущей функции. В противном случае результатом выражения становится извлечённое значение.

Возможно, вы совершенно уверены, что выражение никогда не вернёт ошибку. Тогда можно написать так:

const number = parseU64("1234", 10) catch unreachable;

Здесь мы точно знаем, что строка «1234» будет успешно преобразована. Поэтому справа указываем значение unreachable. unreachable вызывает недопустимое поведение с проверкой безопасности, поэтому в режимах Debug и ReleaseSafe по умолчанию приводит к аварийному завершению при срабатывании проверки безопасности. Таким образом, если во время отладки приложения здесь неожиданно возникнет ошибка, приложение завершится сбоем, как и должно.

Возможно, вы захотите выполнять разные действия в разных ситуациях. Для этого можно объединить выражения if и switch:

handle_all_error_scenarios.zig
fn doAThing(str: []u8) void {
    if (parseU64(str, 10)) |number| {
        doSomethingWithNumber(number);
    } else |err| switch (err) {
        error.Overflow => {
            // handle overflow...
        },
        // we promise that InvalidChar won't happen (or crash in debug mode if it does)
        error.InvalidChar => unreachable,
    }
}

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

handle_some_error_scenarios.zig
fn doAnotherThing(str: []u8) error{InvalidChar}!void {
    if (parseU64(str, 10)) |number| {
        doSomethingWithNumber(number);
    } else |err| switch (err) {
        error.Overflow => {
            // handle overflow...
        },
        else => |leftover_err| return leftover_err,
    }
}

Необходимо использовать синтаксис захвата переменной. Если переменная не нужна, её можно захватить с помощью _, избегая switch.

handle_no_error_scenarios.zig
fn doADifferentThing(str: []u8) void {
    if (parseU64(str, 10)) |number| {
        doSomethingWithNumber(number);
    } else |_| {
        // do as you'd like
    }
}

errdefer

Ещё одна составляющая обработки ошибок — операторы defer. Помимо безусловного defer, в Zig есть errdefer: отложенное выражение вычисляется при выходе из блока только в том случае, если функция завершилась с ошибкой внутри этого блока.

Пример:

errdefer_example.zig
fn createFoo(param: i32) !Foo {
    const foo = try tryToAllocateFoo();
    // now we have allocated foo. we need to free it if the function fails.
    // but we want to return it if the function succeeds.
    errdefer deallocateFoo(foo);

    const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
    // tmp_buf is truly a temporary resource, and we for sure want to clean it up
    // before this block leaves scope
    defer deallocateTmpBuffer(tmp_buf);

    if (param > 1337) return error.InvalidParam;

    // here the errdefer will not run since we're returning success from the function.
    // but the defer will run!
    return foo;
}

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

Оператор errdefer может дополнительно захватывать ошибку:

test_errdefer_capture.zig
const std = @import("std");

fn captureError(captured: *?anyerror) !void {
    errdefer |err| {
        captured.* = err;
    }
    return error.GeneralFailure;
}

test "errdefer capture" {
    var captured: ?anyerror = null;

    if (captureError(&captured)) unreachable else |err| {
        try std.testing.expectEqual(error.GeneralFailure, captured.?);
        try std.testing.expectEqual(error.GeneralFailure, err);
    }
}
Оболочка
$ zig test test_errdefer_capture.zig
1/1 test_errdefer_capture.test.errdefer capture...OK
All 1 tests passed.

Ещё несколько замечаний об обработке ошибок:

  • Эти средства достаточно выразительны, чтобы отсутствие проверки ошибки приводило к ошибке компиляции. Если вы действительно хотите проигнорировать ошибку, можно добавить catch unreachable и получить дополнительную гарантию: приложение аварийно завершится в режимах Debug и ReleaseSafe, если ваше предположение окажется неверным.
  • Поскольку Zig понимает типы ошибок, он может отдавать предпочтение ветвям, в которых ошибка не возникает. Это небольшое преимущество в производительности, недоступное в других языках.

См. также:

  • defer
  • if
  • switch

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

test_error_union.zig
const expectEqual = @import("std").testing.expectEqual;

test "error union" {
    var foo: anyerror!i32 = undefined;

    // Coerce from child type of an error union:
    foo = 1234;

    // Coerce from an error set:
    foo = error.SomeError;

    // Use compile-time reflection to access the payload type of an error union:
    try comptime expectEqual(i32, @typeInfo(@TypeOf(foo)).error_union.payload);

    // Use compile-time reflection to access the error set type of an error union:
    try comptime expectEqual(anyerror, @typeInfo(@TypeOf(foo)).error_union.error_set);
}
Оболочка
$ zig test test_error_union.zig
1/1 test_error_union.test.error union...OK
All 1 tests passed.

Объединение множеств ошибок

Используйте оператор || для объединения двух множеств ошибок. Результирующее множество содержит ошибки из обоих множеств. Документационные комментарии левой части имеют приоритет над комментариями правой части. В этом примере документационный комментарий для C.PathNotFound — A doc comment.

Это особенно полезно для функций, которые возвращают разные множества ошибок в зависимости от ветвей comptime. Например, стандартная библиотека Zig использует LinuxFileOpenError || WindowsFileOpenError для множества ошибок при открытии файлов.

test_merging_error_sets.zig
const A = error{
    NotDir,

    /// A doc comment
    PathNotFound,
};
const B = error{
    OutOfMemory,

    /// B doc comment
    PathNotFound,
};

const C = A || B;

fn foo() C!void {
    return error.NotDir;
}

test "merge error sets" {
    if (foo()) {
        @panic("unexpected");
    } else |err| switch (err) {
        error.OutOfMemory => @panic("unexpected"),
        error.PathNotFound => @panic("unexpected"),
        error.NotDir => {},
    }
}
Оболочка
$ zig test test_merging_error_sets.zig
1/1 test_merging_error_sets.test.merge error sets...OK
All 1 tests passed.

Множества ошибок с выведенным типом

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

test_inferred_error_sets.zig
// With an inferred error set
pub fn add_inferred(comptime T: type, a: T, b: T) !T {
    const ov = @addWithOverflow(a, b);
    if (ov[1] != 0) return error.Overflow;
    return ov[0];
}

// With an explicit error set
pub fn add_explicit(comptime T: type, a: T, b: T) Error!T {
    const ov = @addWithOverflow(a, b);
    if (ov[1] != 0) return error.Overflow;
    return ov[0];
}

const Error = error{
    Overflow,
};

const std = @import("std");

test "inferred error set" {
    if (add_inferred(u8, 255, 1)) |_| unreachable else |err| switch (err) {
        error.Overflow => {}, // ok
    }
}
Оболочка
$ zig test test_inferred_error_sets.zig
1/1 test_inferred_error_sets.test.inferred error set...OK
All 1 tests passed.

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

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

Возможно, в будущей версии Zig эти ограничения будут устранены.

Трассировки возврата ошибок

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

error_return_trace.zig
pub fn main() !void {
    try foo(12);
}

fn foo(x: i32) !void {
    if (x >= 5) {
        try bar();
    } else {
        try bang2();
    }
}

fn bar() !void {
    if (baz()) {
        try quux();
    } else |err| switch (err) {
        error.FileNotFound => try hello(),
    }
}

fn baz() !void {
    try bang1();
}

fn quux() !void {
    try bang2();
}

fn hello() !void {
    try bang2();
}

fn bang1() !void {
    return error.FileNotFound;
}

fn bang2() !void {
    return error.PermissionDenied;
}
Оболочка
$ zig build-exe error_return_trace.zig
$ ./error_return_trace
error: PermissionDenied
/home/andy/src/zig/doc/langref/error_return_trace.zig:34:5: 0x11d4b9c in bang1 (error_return_trace.zig)
    return error.FileNotFound;
    ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:22:5: 0x11d4bfc in baz (error_return_trace.zig)
    try bang1();
    ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:38:5: 0x11d4c3c in bang2 (error_return_trace.zig)
    return error.PermissionDenied;
    ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:30:5: 0x11d4d1c in hello (error_return_trace.zig)
    try bang2();
    ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:17:31: 0x11d4e2a in bar (error_return_trace.zig)
        error.FileNotFound => try hello(),
                              ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:7:9: 0x11d4f0a in foo (error_return_trace.zig)
        try bar();
        ^
/home/andy/src/zig/doc/langref/error_return_trace.zig:2:5: 0x11d5011 in main (error_return_trace.zig)
    try foo(12);
    ^

Внимательно рассмотрите этот пример. Это не трассировка стека.

Видно, что в итоге была возвращена ошибка PermissionDenied, но исходной ошибкой, с которой всё началось, была FileNotFound. В функции bar код обрабатывает исходный код ошибки, а затем возвращает другую ошибку из оператора switch. Трассировки возврата ошибок наглядно показывают это, тогда как трассировка стека выглядела бы так:

stack_trace.zig
pub fn main() void {
    foo(12);
}

fn foo(x: i32) void {
    if (x >= 5) {
        bar();
    } else {
        bang2();
    }
}

fn bar() void {
    if (baz()) {
        quux();
    } else {
        hello();
    }
}

fn baz() bool {
    return bang1();
}

fn quux() void {
    bang2();
}

fn hello() void {
    bang2();
}

fn bang1() bool {
    return false;
}

fn bang2() void {
    @panic("PermissionDenied");
}
Оболочка
$ zig build-exe stack_trace.zig
$ ./stack_trace
thread 930211 panic: PermissionDenied
/home/andy/src/zig/doc/langref/stack_trace.zig:38:5: 0x11d5e7a in bang2 (stack_trace.zig)
    @panic("PermissionDenied");
    ^
/home/andy/src/zig/doc/langref/stack_trace.zig:30:10: 0x11d5efc in hello (stack_trace.zig)
    bang2();
         ^
/home/andy/src/zig/doc/langref/stack_trace.zig:17:14: 0x11d5eb3 in bar (stack_trace.zig)
        hello();
             ^
/home/andy/src/zig/doc/langref/stack_trace.zig:7:12: 0x11d5898 in foo (stack_trace.zig)
        bar();
           ^
/home/andy/src/zig/doc/langref/stack_trace.zig:2:8: 0x11d5801 in main (stack_trace.zig)
    foo(12);
       ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Здесь трассировка стека не объясняет, как управление в bar дошло до вызова hello(). Чтобы это выяснить, пришлось бы открыть отладчик или дополнительно инструментировать приложение. Трассировка возврата ошибки, напротив, точно показывает, как ошибка распространялась вверх по стеку вызовов.

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

По умолчанию трассировки возврата ошибок включены в сборках Debug и отключены в сборках ReleaseFast, ReleaseSafe и ReleaseSmall.

Есть несколько способов активировать трассировку возврата ошибок:

  • Вернуть ошибку из main
  • Ошибка доходит до catch unreachable, а обработчик паники по умолчанию не переопределён
  • Использовать errorReturnTrace для доступа к текущей трассировке возврата. Чтобы вывести её, можно использовать std.debug.dumpStackTrace. При сборке без поддержки трассировки возврата ошибок эта функция возвращает значение null, известное на этапе comptime.

Подробности реализации

Для анализа затрат на производительность рассмотрим два случая:

  • ошибки не возвращаются
  • возвращаются ошибки

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

stack_trace_struct.zig
pub const StackTrace = struct {
    index: usize,
    instruction_addresses: [N]usize,
};

Здесь N — максимальная глубина вызовов функций, определённая анализом графа вызовов. Рекурсия не учитывается и считается равной 2.

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

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

При генерации кода для функции, возвращающей ошибку, непосредственно перед оператором return (только для операторов return, возвращающих ошибки) Zig генерирует вызов этой функции:

zig_return_error_fn.zig
// marked as "no-inline" in LLVM IR
fn __zig_return_error(stack_trace: *StackTrace) void {
    stack_trace.instruction_addresses[stack_trace.index] = @returnAddress();
    stack_trace.index = (stack_trace.index + 1) % N;
}

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

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

Опциональные типы

Одна из областей, в которой Zig обеспечивает безопасность без ущерба для эффективности и удобочитаемости, — это опциональный тип.

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

optional_integer.zig
// normal integer
const normal_int: i32 = 1234;

// optional integer
const optional_int: ?i32 = 5678;

Теперь переменная optional_int может быть целым числом i32 или значением null.

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

В Zig их нет.

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

Обычно отсутствие null считается недостатком, поскольку из-за него код получается более многословным. Но давайте сравним эквивалентный код на C и Zig.

Задача: вызвать malloc и вернуть null, если результат равен null.

Код на C

call_malloc_in_c.c
// malloc prototype included for reference
void *malloc(size_t size);

struct Foo *do_a_thing(void) {
    char *ptr = malloc(1234);
    if (!ptr) return NULL;
    // ...
}

Код на Zig

call_malloc_from_zig.zig
// malloc prototype included for reference
extern fn malloc(size: usize) ?[*]u8;

fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    _ = ptr; // ...
}

Здесь Zig как минимум не менее удобен, чем C, а возможно, и удобнее. Кроме того, тип «ptr» — это [*]u8, а не ?[*]u8. Ключевое слово orelse снимает опциональность типа, поэтому ptr гарантированно не равен null во всех местах его использования в функции.

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

checking_null_in_c.c
void do_a_thing(struct Foo *foo) {
    // do some stuff

    if (foo) {
        do_something_with_foo(foo);
    }

    // do some stuff
}

В Zig можно добиться того же результата:

checking_null_in_zig.zig
const Foo = struct {};
fn doSomethingWithFoo(foo: *Foo) void {
    _ = foo;
}

fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }

    // do some stuff
}

И снова важно отметить, что внутри блока if foo больше не является опциональным указателем — это указатель, который не может быть null.

Одно из преимуществ этого подхода состоит в том, что функции, принимающие указатели в качестве аргументов, можно аннотировать атрибутом «nonnull» — __attribute__((nonnull)) в GCC. Зная, что аргументы-указатели не могут быть null, оптимизатор иногда может принимать более удачные решения.

Опциональный тип

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

test_optional_type.zig
const expectEqual = @import("std").testing.expectEqual;

test "optional type" {
    // Declare an optional and coerce from null:
    var foo: ?i32 = null;

    // Coerce from child type of an optional
    foo = 1234;

    // Use compile-time reflection to access the child type of the optional:
    try comptime expectEqual(i32, @typeInfo(@TypeOf(foo)).optional.child);
}
Оболочка
$ zig test test_optional_type.zig
1/1 test_optional_type.test.optional type...OK
All 1 tests passed.

null

Как и undefined, null имеет собственный тип, и использовать его можно только после приведения к другому типу:

null.zig
const optional_value: ?i32 = null;

Опциональные указатели

Размер опционального указателя гарантированно совпадает с размером указателя. null опционального указателя гарантированно равен адресу 0.

test_optional_pointer.zig
const expectEqual = @import("std").testing.expectEqual;

test "optional pointers" {
    // Pointers cannot be null. If you want a null pointer, use the optional
    // prefix `?` to make the pointer type optional.
    var ptr: ?*i32 = null;

    var x: i32 = 1;
    ptr = &x;

    try expectEqual(1, ptr.?.*);

    // Optional pointers are the same size as normal pointers, because pointer
    // value 0 is used as the null value.
    try expectEqual(@sizeOf(?*i32), @sizeOf(*i32));
}
Оболочка
$ zig test test_optional_pointer.zig
1/1 test_optional_pointer.test.optional pointers...OK
All 1 tests passed.

См. также:

  • while с опциональными типами
  • if с опциональными типами

Приведение типов

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

Неявное приведение типов

Неявное приведение типов происходит, когда ожидается один тип, а предоставляется другой:

test_type_coercion.zig
test "type coercion - variable declaration" {
    const a: u8 = 1;
    const b: u16 = a;
    _ = b;
}

test "type coercion - function call" {
    const a: u8 = 1;
    foo(a);
}

fn foo(b: u16) void {
    _ = b;
}

test "type coercion - @as builtin" {
    const a: u8 = 1;
    const b = @as(u16, a);
    _ = b;
}
Оболочка
$ zig test test_type_coercion.zig
1/3 test_type_coercion.test.type coercion - variable declaration...OK
2/3 test_type_coercion.test.type coercion - function call...OK
3/3 test_type_coercion.test.type coercion - @as builtin...OK
All 3 tests passed.

Неявное приведение типов разрешено только тогда, когда способ преобразования из одного типа в другой полностью однозначен и гарантированно безопасен. Есть одно исключение — указатели C.

Неявное приведение типов: усиление квалификаторов

Значения с одинаковым представлением во время выполнения можно приводить, усиливая квалификаторы, независимо от глубины их вложенности:

  • const — допускается преобразование из non-const в const
  • volatile — допускается преобразование из non-volatile в volatile
  • align — допускается преобразование из большего выравнивания в меньшее
  • допускается преобразование множеств ошибок в их надмножества

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

test_no_op_casts.zig
test "type coercion - const qualification" {
    var a: i32 = 1;
    const b: *i32 = &a;
    foo(b);
}

fn foo(_: *const i32) void {}
Оболочка
$ zig test test_no_op_casts.zig
1/1 test_no_op_casts.test.type coercion - const qualification...OK
All 1 tests passed.

Кроме того, указатели неявно преобразуются в опциональные указатели на const:

test_pointer_coerce_const_optional.zig
const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;
const mem = std.mem;

test "cast *[1][*:0]const u8 to []const ?[*:0]const u8" {
    const window_name = [1][*:0]const u8{"window name"};
    const x: []const ?[*:0]const u8 = &window_name;
    try expectEqualStrings("window name", mem.span(x[0].?));
}
Оболочка
$ zig test test_pointer_coerce_const_optional.zig
1/1 test_pointer_coerce_const_optional.test.cast *[1][*:0]const u8 to []const ?[*:0]const u8...OK
All 1 tests passed.

Неявное приведение типов: расширение целых чисел и чисел с плавающей точкой

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

test_integer_widening.zig
const std = @import("std");
const builtin = @import("builtin");
const expectEqual = std.testing.expectEqual;
const mem = std.mem;

test "integer widening" {
    const a: u8 = 250;
    const b: u16 = a;
    const c: u32 = b;
    const d: u64 = c;
    const e: u64 = d;
    const f: u128 = e;
    try expectEqual(f, a);
}

test "implicit unsigned integer to signed integer" {
    const a: u8 = 250;
    const b: i16 = a;
    try expectEqual(250, b);
}

test "float widening" {
    const a: f16 = 12.34;
    const b: f32 = a;
    const c: f64 = b;
    const d: f128 = c;
    try expectEqual(d, a);
}
Оболочка
$ zig test test_integer_widening.zig
1/3 test_integer_widening.test.integer widening...OK
2/3 test_integer_widening.test.implicit unsigned integer to signed integer...OK
3/3 test_integer_widening.test.float widening...OK
All 3 tests passed.

Неявное приведение типов: целое число в число с плавающей точкой

Целые числа неявно преобразуются в числа с плавающей точкой, если любое возможное целое значение можно представить без округления (то есть точность целого числа не превышает точность мантиссы числа с плавающей точкой). Целочисленные типы большего размера, которые нельзя безопасно преобразовать неявно, нужно преобразовывать явно с помощью @floatFromInt.

Тип числа с плавающей точкой Наибольшие целочисленные типы
f16 i12 и u11
f32 i25 и u24
f64 i54 и u53
f80 i65 и u64
f128 i114 и u113
c_longdouble Зависит от целевой платформы
test_int_to_float_coercion.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "implicit integer to float" {
    var int: u8 = 123;
    _ = &int;
    const float: f32 = int;
    const int_from_float: u8 = @intFromFloat(float);
    try expectEqual(int, int_from_float);
}
Оболочка
$ zig test test_int_to_float_coercion.zig
1/1 test_int_to_float_coercion.test.implicit integer to float...OK
All 1 tests passed.
test_failed_int_to_float_coercion.zig
test "integer type is too large for implicit cast to float" {
    var int: u25 = 123;
    _ = &int;
    const float: f32 = int;
    _ = float;
}
Оболочка
$ zig test test_failed_int_to_float_coercion.zig
/home/andy/src/zig/doc/langref/test_failed_int_to_float_coercion.zig:4:24: error: expected type 'f32', found 'u25'
    const float: f32 = int;
                       ^~~

Неявное приведение типов: число с плавающей точкой в целое число

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

  • Преобразовать 54.0 в comptime_int, получив @as(comptime_int, 10), которое затем преобразуется в @as(f32, 10)
  • Преобразовать 5 в comptime_float, получив @as(comptime_float, 10.8), которое затем преобразуется в @as(f32, 10.8)
test_ambiguous_coercion.zig
// Compile time coercion of float to int
test "implicit cast to comptime_int" {
    const f: f32 = 54.0 / 5;
    _ = f;
}
Оболочка
$ zig test test_ambiguous_coercion.zig
/home/andy/src/zig/doc/langref/test_ambiguous_coercion.zig:3:25: error: ambiguous coercion of division operands 'comptime_float' and 'comptime_int'; non-zero remainder '4'
    const f: f32 = 54.0 / 5;
                   ~~~~~^~~

Неявное приведение типов: срезы, массивы и указатели

test_coerce_slices_arrays_and_pointers.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectEqualSlices = std.testing.expectEqualSlices;

// You can assign constant pointers to arrays to a slice with
// const modifier on the element type. Useful in particular for
// String literals.
test "*const [N]T to []const T" {
    const x1: []const u8 = "hello";
    const x2: []const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(x1, x2);

    const y: []const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, y[0]);
}

// Likewise, it works when the destination type is an error union.
test "*const [N]T to E![]const T" {
    const x1: anyerror![]const u8 = "hello";
    const x2: anyerror![]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(try x1, try x2);

    const y: anyerror![]const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, (try y)[0]);
}

// Likewise, it works when the destination type is an optional.
test "*const [N]T to ?[]const T" {
    const x1: ?[]const u8 = "hello";
    const x2: ?[]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(x1.?, x2.?);

    const y: ?[]const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, y.?[0]);
}

// In this cast, the array length becomes the slice length.
test "*[N]T to []T" {
    var buf: [5]u8 = "hello".*;
    const x: []u8 = &buf;
    try expectEqualStrings("hello", x);

    const buf2 = [2]f32{ 1.2, 3.4 };
    const x2: []const f32 = &buf2;
    try expectEqualSlices(f32, &[2]f32{ 1.2, 3.4 }, x2);
}

// Single-item pointers to arrays can be coerced to many-item pointers.
test "*[N]T to [*]T" {
    var buf: [5]u8 = "hello".*;
    const x: [*]u8 = &buf;
    try expectEqual('o', x[4]);
    // x[5] would be an uncaught out of bounds pointer dereference!
}

// Likewise, it works when the destination type is an optional.
test "*[N]T to ?[*]T" {
    var buf: [5]u8 = "hello".*;
    const x: ?[*]u8 = &buf;
    try expectEqual('o', x.?[4]);
}

// Single-item pointers can be cast to len-1 single-item arrays.
test "*T to *[1]T" {
    var x: i32 = 1234;
    const y: *[1]i32 = &x;
    const z: [*]i32 = y;
    try expectEqual(1234, z[0]);
}

// Sentinel-terminated slices can be coerced into sentinel-terminated pointers
test "[:x]T to [*:x]T" {
    const buf: [:0]const u8 = "hello";
    const buf2: [*:0]const u8 = buf;
    try expectEqual('o', buf2[4]);
}
Оболочка
$ zig test test_coerce_slices_arrays_and_pointers.zig
1/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to []const T...OK
2/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to E![]const T...OK
3/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to ?[]const T...OK
4/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to []T...OK
5/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to [*]T...OK
6/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to ?[*]T...OK
7/8 test_coerce_slices_arrays_and_pointers.test.*T to *[1]T...OK
8/8 test_coerce_slices_arrays_and_pointers.test.[:x]T to [*:x]T...OK
All 8 tests passed.

См. также:

  • Указатели C

Неявное приведение типов: опциональные типы

Тип полезной нагрузки опциональных типов, а также значение null, неявно преобразуются в опциональный тип.

test_coerce_optionals.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coerce to optionals" {
    const x: ?i32 = 1234;
    const y: ?i32 = null;

    try expectEqual(1234, x.?);
    try expectEqual(null, y);
}
Оболочка
$ zig test test_coerce_optionals.zig
1/1 test_coerce_optionals.test.coerce to optionals...OK
All 1 tests passed.

Опциональные типы также работают внутри объединения ошибки с типом:

test_coerce_optional_wrapped_error_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coerce to optionals wrapped in error union" {
    const x: anyerror!?i32 = 1234;
    const y: anyerror!?i32 = null;

    try expectEqual(1234, (try x).?);
    try expectEqual(null, (try y));
}
Оболочка
$ zig test test_coerce_optional_wrapped_error_union.zig
1/1 test_coerce_optional_wrapped_error_union.test.coerce to optionals wrapped in error union...OK
All 1 tests passed.

Неявное приведение типов: объединения ошибок

Тип полезной нагрузки объединения ошибки с типом, а также тип множества ошибок неявно преобразуются в тип объединения ошибки с типом:

test_coerce_to_error_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coercion to error unions" {
    const x: anyerror!i32 = 1234;
    const y: anyerror!i32 = error.Failure;

    try expectEqual(1234, (try x));
    try std.testing.expectError(error.Failure, y);
}
Оболочка
$ zig test test_coerce_to_error_union.zig
1/1 test_coerce_to_error_union.test.coercion to error unions...OK
All 1 tests passed.

Неявное приведение типов: числа, известные во время компиляции

Если во время компиляции известно, что число представимо в целевом типе, его можно преобразовать неявно:

test_coerce_large_to_small.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coercing large integer type to smaller one when value is comptime-known to fit" {
    const x: u64 = 255;
    const y: u8 = x;
    try expectEqual(255, y);
}
Оболочка
$ zig test test_coerce_large_to_small.zig
1/1 test_coerce_large_to_small.test.coercing large integer type to smaller one when value is comptime-known to fit...OK
All 1 tests passed.

Неявное приведение типов: объединения и перечисления

Помеченные объединения можно преобразовывать в перечисления, а перечисления — в помеченные объединения, если во время компиляции известно, что значение соответствует полю объединения, имеющему только одно возможное значение, например void:

test_coerce_unions_enums.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const E = enum {
    one,
    two,
    three,
};

const U = union(E) {
    one: i32,
    two: f32,
    three,
};

const U2 = union(enum) {
    a: void,
    b: f32,

    fn tag(self: U2) usize {
        switch (self) {
            .a => return 1,
            .b => return 2,
        }
    }
};

test "coercion between unions and enums" {
    const u = U{ .two = 12.34 };
    const e: E = u; // coerce union to enum
    try expectEqual(E.two, e);

    const three = E.three;
    const u_2: U = three; // coerce enum to union
    try expectEqual(E.three, u_2);

    const u_3: U = .three; // coerce enum literal to union
    try expectEqual(E.three, u_3);

    const u_4: U2 = .a; // coerce enum literal to union with inferred enum tag type.
    try expectEqual(1, u_4.tag());

    // The following example is invalid.
    // error: coercion from enum '@EnumLiteral()' to union 'test_coerce_unions_enum.U2' must initialize 'f32' field 'b'
    //var u_5: U2 = .b;
    //try expectEqual(2, u_5.tag());
}
Оболочка
$ zig test test_coerce_unions_enums.zig
1/1 test_coerce_unions_enums.test.coercion between unions and enums...OK
All 1 tests passed.

См. также:

  • union
  • enum

Неявное приведение типов: undefined

undefined можно неявно преобразовать к любому типу.

Неявное приведение типов: кортежи в массивы

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

test_coerce_tuples_arrays.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Tuple = struct { u8, u8 };
test "coercion from homogeneous tuple to array" {
    const tuple: Tuple = .{ 5, 6 };
    const array: [2]u8 = tuple;
    _ = array;
}
Оболочка
$ zig test test_coerce_tuples_arrays.zig
1/1 test_coerce_tuples_arrays.test.coercion from homogeneous tuple to array...OK
All 1 tests passed.

Явное приведение типов

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

  • @bitCast — изменить тип, сохранив битовое представление
  • @alignCast — увеличить выравнивание указателя
  • @enumFromInt — получить значение перечисления по целочисленному значению его тега
  • @errorFromInt — получить код ошибки по его целочисленному значению
  • @errorCast — преобразовать к меньшему множеству ошибок
  • @floatCast — преобразовать число с плавающей точкой большего разряда в число меньшего разряда
  • @floatFromInt — преобразовать целое число в число с плавающей точкой
  • @intCast — преобразовать один целочисленный тип в другой
  • @intFromBool — преобразовать true в 1, а false в 0
  • @intFromEnum — получить целочисленное значение тега перечисления или помеченного объединения
  • @intFromError — получить целочисленное значение кода ошибки
  • @intFromFloat — получить целую часть числа с плавающей точкой
  • @intFromPtr — получить адрес указателя
  • @ptrFromInt — преобразовать адрес в указатель
  • @ptrCast — преобразовать один тип указателя в другой
  • @truncate — преобразовать один целочисленный тип в другой, отбросив биты

Определение общего типа

Определение общего типа происходит в следующих случаях:

  • выражения switch
  • выражения if
  • выражения while
  • выражения for
  • Несколько операторов break в блоке
  • Некоторые бинарные операции

При таком определении типа выбирается тип, в который можно преобразовать все совместимые типы. Вот несколько примеров:

test_peer_type_resolution.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;

test "peer resolve int widening" {
    const a: i8 = 12;
    const b: i16 = 34;
    const c = a + b;
    try expectEqual(46, c);
    try expectEqual(i16, @TypeOf(c));
}

test "peer resolve small int and float" {
    // This only works for integer types that can coerce to the float type.
    // Larger integer types will cause a compiler error; no float widening occurs.
    var i: u8 = 12;
    var f: f32 = 34;
    _ = .{ &i, &f };
    const x = i + f;
    try expectEqual(x, 46.0);
    try expectEqual(@TypeOf(x), f32);
}

test "peer resolve arrays of different size to const slice" {
    try expectEqualStrings("true", boolToStr(true));
    try expectEqualStrings("false", boolToStr(false));
    try comptime expectEqualStrings("true", boolToStr(true));
    try comptime expectEqualStrings("false", boolToStr(false));
}
fn boolToStr(b: bool) []const u8 {
    return if (b) "true" else "false";
}

test "peer resolve array and const slice" {
    try testPeerResolveArrayConstSlice(true);
    try comptime testPeerResolveArrayConstSlice(true);
}
fn testPeerResolveArrayConstSlice(b: bool) !void {
    const value1 = if (b) "aoeu" else @as([]const u8, "zz");
    const value2 = if (b) @as([]const u8, "zz") else "aoeu";
    try expectEqualStrings("aoeu", value1);
    try expectEqualStrings("zz", value2);
}

test "peer type resolution: ?T and T" {
    try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
    try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
    comptime {
        try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
        try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
    }
}
fn peerTypeTAndOptionalT(c: bool, b: bool) ?usize {
    if (c) {
        return if (b) null else @as(usize, 0);
    }

    return @as(usize, 3);
}

test "peer type resolution: *[0]u8 and []const u8" {
    try expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
    try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
    comptime {
        try expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
        try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
    }
}
fn peerTypeEmptyArrayAndSlice(a: bool, slice: []const u8) []const u8 {
    if (a) {
        return &[_]u8{};
    }

    return slice[0..1];
}
test "peer type resolution: *[0]u8, []const u8, and anyerror![]u8" {
    {
        var data = "hi".*;
        const slice = data[0..];
        try expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
        try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
    }
    comptime {
        var data = "hi".*;
        const slice = data[0..];
        try expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
        try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
    }
}
fn peerTypeEmptyArrayAndSliceAndError(a: bool, slice: []u8) anyerror![]u8 {
    if (a) {
        return &[_]u8{};
    }

    return slice[0..1];
}

test "peer type resolution: *const T and ?*T" {
    const a: *const usize = @ptrFromInt(0x123456780);
    const b: ?*usize = @ptrFromInt(0x123456780);
    try expectEqual(a, b);
    try expectEqual(b, a);
}

test "peer type resolution: error union switch" {
    // The non-error and error cases are only peers if the error case is just a switch expression;
    // the pattern `if (x) {...} else |err| blk: { switch (err) {...} }` does not consider the
    // non-error and error case to be peers.
    var a: error{ A, B, C }!u32 = 0;
    _ = &a;
    const b = if (a) |x|
        x + 3
    else |err| switch (err) {
        error.A => 0,
        error.B => 1,
        error.C => null,
    };
    try expectEqual(?u32, @TypeOf(b));

    // The non-error and error cases are only peers if the error case is just a switch expression;
    // the pattern `x catch |err| blk: { switch (err) {...} }` does not consider the unwrapped `x`
    // and error case to be peers.
    const c = a catch |err| switch (err) {
        error.A => 0,
        error.B => 1,
        error.C => null,
    };
    try expectEqual(?u32, @TypeOf(c));
}
Оболочка
$ zig test test_peer_type_resolution.zig
1/9 test_peer_type_resolution.test.peer resolve int widening...OK
2/9 test_peer_type_resolution.test.peer resolve small int and float...OK
3/9 test_peer_type_resolution.test.peer resolve arrays of different size to const slice...OK
4/9 test_peer_type_resolution.test.peer resolve array and const slice...OK
5/9 test_peer_type_resolution.test.peer type resolution: ?T and T...OK
6/9 test_peer_type_resolution.test.peer type resolution: *[0]u8 and []const u8...OK
7/9 test_peer_type_resolution.test.peer type resolution: *[0]u8, []const u8, and anyerror![]u8...OK
8/9 test_peer_type_resolution.test.peer type resolution: *const T and ?*T...OK
9/9 test_peer_type_resolution.test.peer type resolution: error union switch...OK
All 9 tests passed.

Типы нулевого размера

Для некоторых типов значение @sizeOf равно 0:

  • void
  • Целые числа u0 и i0.
  • Массивы и векторы длины 0 или с элементами типа нулевого размера.
  • Перечисление только с одним тегом.
  • Структура, все поля которой имеют тип нулевого размера.
  • Объединение только с одним полем, имеющим тип нулевого размера.

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

zero_bit_types.zig
export fn entry() void {
    var x: void = {};
    var y: void = {};
    x = y;
    y = x;
}

При преобразовании в машинный код тело entry не содержит сгенерированного кода даже в режиме отладки. Например, для x86_64:

0000000000000010 <entry>:
  10:	55                   	push   %rbp
  11:	48 89 e5             	mov    %rsp,%rbp
  14:	5d                   	pop    %rbp
  15:	c3                   	retq   

Эти инструкции ассемблера не содержат кода, связанного со значениями void, — они выполняют только пролог и эпилог вызова функции.

void

void может быть полезен при создании экземпляров обобщённых типов. Например, для Map(Key, Value) можно передать void в качестве типа Value, чтобы получить Set:

test_void_in_hashmap.zig
const std = @import("std");
const expect = std.testing.expect;

test "turn HashMap into a set with void" {
    var map = std.AutoHashMap(i32, void).init(std.testing.allocator);
    defer map.deinit();

    try map.put(1, {});
    try map.put(2, {});

    try expect(map.contains(2));
    try expect(!map.contains(3));

    _ = map.remove(2);
    try expect(!map.contains(2));
}
Оболочка
$ zig test test_void_in_hashmap.zig
1/1 test_void_in_hashmap.test.turn HashMap into a set with void...OK
All 1 tests passed.

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

void отличается от anyopaque. Размер void равен 0 байт, а размер anyopaque неизвестен, но не равен нулю.

Игнорировать можно только значения выражений типа void. Например, игнорирование выражения, не имеющего тип void, приводит к ошибке компиляции:

test_expression_ignored.zig
test "ignoring expression value" {
    foo();
}

fn foo() i32 {
    return 1234;
}
Оболочка
$ zig test test_expression_ignored.zig
/home/andy/src/zig/doc/langref/test_expression_ignored.zig:2:8: error: value of type 'i32' ignored
    foo();
    ~~~^~
/home/andy/src/zig/doc/langref/test_expression_ignored.zig:2:8: note: all non-void values must be used
/home/andy/src/zig/doc/langref/test_expression_ignored.zig:2:8: note: to discard the value, assign it to '_'

Однако, если выражение имеет тип void, ошибки не будет. Результаты выражений можно явно игнорировать, присваивая их _.

test_void_ignored.zig
test "void is ignored" {
    returnsVoid();
}

test "explicitly ignoring expression value" {
    _ = foo();
}

fn returnsVoid() void {}

fn foo() i32 {
    return 1234;
}
Оболочка
$ zig test test_void_ignored.zig
1/2 test_void_ignored.test.void is ignored...OK
2/2 test_void_ignored.test.explicitly ignoring expression value...OK
All 2 tests passed.

Семантика расположения результата

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

Для наглядности рассмотрим оператор const x: u32 = 42;. Аннотация типа задаёт тип результата u32 для выражения инициализации 42, указывая компилятору преобразовать это целое число (изначально имеющее тип comptime_int) к заданному типу. Далее мы рассмотрим другие примеры.

Это не деталь реализации: описанная выше логика закреплена в спецификации языка Zig и является основным механизмом вывода типов в языке. В совокупности эта система называется «семантикой расположения результата».

Типы результата

Если возможно, типы результата рекурсивно распространяются по выражениям. Например, если выражению &e задан тип результата *u32, то выражению e также задаётся тип результата u32, что позволяет языку выполнить это преобразование до получения ссылки.

Механизм типов результата используется встроенными функциями приведения типов, такими как @intCast. Вместо того чтобы принимать в качестве аргумента тип, к которому нужно привести значение, эти встроенные функции определяют его по типу результата. Часто тип результата известен из контекста; если это не так, можно явно задать его с помощью встроенной функции @as.

Типы результата для компонентов простого выражения можно разложить следующим образом:

result_type_propagation.zig
const expectEqual = @import("std").testing.expectEqual;
test "result type propagates through struct initializer" {
    const S = struct { x: u32 };
    const val: u64 = 123;
    const s: S = .{ .x = @intCast(val) };
    // .{ .x = @intCast(val) }   has result type `S` due to the type annotation
    //         @intCast(val)     has result type `u32` due to the type of the field `S.x`
    //                  val      has no result type, as it is permitted to be any integer type
    try expectEqual(@as(u32, 123), s.x);
}
Оболочка
$ zig test result_type_propagation.zig
1/1 result_type_propagation.test.result type propagates through struct initializer...OK
All 1 tests passed.

Эта информация о типе результата полезна для упомянутых встроенных функций приведения типов, а также позволяет избежать создания значений до преобразования и в некоторых случаях избавляет от необходимости явно приводить типы. В таблице ниже показано, как некоторые распространённые выражения распространяют типы результата; x и y обозначают произвольные подвыражения.

Выражение Тип результата родительского выражения Тип результата подвыражения
const val: T = x - x имеет тип T
var val: T = x - x имеет тип T
val = x - x имеет тип @TypeOf(val)
@as(T, x) - x имеет тип T
&x *T x имеет тип T
&x []T x — это некоторый массив типа T
f(x) - x имеет тип первого параметра f
.{x} T x имеет тип @FieldType(T, "0")
.{ .a = x } T x имеет тип @FieldType(T, "a")
T{x} - x имеет тип @FieldType(T, "0")
T{ .a = x } - x имеет тип @FieldType(T, "a")
@Int(x, y) - x имеет тип std.builtin.Signedness, а y имеет тип u16
@typeInfo(x) - x имеет тип type
x << y - y имеет тип std.math.Log2IntCeil(@TypeOf(x))

Расположения результата

Помимо информации о типе результата, каждому выражению может быть назначено расположение результата — указатель, по которому значение должно быть записано непосредственно. Эта система позволяет избежать промежуточного копирования при инициализации структур данных, что может быть важно для типов, которым необходимо иметь фиксированный адрес в памяти («закреплённых» типов).

При компиляции простого выражения присваивания x = e многие языки создают временное значение e в стеке, а затем присваивают его x, возможно, попутно выполняя преобразование типа. Zig действует иначе. Выражению e задаётся тип результата, совпадающий с типом x, и расположение результата &x. Для многих синтаксических форм e это не имеет практического значения. Однако при работе с более сложными синтаксическими конструкциями это может иметь важные семантические последствия.

Например, если выражению .{ .a = x, .b = y } задано расположение результата ptr, то выражению x задаётся расположение результата &ptr.a, а выражению y — расположение результата &ptr.b. Без этой системы выражение создало бы временное значение структуры целиком в стеке, а затем скопировало бы его по целевому адресу. По сути, Zig преобразует присваивание foo = .{ .a = x, .b = y } в два оператора: foo.a = x; foo.b = y;.

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

result_location_interfering_with_swap.zig
const expectEqual = @import("std").testing.expectEqual;
test "attempt to swap array elements with array initializer" {
    var arr: [2]u32 = .{ 1, 2 };
    arr = .{ arr[1], arr[0] };
    // The previous line is equivalent to the following two lines:
    //   arr[0] = arr[1];
    //   arr[1] = arr[0];
    // So this fails!
    try expectEqual(2, arr[0]); // succeeds
    try expectEqual(1, arr[1]); // fails
}
Оболочка
$ zig test result_location_interfering_with_swap.zig
1/1 result_location_interfering_with_swap.test.attempt to swap array elements with array initializer...expected 1, found 2
FAIL (TestExpectedEqual)
/home/andy/src/zig/lib/std/testing.zig:118:17: 0x1234e54 in expectEqualInner__anon_42791 (std.zig)
                return error.TestExpectedEqual;
                ^
/home/andy/src/zig/doc/langref/result_location_interfering_with_swap.zig:10:5: 0x1234fc5 in test.attempt to swap array elements with array initializer (result_location_interfering_with_swap.zig)
    try expectEqual(1, arr[1]); // fails
    ^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/2726b856c754d9fa245ea804407f8c1d/test --seed=0x22805754

В таблице ниже показано, как некоторые распространённые выражения распространяют расположения результата; x и y обозначают произвольные подвыражения. Обратите внимание, что некоторые выражения не могут задавать осмысленные расположения результата для подвыражений, даже если у самих выражений есть расположение результата.

Выражение Расположение результата Расположения результата подвыражений
const val: T = x - x имеет расположение результата &val
var val: T = x - x имеет расположение результата &val
val = x - x имеет расположение результата &val
@as(T, x) ptr x не имеет расположения результата
&x ptr x не имеет расположения результата
f(x) ptr x не имеет расположения результата
.{x} ptr x имеет расположение результата &ptr[0]
.{ .a = x } ptr x имеет расположение результата &ptr.a
T{x} ptr x не имеет расположения результата (типизированные инициализаторы не распространяют расположения результата)
T{ .a = x } ptr x не имеет расположения результата (типизированные инициализаторы не распространяют расположения результата)
@Int(x, y) - x и y не имеют расположений результата
@typeInfo(x) ptr x не имеет расположения результата
x << y ptr x и y не имеют расположений результата

comptime

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

Знакомство с понятием времени компиляции

Параметры времени компиляции

Параметры времени компиляции — способ реализации обобщённых типов в Zig. Это утиная типизация во время компиляции.

compile-time_duck_typing.zig
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
fn gimmeTheBiggerFloat(a: f32, b: f32) f32 {
    return max(f32, a, b);
}
fn gimmeTheBiggerInteger(a: u64, b: u64) u64 {
    return max(u64, a, b);
}

В Zig типы являются полноправными сущностями. Их можно присваивать переменным, передавать в качестве параметров функциям и возвращать из функций. Однако использовать их можно только в выражениях, значения которых известны во время компиляции, поэтому параметр T в приведённом выше фрагменте необходимо пометить как comptime.

Параметр comptime означает, что:

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

Например, если добавить к приведённому выше фрагменту ещё одну функцию:

test_unresolved_comptime_value.zig
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
test "try to pass a runtime type" {
    foo(false);
}
fn foo(condition: bool) void {
    const result = max(if (condition) f32 else u64, 1234, 5678);
    _ = result;
}
Оболочка
$ zig test test_unresolved_comptime_value.zig
/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:8:28: error: unable to resolve comptime value
    const result = max(if (condition) f32 else u64, 1234, 5678);
                           ^~~~~~~~~
/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:8:24: note: argument to comptime parameter must be comptime-known
    const result = max(if (condition) f32 else u64, 1234, 5678);
                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:1:8: note: parameter declared comptime here
fn max(comptime T: type, a: T, b: T) T {
       ^~~~~~~~
referenced by:
    test.try to pass a runtime type: /home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:5:8

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

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

Например:

test_comptime_mismatched_type.zig
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
test "try to compare bools" {
    _ = max(bool, true, false);
}
Оболочка
$ zig test test_comptime_mismatched_type.zig
/home/andy/src/zig/doc/langref/test_comptime_mismatched_type.zig:2:18: error: operator > not allowed for type 'bool'
    return if (a > b) a else b;
               ~~^~~
referenced by:
    test.try to compare bools: /home/andy/src/zig/doc/langref/test_comptime_mismatched_type.zig:5:12

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

test_comptime_max_with_bool.zig
fn max(comptime T: type, a: T, b: T) T {
    if (T == bool) {
        return a or b;
    } else if (a > b) {
        return a;
    } else {
        return b;
    }
}
test "try to compare bools" {
    try @import("std").testing.expectEqual(true, max(bool, false, true));
}
Оболочка
$ zig test test_comptime_max_with_bool.zig
1/1 test_comptime_max_with_bool.test.try to compare bools...OK
All 1 tests passed.

Это работает, потому что Zig неявно встраивает выражения if, когда условие известно на этапе компиляции, а компилятор гарантирует, что анализ ветви, которая не будет выполнена, будет пропущен.

Это означает, что фактически сгенерированная в такой ситуации функция max выглядит так:

compiler_generated_function.zig
fn max(a: bool, b: bool) bool {
    {
        return a or b;
    }
}

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

То же самое работает и для выражений switch — они неявно встраиваются, когда целевое выражение известно на этапе компиляции.

Переменные времени компиляции

В Zig программист может пометить переменные как comptime. Это гарантирует компилятору, что каждая загрузка и запись переменной выполняется на этапе компиляции. Любое нарушение этого условия приводит к ошибке компиляции.

В сочетании с тем, что мы можем использовать циклы inline, это позволяет написать функцию, которая частично вычисляется на этапе компиляции, а частично — во время выполнения.

Например:

test_comptime_evaluation.zig
const expectEqual = @import("std").testing.expectEqual;

const CmdFn = struct {
    name: []const u8,
    func: fn (i32) i32,
};

const cmd_fns = [_]CmdFn{
    CmdFn{ .name = "one", .func = one },
    CmdFn{ .name = "two", .func = two },
    CmdFn{ .name = "three", .func = three },
};
fn one(value: i32) i32 {
    return value + 1;
}
fn two(value: i32) i32 {
    return value + 2;
}
fn three(value: i32) i32 {
    return value + 3;
}

fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
    var result: i32 = start_value;
    comptime var i = 0;
    inline while (i < cmd_fns.len) : (i += 1) {
        if (cmd_fns[i].name[0] == prefix_char) {
            result = cmd_fns[i].func(result);
        }
    }
    return result;
}

test "perform fn" {
    try expectEqual(6, performFn('t', 1));
    try expectEqual(1, performFn('o', 0));
    try expectEqual(99, performFn('w', 99));
}
Оболочка
$ zig test test_comptime_evaluation.zig
1/1 test_comptime_evaluation.test.perform fn...OK
All 1 tests passed.

Этот пример несколько надуманный, поскольку вычисление на этапе компиляции здесь не нужно: код прекрасно работал бы, если бы всё выполнялось во время выполнения. Однако в результате генерируется другой код. В этом примере функция performFn генерируется трижды для разных значений prefix_char:

performFn_1
// From the line:
// expect(performFn('t', 1) == 6);
fn performFn(start_value: i32) i32 {
    var result: i32 = start_value;
    result = two(result);
    result = three(result);
    return result;
}
performFn_2
// From the line:
// expect(performFn('o', 0) == 1);
fn performFn(start_value: i32) i32 {
    var result: i32 = start_value;
    result = one(result);
    return result;
}
performFn_3
// From the line:
// expect(performFn('w', 99) == 99);
fn performFn(start_value: i32) i32 {
    var result: i32 = start_value;
    _ = &result;
    return result;
}

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

Выражения времени компиляции

В Zig важно, известно ли данное выражение на этапе компиляции или только во время выполнения. Программист может использовать выражение comptime, чтобы гарантировать вычисление выражения на этапе компиляции. Если этого сделать не удастся, компилятор выдаст ошибку. Например:

test_comptime_call_extern_function.zig
extern fn exit() noreturn;

test "foo" {
    comptime {
        exit();
    }
}
Оболочка
$ zig test test_comptime_call_extern_function.zig
/home/andy/src/zig/doc/langref/test_comptime_call_extern_function.zig:5:13: error: comptime call of extern function
        exit();
        ~~~~^~
/home/andy/src/zig/doc/langref/test_comptime_call_extern_function.zig:4:5: note: 'comptime' keyword forces comptime evaluation
    comptime {
    ^~~~~~~~

Нет смысла в том, чтобы программа могла вызывать exit() (или любую другую внешнюю функцию) на этапе компиляции, поэтому это приводит к ошибке компиляции. Однако выражение comptime делает гораздо больше, чем просто иногда вызывает ошибку компиляции.

Внутри выражения comptime:

  • Все переменные являются переменными comptime.
  • Все выражения if, while, for и switch вычисляются на этапе компиляции либо вызывают ошибку компиляции, если это невозможно.
  • Все выражения return и try недопустимы (если только сама функция не вызывается на этапе компиляции).
  • Весь код с побочными эффектами во время выполнения или зависящий от значений времени выполнения вызывает ошибку компиляции.
  • При всех вызовах функций компилятор интерпретирует функцию на этапе компиляции и выдаёт ошибку компиляции, если функция пытается выполнить действие с глобальными побочными эффектами во время выполнения.

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

Рассмотрим пример:

test_fibonacci_recursion.zig
const expectEqual = @import("std").testing.expectEqual;

fn fibonacci(index: u32) u32 {
    if (index < 2) return index;
    return fibonacci(index - 1) + fibonacci(index - 2);
}

test "fibonacci" {
    // test fibonacci at run-time
    try expectEqual(13, fibonacci(7));

    // test fibonacci at compile-time
    try comptime expectEqual(13, fibonacci(7));
}
Оболочка
$ zig test test_fibonacci_recursion.zig
1/1 test_fibonacci_recursion.test.fibonacci...OK
All 1 tests passed.

Представьте, что мы забыли базовый случай рекурсивной функции и попытались запустить тесты:

test_fibonacci_comptime_overflow.zig
const expectEqual = @import("std").testing.expectEqual;

fn fibonacci(index: u32) u32 {
    //if (index < 2) return index;
    return fibonacci(index - 1) + fibonacci(index - 2);
}

test "fibonacci" {
    try comptime expectEqual(13, fibonacci(7));
}
Оболочка
$ zig test test_fibonacci_comptime_overflow.zig
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:28: error: overflow of integer type 'u32' with value '-1'
    return fibonacci(index - 1) + fibonacci(index - 2);
                     ~~~~~~^~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:21: note: called at comptime here (7 times)
    return fibonacci(index - 1) + fibonacci(index - 2);
           ~~~~~~~~~^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:9:43: note: called at comptime here
    try comptime expectEqual(13, fibonacci(7));
                                 ~~~~~~~~~^~~

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

К счастью, мы использовали беззнаковое целое число, поэтому при попытке вычесть 1 из 0 возникло недопустимое поведение, которое всегда приводит к ошибке компиляции, если компилятор знает, что оно произошло. Но что было бы, если бы мы использовали знаковое целое число?

fibonacci_comptime_infinite_recursion.zig
const assert = @import("std").debug.assert;

fn fibonacci(index: i32) i32 {
    //if (index < 2) return index;
    return fibonacci(index - 1) + fibonacci(index - 2);
}

test "fibonacci" {
    try comptime assert(fibonacci(7) == 13);
}

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

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

Что произойдёт, если мы исправим базовый случай, но укажем неверное значение в строке expect?

test_fibonacci_comptime_unreachable.zig
const assert = @import("std").debug.assert;

fn fibonacci(index: i32) i32 {
    if (index < 2) return index;
    return fibonacci(index - 1) + fibonacci(index - 2);
}

test "fibonacci" {
    try comptime assert(fibonacci(7) == 99999);
}
Оболочка
$ zig test test_fibonacci_comptime_unreachable.zig
/home/andy/src/zig/lib/std/debug.zig:421:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_unreachable.zig:9:24: note: called at comptime here
    try comptime assert(fibonacci(7) == 99999);
                 ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~

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

test_container-level_comptime_expressions.zig
const first_25_primes = firstNPrimes(25);
const sum_of_first_25_primes = sum(&first_25_primes);

fn firstNPrimes(comptime n: usize) [n]i32 {
    var prime_list: [n]i32 = undefined;
    var next_index: usize = 0;
    var test_number: i32 = 2;
    while (next_index < prime_list.len) : (test_number += 1) {
        var test_prime_index: usize = 0;
        var is_prime = true;
        while (test_prime_index < next_index) : (test_prime_index += 1) {
            if (test_number % prime_list[test_prime_index] == 0) {
                is_prime = false;
                break;
            }
        }
        if (is_prime) {
            prime_list[next_index] = test_number;
            next_index += 1;
        }
    }
    return prime_list;
}

fn sum(numbers: []const i32) i32 {
    var result: i32 = 0;
    for (numbers) |x| {
        result += x;
    }
    return result;
}

test "variable values" {
    try @import("std").testing.expectEqual(1060, sum_of_first_25_primes);
}
Оболочка
$ zig test test_container-level_comptime_expressions.zig
1/1 test_container-level_comptime_expressions.test.variable values...OK
All 1 tests passed.

При компиляции этой программы Zig генерирует константы с уже вычисленными ответами. Вот строки из сгенерированного LLVM IR:

@0 = internal unnamed_addr constant [25 x i32] [i32 2, i32 3, i32 5, i32 7, i32 11, i32 13, i32 17, i32 19, i32 23, i32 29, i32 31, i32 37, i32 41, i32 43, i32 47, i32 53, i32 59, i32 61, i32 67, i32 71, i32 73, i32 79, i32 83, i32 89, i32 97]
@1 = internal unnamed_addr constant i32 1060

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

Обобщённые структуры данных

Zig использует возможности comptime для реализации обобщённых структур данных без введения специального синтаксиса.

Вот пример обобщённой структуры данных List.

generic_data_structure.zig
fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

// The generic List data structure can be instantiated by passing in a type:
var buffer: [10]i32 = undefined;
var list = List(i32){
    .items = &buffer,
    .len = 0,
};

Вот и всё. Это функция, возвращающая анонимную структуру struct. Для сообщений об ошибках и отладки Zig выводит имя "List(i32)" из имени функции и параметров, переданных при создании анонимной структуры.

Чтобы явно задать типу имя, присвойте его константе.

anonymous_struct_name.zig
const Node = struct {
    next: ?*Node,
    name: []const u8,
};

var node_a = Node{
    .next = null,
    .name = "Node A",
};

var node_b = Node{
    .next = &node_a,
    .name = "Node B",
};

В этом примере структура Node ссылается сама на себя. Это работает, потому что объявления верхнего уровня не зависят от порядка. Если компилятор может определить размер структуры, она может ссылаться сама на себя. В данном случае Node ссылается сама на себя через указатель, размер которого определён на этапе компиляции, поэтому всё работает.

Разбор примера: print в Zig

Объединим всё это и посмотрим, как работает print в Zig.

print.zig
const print = @import("std").debug.print;

const a_number: i32 = 1234;
const a_string = "foobar";

pub fn main() void {
    print("here is a string: '{s}' here is a number: {}\n", .{ a_string, a_number });
}
Оболочка
$ zig build-exe print.zig
$ ./print
here is a string: 'foobar' here is a number: 1234

Давайте изучим реализацию и посмотрим, как она работает:

poc_print_fn.zig
const Writer = struct {
    /// Calls print and then flushes the buffer.
    pub fn print(self: *Writer, comptime format: []const u8, args: anytype) anyerror!void {
        const State = enum {
            start,
            open_brace,
            close_brace,
        };

        comptime var start_index: usize = 0;
        comptime var state = State.start;
        comptime var next_arg: usize = 0;

        inline for (format, 0..) |c, i| {
            switch (state) {
                State.start => switch (c) {
                    '{' => {
                        if (start_index < i) try self.write(format[start_index..i]);
                        state = State.open_brace;
                    },
                    '}' => {
                        if (start_index < i) try self.write(format[start_index..i]);
                        state = State.close_brace;
                    },
                    else => {},
                },
                State.open_brace => switch (c) {
                    '{' => {
                        state = State.start;
                        start_index = i;
                    },
                    '}' => {
                        try self.printValue(args[next_arg]);
                        next_arg += 1;
                        state = State.start;
                        start_index = i + 1;
                    },
                    's' => {
                        continue;
                    },
                    else => @compileError("Unknown format character: " ++ [1]u8{c}),
                },
                State.close_brace => switch (c) {
                    '}' => {
                        state = State.start;
                        start_index = i;
                    },
                    else => @compileError("Single '}' encountered in format string"),
                },
            }
        }
        comptime {
            if (args.len != next_arg) {
                @compileError("Unused arguments");
            }
            if (state != State.start) {
                @compileError("Incomplete format string: " ++ format);
            }
        }
        if (start_index < format.len) {
            try self.write(format[start_index..format.len]);
        }
        try self.flush();
    }

    fn write(self: *Writer, value: []const u8) !void {
        _ = self;
        _ = value;
    }
    pub fn printValue(self: *Writer, value: anytype) !void {
        _ = self;
        _ = value;
    }
    fn flush(self: *Writer) !void {
        _ = self;
    }
};

Это пробная реализация; фактическая функция в стандартной библиотеке поддерживает больше возможностей форматирования.

Обратите внимание, что эта функция не встроена в компилятор Zig: это пользовательский код из стандартной библиотеки.

При анализе этой функции на основе приведённого выше примера Zig частично вычисляет её и генерирует функцию, которая фактически выглядит так:

Сгенерированная функция print
pub fn print(self: *Writer, arg0: []const u8, arg1: i32) !void {
    try self.write("here is a string: '");
    try self.printValue(arg0);
    try self.write("' here is a number: ");
    try self.printValue(arg1);
    try self.write("\n");
    try self.flush();
}

printValue — это функция, принимающая параметр любого типа и выполняющая разные действия в зависимости от типа:

poc_printValue_fn.zig
const Writer = struct {
    pub fn printValue(self: *Writer, value: anytype) !void {
        switch (@typeInfo(@TypeOf(value))) {
            .int => {
                return self.writeInt(value);
            },
            .float => {
                return self.writeFloat(value);
            },
            .pointer => {
                return self.write(value);
            },
            else => {
                @compileError("Unable to print type '" ++ @typeName(@TypeOf(value)) ++ "'");
            },
        }
    }

    fn write(self: *Writer, value: []const u8) !void {
        _ = self;
        _ = value;
    }
    fn writeInt(self: *Writer, value: anytype) !void {
        _ = self;
        _ = value;
    }
    fn writeFloat(self: *Writer, value: anytype) !void {
        _ = self;
        _ = value;
    }
};

А что произойдёт, если передать функции print слишком много аргументов?

test_print_too_many_args.zig
const print = @import("std").debug.print;

const a_number: i32 = 1234;
const a_string = "foobar";

test "print too many arguments" {
    print("here is a string: '{s}' here is a number: {}\n", .{
        a_string,
        a_number,
        a_number,
    });
}
Оболочка
$ zig test test_print_too_many_args.zig
/home/andy/src/zig/lib/std/Io/Writer.zig:736:18: error: unused argument in 'here is a string: '{s}' here is a number: {}
                                                        '
            1 => @compileError("unused argument in '" ++ fmt ++ "'"),
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    print__anon_42792: /home/andy/src/zig/lib/std/debug.zig:312:39
    test.print too many arguments: /home/andy/src/zig/doc/langref/test_print_too_many_args.zig:7:10

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

Zig не важно, является ли аргумент формата строковым литералом; важно лишь, чтобы это было значение, известное на этапе компиляции и приводимое к []const u8:

print_comptime-known_format.zig
const print = @import("std").debug.print;

const a_number: i32 = 1234;
const a_string = "foobar";
const fmt = "here is a string: '{s}' here is a number: {}\n";

pub fn main() void {
    print(fmt, .{ a_string, a_number });
}
Оболочка
$ zig build-exe print_comptime-known_format.zig
$ ./print_comptime-known_format
here is a string: 'foobar' here is a number: 1234

Это работает без проблем.

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

См. также:

  • inline while
  • inline for

Ассемблер

В некоторых случаях может потребоваться напрямую управлять машинным кодом, генерируемым программами на Zig, вместо того чтобы полагаться на генерацию кода Zig. Для этого можно использовать встроенный ассемблер. Вот пример реализации Hello, World для x86_64 Linux с помощью встроенного ассемблера:

inline_assembly.zig
pub fn main() noreturn {
    const msg = "hello world\n";
    _ = syscall3(SYS_write, STDOUT_FILENO, @intFromPtr(msg), msg.len);
    _ = syscall1(SYS_exit, 0);
    unreachable;
}

pub const SYS_write = 1;
pub const SYS_exit = 60;

pub const STDOUT_FILENO = 1;

pub fn syscall1(number: usize, arg1: usize) usize {
    return asm volatile ("syscall"
        : [ret] "={rax}" (-> usize),
        : [number] "{rax}" (number),
          [arg1] "{rdi}" (arg1),
        : .{ .rcx = true, .r11 = true });
}

pub fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) usize {
    return asm volatile ("syscall"
        : [ret] "={rax}" (-> usize),
        : [number] "{rax}" (number),
          [arg1] "{rdi}" (arg1),
          [arg2] "{rsi}" (arg2),
          [arg3] "{rdx}" (arg3),
        : .{ .rcx = true, .r11 = true });
}
Оболочка
$ zig build-exe inline_assembly.zig -target x86_64-linux
$ ./inline_assembly
hello world

Разберём синтаксис:

Assembly Syntax Explained.zig
pub fn syscall1(number: usize, arg1: usize) usize {
    // Inline assembly is an expression which returns a value.
    // the `asm` keyword begins the expression.
    return asm
    // `volatile` is an optional modifier that tells Zig this
    // inline assembly expression has side-effects. Without
    // `volatile`, Zig is allowed to delete the inline assembly
    // code if the result is unused.
    volatile (
    // Next is a comptime string which is the assembly code.
    // Inside this string one may use `%[ret]`, `%[number]`,
    // or `%[arg1]` where a register is expected, to specify
    // the register that Zig uses for the argument or return value,
    // if the register constraint strings are used. However in
    // the below code, this is not used. A literal `%` can be
    // obtained by escaping it with a double percent: `%%`.
    // Often multiline string syntax comes in handy here.
        \\syscall
        // Next is the output. It is possible in the future Zig will
        // support multiple outputs, depending on how
        // https://github.com/ziglang/zig/issues/215 is resolved.
        // It is allowed for there to be no outputs, in which case
        // this colon would be directly followed by the colon for the inputs.
        :
        // This specifies the name to be used in `%[ret]` syntax in
        // the above assembly string. This example does not use it,
        // but the syntax is mandatory.
          [ret]
          // Next is the output constraint string. This feature is still
          // considered unstable in Zig, and so LLVM/GCC documentation
          // must be used to understand the semantics.
          // http://releases.llvm.org/10.0.0/docs/LangRef.html#inline-asm-constraint-string
          // https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
          // In this example, the constraint string means "the result value of
          // this inline assembly instruction is whatever is in $rax".
          "={rax}"
          // Next is either a value binding, or `->` and then a type. The
          // type is the result type of the inline assembly expression.
          // If it is a value binding, then `%[ret]` syntax would be used
          // to refer to the register bound to the value.
          (-> usize),
          // Next is the list of inputs.
          // The constraint for these inputs means, "when the assembly code is
          // executed, $rax shall have the value of `number` and $rdi shall have
          // the value of `arg1`". Any number of input parameters is allowed,
          // including none.
        : [number] "{rax}" (number),
          [arg1] "{rdi}" (arg1),
          // Next is the list of clobbers. These declare a set of registers whose
          // values will not be preserved by the execution of this assembly code.
          // These do not include output or input registers. The special clobber
          // value of "memory" means that the assembly writes to arbitrary undeclared
          // memory locations - not only the memory pointed to by a declared indirect
          // output. In this example we list $rcx and $r11 because it is known the
          // kernel syscall does not preserve these registers.
        : .{ .rcx = true, .r11 = true });
}

Для целевых платформ x86 и x86_64 используется синтаксис AT&T, а не более популярный синтаксис Intel. Это связано с техническими ограничениями: разбор ассемблера выполняет LLVM, а поддержка синтаксиса Intel в LLVM содержит ошибки и недостаточно хорошо протестирована.

Возможно, когда-нибудь у Zig появится собственный ассемблер. Это позволит ему более естественно интегрироваться в язык и поддерживать популярный синтаксис NASM. Этот раздел документации будет обновлён до выпуска версии 1.0.0, когда появится окончательное заявление о статусе синтаксиса AT&T по сравнению с Intel/NASM.

Ограничения выходных данных

Ограничения выходных данных в Zig всё ещё считаются нестабильными, поэтому для понимания их семантики необходимо использовать документацию LLVM и документацию GCC.

Обратите внимание, что в рамках задачи № 215 планируются некоторые несовместимые изменения ограничений выходных данных.

Ограничения входных данных

Ограничения входных данных в Zig всё ещё считаются нестабильными, поэтому для понимания их семантики необходимо использовать документацию LLVM и документацию GCC.

Обратите внимание, что в рамках задачи № 215 планируются некоторые несовместимые изменения ограничений входных данных.

Изменяемые регистры

Список изменяемых регистров — это набор регистров, значения которых не сохраняются при выполнении ассемблерного кода. Выходные и входные регистры в него не входят. Специальное значение изменяемого регистра "memory" означает, что ассемблер выполняет запись в произвольные необъявленные области памяти, а не только в память, на которую указывает объявленный косвенный выходной параметр.

Необъявление полного набора изменяемых регистров для данного выражения встроенного ассемблера является непроверяемым недопустимым поведением.

Глобальный ассемблер

Если ассемблерное выражение находится в блоке comptime на уровне контейнера, это глобальный ассемблер.

Для этого вида ассемблера действуют иные правила, чем для встроенного ассемблера. Во-первых, volatile недопустим, поскольку весь глобальный ассемблер включается безусловно. Во-вторых, у него нет входных данных, выходных данных или изменяемых регистров. Весь глобальный ассемблер без изменений объединяется в одну длинную строку и собирается вместе. Здесь нет правил подстановки шаблонов для %, подобных тем, что применяются в выражениях встроенного ассемблера.

test_global_assembly.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

comptime {
    asm (
        \\.global my_func;
        \\.type my_func, @function;
        \\my_func:
        \\  lea (%rdi,%rsi,1),%eax
        \\  retq
    );
}

extern fn my_func(a: i32, b: i32) i32;

test "global assembly" {
    try expectEqual(46, my_func(12, 34));
}
Оболочка
$ zig test test_global_assembly.zig -target x86_64-linux -fllvm
1/1 test_global_assembly.test.global assembly...OK
All 1 tests passed.

Атомарные операции

TODO: @atomic rmw

TODO: встроенное перечисление порядка памяти для атомарных операций

См. также:

  • @atomicLoad
  • @atomicStore
  • @atomicRmw
  • @cmpxchgWeak
  • @cmpxchgStrong

Асинхронные функции

Асинхронные функции перестали работать с выпуском версии 0.11.0. Текущий план — вернуть их в качестве низкоуровневого примитива для реализации ввода-вывода.

Задача для отслеживания: Предложение: корутины без стека как низкоуровневые примитивы

Встроенные функции

Встроенные функции предоставляются компилятором и имеют префикс @. Ключевое слово comptime в параметре означает, что параметр должен быть известен на этапе компиляции.

@addrSpaceCast

@addrSpaceCast(ptr: anytype) anytype

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

@addWithOverflow

@addWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }

Выполняет a + b и возвращает кортеж с результатом и возможным битом переполнения.

@alignCast

@alignCast(ptr: anytype) anytype

ptr может быть *T, ?*T или []T. Изменяет выравнивание указателя. Требуемое выравнивание определяется на основе типа результата.

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

@alignOf

@alignOf(comptime T: type) comptime_int

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

const assert = @import("std").debug.assert;
comptime {
    assert(*u32 == *align(@alignOf(u32)) u32);
}

Результат — константа времени компиляции, зависящая от целевой платформы. Гарантируется, что он меньше или равен @sizeOf(T).

См. также:

  • Выравнивание

@as

@as(comptime T: type, expression) T

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

@atomicLoad

@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: AtomicOrder) T

Эта встроенная функция атомарно разыменовывает указатель на T и возвращает значение.

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

AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.

См. также:

  • @atomicStore
  • @atomicRmw
  • @cmpxchgWeak
  • @cmpxchgStrong

@atomicRmw

@atomicRmw(comptime T: type, ptr: *T, comptime op: AtomicRmwOp, operand: T, comptime ordering: AtomicOrder) T

Эта встроенная функция разыменовывает указатель на T, атомарно изменяет значение и возвращает предыдущее значение.

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

AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.

AtomicRmwOp можно найти с помощью @import("std").builtin.AtomicRmwOp.

См. также:

  • @atomicStore
  • @atomicLoad
  • @cmpxchgWeak
  • @cmpxchgStrong

@atomicStore

@atomicStore(comptime T: type, ptr: *T, value: T, comptime ordering: AtomicOrder) void

Эта встроенная функция разыменовывает указатель на T и атомарно сохраняет переданное значение.

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

AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.

См. также:

  • @atomicLoad
  • @atomicRmw
  • @cmpxchgWeak
  • @cmpxchgStrong

@bitCast

@bitCast(value: anytype) anytype

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

Утверждает, что @sizeOf(@TypeOf(value)) == @sizeOf(DestType).

Утверждает, что @typeInfo(DestType) != .pointer. Если это необходимо, используйте @ptrCast или @ptrFromInt.

Например, может использоваться для следующих целей:

  • Преобразование f32 в u32 бит
  • Преобразование i32 в u32 с сохранением дополнительного кода

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

@bitOffsetOf

@bitOffsetOf(comptime T: type, comptime field_name: []const u8) comptime_int

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

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

См. также:

  • @offsetOf

@bitSizeOf

@bitSizeOf(comptime T: type) comptime_int

Эта функция возвращает количество битов, необходимых для хранения T в памяти, если бы этот тип был полем упакованной структуры или объединения. Результат — константа времени компиляции, зависящая от целевой платформы.

Эта функция измеряет размер во время выполнения. Для типов, недопустимых во время выполнения, таких как comptime_int и type, результатом будет 0.

См. также:

  • @sizeOf
  • @typeInfo

@branchHint

@branchHint(hint: BranchHint) void

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

BranchHint можно найти с помощью @import("std").builtin.BranchHint.

Эта функция допустима только в качестве первого оператора в ветви потока управления или первого оператора функции.

@breakpoint

@breakpoint() void

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

Эта функция допустима только в области видимости функции.

См. также:

  • @trap

@mulAdd

@mulAdd(comptime T: type, a: T, b: T, c: T) T

Умножение с последующим сложением за одну операцию, аналогичное (a * b) + c, но с единственным округлением, поэтому результат точнее.

Поддерживает числа с плавающей запятой и векторы таких чисел.

@byteSwap

@byteSwap(operand: anytype) T

@TypeOf(operand) должен быть целочисленным типом или векторным целочисленным типом с количеством битов, кратным 8.

operand может быть целым числом или вектором.

Меняет порядок байтов целого числа. Это преобразует целое число с порядком байтов от старшего к младшему в целое число с порядком от младшего к старшему и наоборот.

Обратите внимание: для размещения в памяти с учётом порядка байтов целочисленный тип должен соответствовать количеству байтов, возвращаемому функцией @sizeOf. Это демонстрируется на примере u24. @sizeOf(u24) == 4, то есть u24 занимает в памяти 4 байта, и именно эти 4 байта меняются местами в системах с порядком байтов от младшего к старшему и от старшего к младшему. Если же для T указать тип u24, будут переставлены только 3 байта.

@bitReverse

@bitReverse(integer: anytype) T

@TypeOf(anytype) принимает любой целочисленный тип или векторный целочисленный тип.

Меняет порядок битов целочисленного значения, включая знаковый бит, если он есть.

Например, 0b10110110 (u8 = 182, i8 = -74) преобразуется в 0b01101101 (u8 = 109, i8 = 109).

@offsetOf

@offsetOf(comptime T: type, comptime field_name: []const u8) comptime_int

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

См. также:

  • @bitOffsetOf

@call

@call(modifier: std.builtin.CallModifier, function: anytype, args: anytype) anytype

Вызывает функцию так же, как и вызов выражения с помощью круглых скобок:

test_call_builtin.zig
const expectEqual = @import("std").testing.expectEqual;

test "noinline function call" {
    try expectEqual(12, @call(.auto, add, .{ 3, 9 }));
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}
Оболочка
$ zig test test_call_builtin.zig
1/1 test_call_builtin.test.noinline function call...OK
All 1 tests passed.

@call предоставляет больше возможностей, чем обычный синтаксис вызова функции. Ниже приведено определение перечисления CallModifier:

builtin.CallModifier struct.zig
pub const CallModifier = enum {
    /// Equivalent to function call syntax.
    auto,

    /// Prevents tail call optimization. This guarantees that the return
    /// address will point to the callsite, as opposed to the callsite's
    /// callsite. If the call is otherwise required to be tail-called
    /// or inlined, a compile error is emitted instead.
    never_tail,

    /// Guarantees that the call will not be inlined. If the call is
    /// otherwise required to be inlined, a compile error is emitted instead.
    never_inline,

    /// Asserts that the function call will not suspend. This allows a
    /// non-async function to call an async function.
    no_suspend,

    /// Guarantees that the call will be generated with tail call optimization.
    /// If this is not possible, a compile error is emitted instead.
    always_tail,

    /// Guarantees that the call will be inlined at the callsite.
    /// If this is not possible, a compile error is emitted instead.
    always_inline,

    /// Evaluates the call at compile-time. If the call cannot be completed at
    /// compile-time, a compile error is emitted instead.
    compile_time,
};

@cDefine

@cDefine(comptime name: []const u8, value) void

Эта функция может использоваться только внутри @cImport.

Она добавляет #define $name $value во временный буфер @cImport.

Чтобы определить макрос без значения, например так:

#define _GNU_SOURCE

Используйте значение void, например так:

@cDefine("_GNU_SOURCE", {})

См. также:

  • Импорт заголовочного файла C
  • @cInclude
  • @cImport
  • @cUndef
  • void

@cImport

@cImport(expression) type

Эта функция анализирует код C и импортирует функции, типы, переменные и совместимые определения макросов в новый пустой тип структуры, после чего возвращает этот тип.

Выражение expression интерпретируется на этапе компиляции. Встроенные функции @cInclude, @cDefine и @cUndef работают внутри этого выражения, добавляя данные во временный буфер, который затем анализируется как код C.

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

Несколько выражений @cImport могут понадобиться в следующих случаях:

  • Чтобы избежать конфликта символов, например, если в foo.h и bar.h оба определены #define CONNECTION_COUNT
  • Чтобы анализировать код C с разными определениями препроцессора

См. также:

  • Импорт заголовочного файла C
  • @cInclude
  • @cDefine
  • @cUndef

@cInclude

@cInclude(comptime path: []const u8) void

Эта функция может использоваться только внутри @cImport.

Она добавляет #include <$path>\n во временный буфер c_import.

См. также:

  • Импорт заголовочного файла C
  • @cImport
  • @cDefine
  • @cUndef

@clz

@clz(operand: anytype) anytype

@TypeOf(operand) должен быть целочисленным типом или векторным целочисленным типом.

operand может быть целым числом или вектором.

Подсчитывает количество нулевых битов в старших разрядах целого числа (при порядке от старшего байта к младшему) — «подсчёт ведущих нулей».

Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, достаточным для представления числа битов целочисленного типа.

Если operand равен нулю, @clz возвращает разрядность целочисленного типа T.

См. также:

  • @ctz
  • @popCount

@cmpxchgStrong

@cmpxchgStrong(comptime T: type, ptr: *T, expected_value: T, new_value: T, success_order: AtomicOrder, fail_order: AtomicOrder) ?T

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

not_atomic_cmpxchgStrong.zig
fn cmpxchgStrongButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_value: T) ?T {
    const old_value = ptr.*;
    if (old_value == expected_value) {
        ptr.* = new_value;
        return null;
    } else {
        return old_value;
    }
}

Если вы используете cmpxchg в цикле повторных попыток, лучше выбрать @cmpxchgWeak, поскольку эту операцию можно эффективнее реализовать машинными инструкциями.

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

@typeInfo(@TypeOf(ptr)).pointer.alignment должен быть >= @sizeOf(T).

AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.

См. также:

  • @atomicStore
  • @atomicLoad
  • @atomicRmw
  • @cmpxchgWeak

@cmpxchgWeak

@cmpxchgWeak(comptime T: type, ptr: *T, expected_value: T, new_value: T, success_order: AtomicOrder, fail_order: AtomicOrder) ?T

Эта функция выполняет слабую атомарную операцию сравнения и обмена, возвращая null, если текущее значение совпадает с ожидаемым. Она эквивалентна следующему коду, но с атомарной операцией:

cmpxchgWeakButNotAtomic
fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_value: T) ?T {
    const old_value = ptr.*;
    if (old_value == expected_value and usuallyTrueButSometimesFalse()) {
        ptr.* = new_value;
        return null;
    } else {
        return old_value;
    }
}

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

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

@typeInfo(@TypeOf(ptr)).pointer.alignment должен быть >= @sizeOf(T).

AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.

См. также:

  • @atomicStore
  • @atomicLoad
  • @atomicRmw
  • @cmpxchgStrong

@compileError

@compileError(comptime msg: []const u8) noreturn

Эта функция при семантическом анализе вызывает ошибку компиляции с сообщением msg.

Существует несколько способов избежать семантической проверки кода, например использовать if или switch с константами времени компиляции, а также функции comptime.

@compileLog

@compileLog(...) void

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

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

Эту функцию можно использовать для «отладки с помощью printf» кода, выполняющегося во время компиляции.

test_compileLog_builtin.zig
const print = @import("std").debug.print;

const num1 = blk: {
    var val1: i32 = 99;
    @compileLog("comptime val1 = ", val1);
    val1 = val1 + 1;
    break :blk val1;
};

test "main" {
    @compileLog("comptime in main");

    print("Runtime in main, num1 = {}.\n", .{num1});
}
Оболочка
$ zig test test_compileLog_builtin.zig
/home/andy/src/zig/doc/langref/test_compileLog_builtin.zig:5:5: error: found compile log statement
    @compileLog("comptime val1 = ", val1);
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_compileLog_builtin.zig:11:5: note: also here
    @compileLog("comptime in main");
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    test.main: /home/andy/src/zig/doc/langref/test_compileLog_builtin.zig:13:46

Compile Log Output:
@as(*const [16:0]u8, "comptime val1 = "), @as(i32, 99)
@as(*const [16:0]u8, "comptime in main")

@constCast

@constCast(value: anytype) DestType

Удаляет квалификатор const у указателя.

@ctz

@ctz(operand: anytype) anytype

@TypeOf(operand) должен быть целочисленным типом или векторным целочисленным типом.

operand может быть целым числом или вектором.

Подсчитывает количество младших (в смысле порядка байтов от старшего к младшему — конечных) нулей в целом числе — «подсчёт конечных нулей».

Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, необходимым для представления количества битов целочисленного типа.

Если operand равно нулю, @ctz возвращает разрядность целочисленного типа T.

См. также:

  • @clz
  • @popCount

@cUndef

@cUndef(comptime name: []const u8) void

Эта функция может встречаться только внутри @cImport.

Она добавляет #undef $name во временный буфер @cImport.

См. также:

  • Импорт из заголовочного файла C
  • @cImport
  • @cDefine
  • @cInclude

@cVaArg

@cVaArg(operand: *std.builtin.VaList, comptime T: type) T

Реализует макрос C va_arg.

См. также:

  • @cVaCopy
  • @cVaEnd
  • @cVaStart

@cVaCopy

@cVaCopy(src: *std.builtin.VaList) std.builtin.VaList

Реализует макрос C va_copy.

См. также:

  • @cVaArg
  • @cVaEnd
  • @cVaStart

@cVaEnd

@cVaEnd(src: *std.builtin.VaList) void

Реализует макрос C va_end.

См. также:

  • @cVaArg
  • @cVaCopy
  • @cVaStart

@cVaStart

@cVaStart() std.builtin.VaList

Реализует макрос C va_start. Допустим только внутри функции с переменным числом аргументов.

См. также:

  • @cVaArg
  • @cVaCopy
  • @cVaEnd

@divExact

@divExact(numerator: T, denominator: T) T

Точное деление. Вызывающий код гарантирует denominator != 0 и @divTrunc(numerator, denominator) * denominator == numerator.

  • @divExact(6, 3) == 2
  • @divExact(a, b) * b == a

Для функции, возвращающей возможный код ошибки, используйте @import("std").math.divExact.

См. также:

  • @divTrunc
  • @divFloor

@divFloor

@divFloor(numerator: T, denominator: T) T

Деление с округлением вниз. Округляет в сторону отрицательной бесконечности. Для беззнаковых целых чисел оно совпадает с numerator / denominator. Вызывающий код гарантирует denominator != 0 и !(@typeInfo(T) == .int and T.is_signed and numerator == std.math.minInt(T) and denominator == -1).

  • @divFloor(-5, 3) == -2
  • (@divFloor(a, b) * b) + @mod(a, b) == a

Для функции, возвращающей возможный код ошибки, используйте @import("std").math.divFloor.

См. также:

  • @divTrunc
  • @divExact

@divTrunc

@divTrunc(numerator: T, denominator: T) T

Деление с усечением. Округляет в сторону нуля. Для беззнаковых целых чисел оно совпадает с numerator / denominator. Вызывающий код гарантирует denominator != 0 и !(@typeInfo(T) == .int and T.is_signed and numerator == std.math.minInt(T) and denominator == -1).

  • @divTrunc(-5, 3) == -1
  • (@divTrunc(a, b) * b) + @rem(a, b) == a

Для функции, возвращающей возможный код ошибки, используйте @import("std").math.divTrunc.

См. также:

  • @divFloor
  • @divExact

@embedFile

@embedFile(comptime path: []const u8) *const [N:0]u8

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

path является абсолютным путём или путём относительно текущего файла, как и @import.

См. также:

  • @import

@enumFromInt

@enumFromInt(integer: anytype) anytype

Преобразует целое число в значение перечисления. Возвращаемый тип выводится из контекста.

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

См. также:

  • @intFromEnum

@errorFromInt

@errorFromInt(value: @Int(.unsigned, @bitSizeOf(anyerror))) anyerror

Преобразует целочисленное представление ошибки в тип глобального набора ошибок.

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

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

См. также:

  • @intFromError

@errorName

@errorName(err: anyerror) [:0]const u8

Эта функция возвращает строковое представление ошибки. Строковое представление error.OutOfMem — "OutOfMem".

Если во всём приложении нет вызовов @errorName или для всех вызовов значение err известно во время компиляции, таблица имён ошибок не создаётся.

@errorReturnTrace

@errorReturnTrace() ?*builtin.StackTrace

Если двоичный файл собран с трассировкой возврата ошибок и эта функция вызывается из функции, которая вызывает функцию с типом возврата «ошибка» или «объединение ошибки», возвращает объект трассировки стека. В противном случае возвращает null.

@errorCast

@errorCast(value: anytype) anytype

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

@export

@export(comptime ptr: *const anyopaque, comptime options: std.builtin.ExportOptions) void

Создаёт в выходном объектном файле символ, ссылающийся на цель ptr.

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

Эту встроенную функцию можно вызвать из блока comptime, чтобы условно экспортировать символы. Если ptr указывает на функцию с соглашением о вызовах C, а options.linkage равен .strong, это эквивалентно ключевому слову export, применённому к функции:

export_builtin.zig
comptime {
    @export(&internalName, .{ .name = "foo", .linkage = .strong });
}

fn internalName() callconv(.c) void {}
Оболочка
$ zig build-obj export_builtin.zig

Это эквивалентно следующему:

export_builtin_equivalent_code.zig
export fn foo() void {}
Оболочка
$ zig build-obj export_builtin_equivalent_code.zig

Обратите внимание: даже при использовании export можно применить синтаксис @"foo" для идентификаторов, чтобы выбрать любую строку в качестве имени символа:

export_any_symbol_name.zig
export fn @"A function name that is a complete sentence."() void {}
Оболочка
$ zig build-obj export_any_symbol_name.zig

При просмотре полученного объектного файла видно, что символ используется без изменений:

00000000000001f0 T A function name that is a complete sentence.

См. также:

  • Экспорт библиотеки C

@extern

@extern(T: type, comptime options: std.builtin.ExternOptions) T

Создаёт ссылку на внешний символ в выходном объектном файле. T должен быть типом указателя.

См. также:

  • @export

@field

@field(lhs: anytype, comptime field_name: []const u8) (field)

Выполняет доступ к полю по строке, известной во время компиляции. Работает как с полями, так и с объявлениями.

test_field_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Point = struct {
    x: u32,
    y: u32,

    pub var z: u32 = 1;
};

test "field access by string" {
    var p = Point{ .x = 0, .y = 0 };

    @field(p, "x") = 4;
    @field(p, "y") = @field(p, "x") + 1;

    try expectEqual(4, @field(p, "x"));
    try expectEqual(5, @field(p, "y"));
}

test "decl access by string" {
    try expectEqual(1, @field(Point, "z"));

    @field(Point, "z") = 2;
    try expectEqual(2, @field(Point, "z"));
}
Оболочка
$ zig test test_field_builtin.zig
1/2 test_field_builtin.test.field access by string...OK
2/2 test_field_builtin.test.decl access by string...OK
All 2 tests passed.

@fieldParentPtr

@fieldParentPtr(comptime field_name: []const u8, field_ptr: *T) anytype

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

Если field_ptr не указывает на поле field_name экземпляра типа результата, а тип результата имеет неопределённую раскладку, возникает непроверяемое недопустимое поведение.

@FieldType

@FieldType(comptime Type: type, comptime field_name: []const u8) type

Получив тип и имя одного из его полей, возвращает тип этого поля.

@floatCast

@floatCast(value: anytype) anytype

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

@floatFromInt

@floatFromInt(int: anytype) anytype

Преобразует целое число в ближайшее представимое число с плавающей точкой. Возвращаемый тип выводится из контекста. Для обратного преобразования используйте @intFromFloat. Эта операция допустима для всех значений всех целочисленных типов.

@frameAddress

@frameAddress() usize

Эта функция возвращает базовый указатель текущего кадра стека.

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

Эта функция допустима только в области видимости функции.

@hasDecl

@hasDecl(comptime Container: type, comptime name: []const u8) bool

Возвращает, содержит ли контейнер объявление с именем name.

test_hasDecl_builtin.zig
const std = @import("std");
const expect = std.testing.expect;

const Foo = struct {
    nope: i32,

    pub var blah = "xxx";
    const hi = 1;
};

test "@hasDecl" {
    try expect(@hasDecl(Foo, "blah"));

    // Even though `hi` is private, @hasDecl returns true because this test is
    // in the same file scope as Foo. It would return false if Foo was declared
    // in a different file.
    try expect(@hasDecl(Foo, "hi"));

    // @hasDecl is for declarations; not fields.
    try expect(!@hasDecl(Foo, "nope"));
    try expect(!@hasDecl(Foo, "nope1234"));
}
Оболочка
$ zig test test_hasDecl_builtin.zig
1/1 test_hasDecl_builtin.test.@hasDecl...OK
All 1 tests passed.

См. также:

  • @hasField

@hasField

@hasField(comptime Container: type, comptime name: []const u8) bool

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

Результат является константой времени компиляции.

Функции, переменные и константы не учитываются.

См. также:

  • @hasDecl

@import

@import(comptime target: []const u8) anytype

Импортирует файл по пути target, добавляя его в компиляцию, если он ещё не добавлен. target — это либо относительный путь к другому файлу от файла, содержащего вызов @import, либо имя модуля, при котором импорт ссылается на корневой исходный файл этого модуля. В обоих случаях путь к файлу должен оканчиваться на .zig (для исходного файла Zig) или .zon (для файла данных ZON).

Если target указывает на исходный файл Zig, то @import возвращает соответствующий тип структуры этого файла — по сути, как если бы вызов встроенной функции был заменён на struct { FILE_CONTENTS }. Возвращаемый тип — type.

Если target указывает на файл ZON, то @import возвращает значение литерала в файле. Если тип результата выводится из контекста (типы результата), возвращаемым типом будет этот тип, а литерал ZON интерпретируется как значение этого типа (тип результата передаётся через выражение ZON). В противном случае возвращаемым типом будет тип эквивалентного выражения Zig — по сути, как если бы вызов встроенной функции был заменён содержимым файла ZON.

Для импорта всегда доступны следующие модули:

  • @import("std") — стандартная библиотека Zig
  • @import("builtin") — сведения, зависящие от целевой платформы. Команда zig build-exe --show-builtin выводит исходный код в stdout для справки.
  • @import("root") — псевдоним корневого модуля. В типичной структуре проекта это означает, что он ссылается на src/main.zig.

См. также:

  • Переменные компиляции
  • @embedFile

@inComptime

@inComptime() bool

Возвращает, выполнялась ли встроенная функция в контексте comptime. Результат является константой времени компиляции.

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

См. также:

  • comptime

@intCast

@intCast(int: anytype) anytype

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

test_intCast_builtin.zig
test "integer cast panic" {
    var a: u16 = 0xabcd; // runtime-known
    _ = &a;
    const b: u8 = @intCast(a);
    _ = b;
}
Оболочка
$ zig test test_intCast_builtin.zig
1/1 test_intCast_builtin.test.integer cast panic...thread 934579 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/test_intCast_builtin.zig:4:19: 0x1234e40 in test.integer cast panic (test_intCast_builtin.zig)
    const b: u8 = @intCast(a);
                  ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/22604da2684fadfdf98324181ed766ec/test --seed=0x9949decf

Чтобы усечь значащие биты числа, выходящего за диапазон целевого типа, используйте @truncate.

Если T равно comptime_int, то семантически это эквивалентно приведению типа.

@intFromBool

@intFromBool(value: bool) u1

Преобразует true в @as(u1, 1), а false — в @as(u1, 0).

@intFromEnum

@intFromEnum(enum_or_tagged_union: anytype) anytype

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

Если возможно только одно значение перечисления, результатом будет comptime_int, известное на этапе comptime.

См. также:

  • @enumFromInt

@intFromError

@intFromError(err: anytype) @Int(.unsigned, @bitSizeOf(anyerror))

Поддерживает следующие типы:

  • Глобальный набор ошибок
  • Тип набора ошибок
  • Тип объединения ошибки

Преобразует ошибку в её целочисленное представление.

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

См. также:

  • @errorFromInt

@intFromFloat

@intFromFloat(float: anytype) anytype

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

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

См. также:

  • @floatFromInt

@intFromPtr

@intFromPtr(value: anytype) usize

Преобразует value в usize, являющееся адресом указателя. value может быть *T или ?*T.

Для обратного преобразования используйте @ptrFromInt

@max

@max(...) T

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

NaN обрабатываются следующим образом: возвращается наибольшее из переданных значений, не являющихся NaN. Если все операнды — NaN, возвращается NaN.

См. также:

  • @min
  • Векторы

@memcpy

@memcpy(noalias dest, noalias source) void

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

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

source должен быть срезом, указателем на массив или указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.

Тип элементов source должен иметь такое же представление в памяти, как тип элементов dest.

Как и в циклах for, длину должен задавать хотя бы один из аргументов — source или dest; если заданы обе длины, они должны быть равны.

Наконец, области памяти не должны перекрываться.

@memset

@memset(dest, elem) void

Эта функция устанавливает для всех элементов области памяти значение elem.

dest должен быть изменяемым срезом или изменяемым указателем на массив. Он может иметь любое выравнивание и любой тип элементов.

elem приводится к типу элементов dest.

Для безопасного обнуления конфиденциальных данных в памяти следует использовать std.crypto.secureZero

@memmove

@memmove(dest, source) void

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

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

source должен быть срезом, указателем на массив или указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.

Тип элементов source должен иметь такое же представление в памяти, как тип элементов dest.

Как и в циклах for, длину должен задавать хотя бы один из аргументов — source или dest; если заданы обе длины, они должны быть равны.

@min

@min(...) T

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

NaN обрабатываются следующим образом: возвращается наименьшее из переданных значений, не являющихся NaN. Если все операнды — NaN, возвращается NaN.

См. также:

  • @max
  • Векторы

@wasmMemorySize

@wasmMemorySize(index: u32) usize

Эта функция возвращает размер памяти Wasm, заданной index, в виде беззнакового значения в единицах страниц Wasm. Обратите внимание: размер каждой страницы Wasm составляет 64 КБ.

Эта функция — низкоуровневая встроенная операция без механизмов безопасности, обычно полезная разработчикам распределителей памяти для Wasm. Поэтому, если вы не пишете распределитель памяти с нуля, используйте что-нибудь вроде @import("std").heap.WasmPageAllocator.

См. также:

  • @wasmMemoryGrow

@wasmMemoryGrow

@wasmMemoryGrow(index: u32, delta: usize) isize

Эта функция увеличивает размер памяти Wasm, заданной index, на delta страниц Wasm (беззнаковое число). Обратите внимание: размер каждой страницы Wasm составляет 64 КБ. В случае успеха возвращает предыдущий размер памяти; если выделение памяти не удаётся, возвращает -1.

Эта функция — низкоуровневая встроенная операция без механизмов безопасности, обычно полезная разработчикам распределителей памяти для Wasm. Поэтому, если вы не пишете распределитель памяти с нуля, используйте что-нибудь вроде @import("std").heap.WasmPageAllocator.

test_wasmMemoryGrow_builtin.zig
const std = @import("std");
const native_arch = @import("builtin").target.cpu.arch;
const expectEqual = std.testing.expectEqual;

test "@wasmMemoryGrow" {
    if (native_arch != .wasm32) return error.SkipZigTest;

    const prev = @wasmMemorySize(0);
    try expectEqual(@wasmMemoryGrow(0, 1), prev);
    try expectEqual(@wasmMemorySize(0), prev + 1);
}
Оболочка
$ zig test test_wasmMemoryGrow_builtin.zig
1/1 test_wasmMemoryGrow_builtin.test.@wasmMemoryGrow...SKIP
0 passed; 1 skipped; 0 failed.

См. также:

  • @wasmMemorySize

@mod

@mod(numerator: T, denominator: T) T

Деление по модулю. Для беззнаковых целых чисел оно совпадает с numerator % denominator. Вызывающий код гарантирует denominator != 0; в противном случае при включённых проверках безопасности во время выполнения операция приведёт к делению остатка на ноль.

  • @mod(-5, 3) == 1
  • (@divFloor(a, b) * b) + @mod(a, b) == a

Функцию, возвращающую код ошибки, см. в @import("std").math.mod.

См. также:

  • @rem

@mulWithOverflow

@mulWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }

Выполняет a * b и возвращает кортеж с результатом и возможным битом переполнения.

@panic

@panic(message: []const u8) noreturn

Вызывает функцию-обработчик паники. По умолчанию функция-обработчик паники вызывает общедоступную функцию panic, объявленную в корневом исходном файле, либо, если такая функция не указана, функцию std.builtin.default_panic из std/builtin.zig.

Как правило, лучше использовать @import("std").debug.panic. Однако @panic может быть полезна в двух случаях:

  • Вызов функции паники программиста из кода библиотеки, если она объявлена в корневом исходном файле.
  • При смешивании кода C и Zig — вызов канонической реализации паники из нескольких файлов .o.

См. также:

  • Обработчик паники

@popCount

@popCount(operand: anytype) anytype

@TypeOf(operand) должен быть целочисленным типом.

operand может быть целым числом или вектором.

Подсчитывает количество установленных битов в целом числе — «подсчёт единичных битов».

Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, необходимым для представления количества битов целочисленного типа.

См. также:

  • @ctz
  • @clz

@prefetch

@prefetch(ptr: anytype, comptime options: PrefetchOptions) void

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

Аргумент ptr может иметь любой тип указателя и задаёт адрес памяти для предварительной выборки. Эта функция не разыменовывает указатель; передача указателя на недопустимую область памяти полностью законна и не приводит к недопустимому поведению.

PrefetchOptions можно найти с помощью @import("std").builtin.PrefetchOptions.

@ptrCast

@ptrCast(value: anytype) anytype

Преобразует указатель одного типа в указатель другого типа. Возвращаемый тип выводится из контекста.

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

@ptrCast нельзя использовать для следующих операций:

  • Удаление квалификатора const — используйте @constCast.
  • Удаление квалификатора volatile — используйте @volatileCast.
  • Изменение адресного пространства указателя — используйте @addrSpaceCast.
  • Увеличение выравнивания указателя — используйте @alignCast.
  • Преобразование указателя, не являющегося срезом, в срез — используйте синтаксис среза ptr[start..end].

@ptrFromInt

@ptrFromInt(address: usize) anytype

Преобразует целое число в указатель. Возвращаемый тип выводится из контекста. Для обратного преобразования используйте @intFromPtr. Преобразование адреса 0 в целевой тип, который не является необязательным и не имеет атрибута allowzero, приводит к панике недопустимый null при приведении указателя, если включены проверки безопасности во время выполнения.

Если целевой тип указателя не допускает нулевой адрес, а address равно нулю, возникает недопустимое поведение, проверяемое средствами безопасности.

@rem

@rem(numerator: T, denominator: T) T

Деление с остатком. Для беззнаковых целых чисел оно совпадает с numerator % denominator. Вызывающий код гарантирует denominator != 0; в противном случае при включённых проверках безопасности во время выполнения операция приведёт к делению остатка на ноль.

  • @rem(-5, 3) == -2
  • (@divTrunc(a, b) * b) + @rem(a, b) == a

Функцию, возвращающую код ошибки, см. в @import("std").math.rem.

См. также:

  • @mod

@returnAddress

@returnAddress() usize

Эта функция возвращает адрес следующей инструкции машинного кода, которая будет выполнена после возврата из текущей функции.

Последствия этого зависят от целевой платформы и различаются на разных платформах.

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

@select

@select(comptime T: type, pred: @Vector(len, bool), a: @Vector(len, T), b: @Vector(len, T)) @Vector(len, T)

Поэлементно выбирает значения из a или b на основе pred. Если pred[i] равно true, соответствующим элементом результата будет a[i], иначе — b[i].

См. также:

  • Векторы

@setEvalBranchQuota

@setEvalBranchQuota(comptime new_quota: u32) void

Увеличивает максимальное количество обратных переходов, которое может выполнить код во время компиляции, прежде чем выполнение будет прервано с ошибкой компиляции.

Если new_quota меньше квоты по умолчанию (1000) или ранее явно заданной квоты, оно игнорируется.

Пример:

test_without_setEvalBranchQuota_builtin.zig
test "foo" {
    comptime {
        var i = 0;
        while (i < 1001) : (i += 1) {}
    }
}
Оболочка
$ zig test test_without_setEvalBranchQuota_builtin.zig
/home/andy/src/zig/doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: error: evaluation exceeded 1000 backwards branches
        while (i < 1001) : (i += 1) {}
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: note: use @setEvalBranchQuota() to raise the branch limit from 1000

Теперь воспользуемся @setEvalBranchQuota:

test_setEvalBranchQuota_builtin.zig
test "foo" {
    comptime {
        @setEvalBranchQuota(1001);
        var i = 0;
        while (i < 1001) : (i += 1) {}
    }
}
Оболочка
$ zig test test_setEvalBranchQuota_builtin.zig
1/1 test_setEvalBranchQuota_builtin.test.foo...OK
All 1 tests passed.

См. также:

  • comptime

@setFloatMode

@setFloatMode(comptime mode: FloatMode) void

Изменяет правила текущей области видимости, определяющие поведение операций с плавающей точкой.

  • .strict (по умолчанию) — операции с плавающей точкой строго соответствуют стандарту IEEE.
  • .optimized — для операций с плавающей точкой допускается всё перечисленное ниже:
    • Предполагать, что аргументы и результат не являются NaN. Оптимизации должны сохранять допустимое поведение при значениях NaN, но значение результата не определено.
    • Предполагать, что аргументы и результат не являются +/-Inf. Оптимизации должны сохранять допустимое поведение при значениях +/-Inf, но значение результата не определено.
    • Считать знак нулевого аргумента или результата несущественным.
    • Использовать обратную величину аргумента вместо выполнения деления.
    • Выполнять свёртку операций с плавающей точкой (например, объединять умножение и следующее за ним сложение в одну операцию умножения с накоплением).
    • Выполнять алгебраически эквивалентные преобразования, которые могут изменить результаты вычислений с плавающей точкой (например, менять группировку операций).
    Это эквивалентно -ffast-math в GCC.

Режим операций с плавающей точкой наследуется вложенными областями видимости и может быть переопределён в любой области видимости. Режим операций с плавающей точкой можно задать в области видимости структуры или модуля с помощью блока comptime.

FloatMode можно найти с помощью @import("std").builtin.FloatMode.

См. также:

  • Операции с плавающей точкой

@setRuntimeSafety

@setRuntimeSafety(comptime safety_on: bool) void

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

test_setRuntimeSafety_builtin.zig
test "@setRuntimeSafety" {
    // The builtin applies to the scope that it is called in. So here, integer overflow
    // will not be caught in ReleaseFast and ReleaseSmall modes:
    // var x: u8 = 255;
    // x += 1; // Unchecked Illegal Behavior in ReleaseFast/ReleaseSmall modes.
    {
        // However this block has safety enabled, so safety checks happen here,
        // even in ReleaseFast and ReleaseSmall modes.
        @setRuntimeSafety(true);
        var x: u8 = 255;
        x += 1;

        {
            // The value can be overridden at any scope. So here integer overflow
            // would not be caught in any build mode.
            @setRuntimeSafety(false);
            // var x: u8 = 255;
            // x += 1; // Unchecked Illegal Behavior in all build modes.
        }
    }
}
Оболочка
$ zig test test_setRuntimeSafety_builtin.zig -OReleaseFast
1/1 test_setRuntimeSafety_builtin.test.@setRuntimeSafety...thread 935744 panic: integer overflow
/home/andy/src/zig/doc/langref/test_setRuntimeSafety_builtin.zig:11:11: 0x10855f8 in test.@setRuntimeSafety (test)
        x += 1;
          ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x1074feb in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x107314a in callMain (test)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x1072c1d in _start (test)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/fedda1f87f11ec581f9d78378c471f74/test --seed=0x5af059f2

Примечание: планируется заменить @setRuntimeSafety на @optimizeFor

@shlExact

@shlExact(value: T, shift_amt: Log2T) T

Выполняет операцию сдвига влево (<<). Для беззнаковых целых чисел результат является неопределённым, если сдвигаются какие-либо биты 1. Для знаковых целых чисел результат является неопределённым, если сдвигаются какие-либо биты, не совпадающие с результирующим знаковым битом.

Тип shift_amt — беззнаковое целое число из log2(@typeInfo(T).int.bits) битов. Это связано с тем, что shift_amt >= @typeInfo(T).int.bits вызывает недопустимое поведение с проверкой безопасности.

comptime_int моделируется как целое число с бесконечным количеством битов, а это значит, что в таком случае @shlExact всегда возвращает результат и не может вызвать ошибку компиляции.

См. также:

  • @shrExact
  • @shlWithOverflow

@shlWithOverflow

@shlWithOverflow(a: anytype, shift_amt: Log2T) struct { @TypeOf(a), u1 }

Выполняет a << b и возвращает кортеж с результатом и возможным битом переполнения.

Тип shift_amt — беззнаковое целое число из log2(@typeInfo(@TypeOf(a)).int.bits) битов. Это связано с тем, что shift_amt >= @typeInfo(@TypeOf(a)).int.bits вызывает недопустимое поведение с проверкой безопасности.

См. также:

  • @shlExact
  • @shrExact

@shrExact

@shrExact(value: T, shift_amt: Log2T) T

Выполняет операцию сдвига вправо (>>). Вызывающая сторона гарантирует, что при сдвиге не будут сдвинуты биты 1.

Тип shift_amt — беззнаковое целое число из log2(@typeInfo(T).int.bits) битов. Это связано с тем, что shift_amt >= @typeInfo(T).int.bits вызывает недопустимое поведение с проверкой безопасности.

См. также:

  • @shlExact
  • @shlWithOverflow

@shuffle

@shuffle(comptime E: type, a: @Vector(a_len, E), b: @Vector(b_len, E), comptime mask: @Vector(mask_len, i32)) @Vector(mask_len, E)

Создаёт новый вектор, выбирая элементы из a и b на основе mask.

Каждый элемент в mask выбирает элемент либо из a, либо из b. Положительные числа выбирают элементы из a, начиная с 0. Отрицательные значения выбирают элементы из b, начиная с -1 и двигаясь вниз. Для индексов из b рекомендуется использовать оператор ~, чтобы оба индекса начинались с 0 (то есть ~@as(i32, 0) — это -1).

Для каждого элемента mask, если он сам или выбранное значение из a либо b является undefined, результирующий элемент будет undefined.

a_len и b_len могут иметь разную длину. Индексы элементов за пределами диапазона в mask приводят к ошибкам компиляции.

Если a или b является undefined, оно эквивалентно вектору, состоящему из undefined, длина которого совпадает с длиной другого вектора. Если оба вектора являются undefined, @shuffle возвращает вектор, все элементы которого — undefined.

E должен быть целым числом, числом с плавающей точкой, указателем или bool. Маска может иметь любую длину вектора, и её длина определяет длину результата.

test_shuffle_builtin.zig
const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;

test "vector @shuffle" {
    const a = @Vector(7, u8){ 'o', 'l', 'h', 'e', 'r', 'z', 'w' };
    const b = @Vector(4, u8){ 'w', 'd', '!', 'x' };

    // To shuffle within a single vector, pass undefined as the second argument.
    // Notice that we can re-order, duplicate, or omit elements of the input vector
    const mask1 = @Vector(5, i32){ 2, 3, 1, 1, 0 };
    const res1: @Vector(5, u8) = @shuffle(u8, a, undefined, mask1);
    try expectEqualStrings("hello", &@as([5]u8, res1));

    // Combining two vectors
    const mask2 = @Vector(6, i32){ -1, 0, 4, 1, -2, -3 };
    const res2: @Vector(6, u8) = @shuffle(u8, a, b, mask2);
    try expectEqualStrings("world!", &@as([6]u8, res2));
}
Командная строка
$ zig test test_shuffle_builtin.zig
1/1 test_shuffle_builtin.test.vector @shuffle...OK
All 1 tests passed.

См. также:

  • Векторы

@sizeOf

@sizeOf(comptime T: type) comptime_int

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

Этот размер может включать байты-заполнители. Если бы в памяти находились два элемента T подряд, заполнение представляло бы собой смещение в байтах между элементом с индексом 0 и элементом с индексом 1. Для целых чисел подумайте, следует ли использовать @sizeOf(T) или @typeInfo(T).int.bits.

Эта функция измеряет размер во время выполнения. Для типов, использование которых во время выполнения запрещено, таких как comptime_int и type, результатом будет 0.

См. также:

  • @bitSizeOf
  • @typeInfo

@splat

@splat(scalar: anytype) anytype

Создаёт массив или вектор, каждый элемент которого равен значению scalar. Возвращаемый тип и, следовательно, длина вектора выводятся автоматически.

test_splat_builtin.zig
const std = @import("std");
const expectEqualSlices = std.testing.expectEqualSlices;

test "vector @splat" {
    const scalar: u32 = 5;
    const result: @Vector(4, u32) = @splat(scalar);
    try expectEqualSlices(u32, &[_]u32{ 5, 5, 5, 5 }, &@as([4]u32, result));
}

test "array @splat" {
    const scalar: u32 = 5;
    const result: [4]u32 = @splat(scalar);
    try expectEqualSlices(u32, &[_]u32{ 5, 5, 5, 5 }, &@as([4]u32, result));
}
Командная строка
$ zig test test_splat_builtin.zig
1/2 test_splat_builtin.test.vector @splat...OK
2/2 test_splat_builtin.test.array @splat...OK
All 2 tests passed.

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

См. также:

  • Векторы
  • @shuffle

@reduce

@reduce(comptime op: std.builtin.ReduceOp, value: anytype) E

Преобразует вектор в скалярное значение (типа E), последовательно выполняя горизонтальную свёртку его элементов с помощью указанного оператора op.

Не каждый оператор доступен для любого типа элементов вектора:

  • Все операторы доступны для векторов целых чисел.
  • .And, .Or, .Xor дополнительно доступны для векторов bool,
  • .Min, .Max, .Add, .Mul дополнительно доступны для векторов чисел с плавающей точкой,

Обратите внимание, что свёртки .Add и .Mul для целочисленных типов выполняются с циклическим переполнением; при применении к типам с плавающей точкой сохраняется ассоциативность операции, если только режим плавающей точки не установлен в .optimized.

test_reduce_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "vector @reduce" {
    const V = @Vector(4, i32);
    const value = V{ 1, -1, 1, -1 };
    const result = value > @as(V, @splat(0));
    // result is { true, false, true, false };
    try comptime expectEqual(@Vector(4, bool), @TypeOf(result));
    const is_all_true = @reduce(.And, result);
    try comptime expectEqual(bool, @TypeOf(is_all_true));
    try expectEqual(false, is_all_true);
}
Командная строка
$ zig test test_reduce_builtin.zig
1/1 test_reduce_builtin.test.vector @reduce...OK
All 1 tests passed.

См. также:

  • Векторы
  • @setFloatMode

@src

@src() std.builtin.SourceLocation

Возвращает структуру SourceLocation, представляющую имя функции и её расположение в исходном коде. Функцию необходимо вызывать внутри функции.

test_src_builtin.zig
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

test "@src" {
    try doTheTest();
}

fn doTheTest() !void {
    const src = @src();

    try expectEqual(10, src.line);
    try expectEqual(17, src.column);
    try expect(std.mem.endsWith(u8, src.fn_name, "doTheTest"));
    try expect(std.mem.endsWith(u8, src.file, "test_src_builtin.zig"));
}
Командная строка
$ zig test test_src_builtin.zig
1/1 test_src_builtin.test.@src...OK
All 1 tests passed.

@sqrt

@sqrt(value: anytype) @TypeOf(value)

Вычисляет квадратный корень числа с плавающей точкой. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@sin

@sin(value: anytype) @TypeOf(value)

Вычисляет синус числа с плавающей точкой, заданного в радианах. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@cos

@cos(value: anytype) @TypeOf(value)

Вычисляет косинус числа с плавающей точкой, заданного в радианах. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@tan

@tan(value: anytype) @TypeOf(value)

Вычисляет тангенс числа с плавающей точкой, заданного в радианах. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@exp

@exp(value: anytype) @TypeOf(value)

Вычисляет экспоненту числа с плавающей точкой по основанию e. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@exp2

@exp2(value: anytype) @TypeOf(value)

Вычисляет экспоненту числа с плавающей точкой по основанию 2. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@log

@log(value: anytype) @TypeOf(value)

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

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@log2

@log2(value: anytype) @TypeOf(value)

Возвращает логарифм числа с плавающей точкой по основанию 2. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@log10

@log10(value: anytype) @TypeOf(value)

Возвращает логарифм числа с плавающей точкой по основанию 10. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@abs

@abs(value: anytype) anytype

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

Поддерживает числа с плавающей точкой, целые числа и векторы чисел с плавающей точкой или целых чисел.

@floor

@floor(value: anytype) @TypeOf(value)

Возвращает наибольшее целое значение, не превышающее заданное число с плавающей точкой. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@ceil

@ceil(value: anytype) @TypeOf(value)

Возвращает наименьшее целое значение, не меньшее заданного числа с плавающей точкой. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@trunc

@trunc(value: anytype) @TypeOf(value)

Округляет заданное число с плавающей точкой до целого в сторону нуля. Если доступна, используется специальная аппаратная инструкция.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@round

@round(value: anytype) @TypeOf(value)

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

test_round_builtin.zig
const expectEqual = @import("std").testing.expectEqual;

test "@round" {
    try expectEqual(1, @round(1.4));
    try expectEqual(2, @round(1.5));
    try expectEqual(-1, @round(-1.4));
    try expectEqual(-3, @round(-2.5));
}
Командная строка
$ zig test test_round_builtin.zig
1/1 test_round_builtin.test.@round...OK
All 1 tests passed.

Поддерживает числа с плавающей точкой и векторы чисел с плавающей точкой.

@subWithOverflow

@subWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }

Выполняет a - b и возвращает кортеж с результатом и возможным битом переполнения.

@tagName

@tagName(value: anytype) [:0]const u8

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

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

@This

@This() type

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

test_this_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "@This()" {
    var items = [_]i32{ 1, 2, 3, 4 };
    const list = List(i32){ .items = items[0..] };
    try expectEqual(4, list.length());
}

fn List(comptime T: type) type {
    return struct {
        const Self = @This();

        items: []T,

        fn length(self: Self) usize {
            return self.items.len;
        }
    };
}
Командная строка
$ zig test test_this_builtin.zig
1/1 test_this_builtin.test.@This()...OK
All 1 tests passed.

При использовании @This() на уровне файла функция возвращает ссылку на структуру, соответствующую текущему файлу.

@trap

@trap() noreturn

Эта функция вставляет платформозависимую инструкцию trap/jam, которую можно использовать для аварийного завершения программы. Она может быть реализована явной генерацией недопустимой инструкции, которая может вызвать исключение недопустимой инструкции. В отличие от @breakpoint(), выполнение после этой точки не продолжается.

Вне области видимости функции эта встроенная функция вызывает ошибку компиляции.

См. также:

  • @breakpoint

@truncate

@truncate(integer: anytype) anytype

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

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

Вызов @truncate для числа, выходящего за диапазон целевого типа, является корректным и допустимым:

test_truncate_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "integer truncation" {
    const a: u16 = 0xabcd;
    const b: u8 = @truncate(a);
    try expectEqual(0xcd, b);
}
Командная строка
$ zig test test_truncate_builtin.zig
1/1 test_truncate_builtin.test.integer truncation...OK
All 1 tests passed.

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

@EnumLiteral

@EnumLiteral() type

Возвращает тип «литерал перечисления», доступный только во время компиляции. Это тип непреобразованных литералов перечисления. Значения этого типа могут быть приведены к любому перечислению с соответствующим полем.

@Int

@Int(comptime signedness: std.builtin.Signedness, comptime bits: u16) type

Возвращает целочисленный тип с заданными знаковостью и разрядностью.

Например, @Int(.unsigned, 18) возвращает тип u18.

@Tuple

@Tuple(comptime field_types: []const type) type

Возвращает тип кортежа с заданными типами полей.

@Pointer

@Pointer(
    comptime size: std.builtin.Type.Pointer.Size,
    comptime attrs: std.builtin.Type.Pointer.Attributes,
    comptime Element: type,
    comptime sentinel: ?Element,
) type

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

@Fn

@Fn(
    comptime param_types: []const type,
    comptime param_attrs: *const [param_types.len]std.builtin.Type.Fn.Param.Attributes,
    comptime ReturnType: type,
    comptime attrs: std.builtin.Type.Fn.Attributes,
) type

Возвращает тип функции со свойствами, указанными аргументами.

@Struct

@Struct(
    comptime layout: std.builtin.Type.ContainerLayout,
    comptime BackingInt: ?type,
    comptime field_names: []const []const u8,
    comptime field_types: *const [field_names.len]type,
    comptime field_attrs: *const [field_names.len]std.builtin.Type.StructField.Attributes,
) type

Возвращает тип структуры со свойствами, указанными аргументами.

@Union

@Union(
    comptime layout: std.builtin.Type.ContainerLayout,
    /// Either the integer tag type, or the integer backing type, depending on `layout`.
    comptime ArgType: ?type,
    comptime field_names: []const []const u8,
    comptime field_types: *const [field_names.len]type,
    comptime field_attrs: *const [field_names.len]std.builtin.Type.UnionField.Attributes,
) type

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

@Enum

@Enum(
    comptime TagInt: type,
    comptime mode: std.builtin.Type.Enum.Mode,
    comptime field_names: []const []const u8,
    comptime field_values: *const [field_names.len]TagInt,
) type

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

@typeInfo

@typeInfo(comptime T: type) std.builtin.Type

Предоставляет возможность отражения типов.

Информация о типах структур, объединений, перечислений и множеств ошибок содержит поля, порядок которых гарантированно совпадает с порядком их появления в исходном файле.

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

@typeName

@typeName(T: type) *const [N:0]u8

Эта функция возвращает строковое представление типа в виде массива. Оно эквивалентно строковому литералу с именем типа. Возвращаемое имя типа является полным: родительское пространство имён включается в имя типа, а части разделяются точками.

@TypeOf

@TypeOf(...) type

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

Выражения вычисляются, однако гарантируется отсутствие у них побочных эффектов во время выполнения:

test_TypeOf_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "no runtime side effects" {
    var data: i32 = 0;
    const T = @TypeOf(foo(i32, &data));
    try comptime expectEqual(i32, T);
    try expectEqual(0, data);
}

fn foo(comptime T: type, ptr: *T) T {
    ptr.* += 1;
    return ptr.*;
}
Командная строка
$ zig test test_TypeOf_builtin.zig
1/1 test_TypeOf_builtin.test.no runtime side effects...OK
All 1 tests passed.

@unionInit

@unionInit(comptime Union: type, comptime active_field_name: []const u8, init_expr) Union

Это то же самое, что и синтаксис инициализации объединения, за исключением того, что имя поля является значением, известным на этапе компиляции, а не токеном идентификатора.

@unionInit передаёт своё местоположение результата в init_expr.

@Vector

@Vector(len: comptime_int, Element: type) type

Создаёт векторы.

@volatileCast

@volatileCast(value: anytype) DestType

Удаляет квалификатор volatile у указателя.

@workGroupId

@workGroupId(comptime dimension: u32) u32

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

@workGroupSize

@workGroupSize(comptime dimension: u32) u32

Возвращает количество рабочих элементов в рабочей группе для измерения dimension.

@workItemId

@workItemId(comptime dimension: u32) u32

Возвращает индекс рабочего элемента в рабочей группе для измерения dimension. Эта функция возвращает значения от 0 (включительно) до @workGroupSize(dimension) (не включительно).

Режим сборки

В Zig есть четыре режима сборки:

  • Debug (по умолчанию)
  • ReleaseFast
  • ReleaseSafe
  • ReleaseSmall

Чтобы добавить стандартные параметры сборки в файл build.zig:

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "example",
        .root_module = b.createModule(.{
            .root_source_file = b.path("example.zig"),
            .optimize = optimize,
        }),
    });
    b.default_step.dependOn(&exe.step);
}

После этого станут доступны следующие параметры:

-Doptimize=Debug
Оптимизация отключена, проверки безопасности включены (по умолчанию)
-Doptimize=ReleaseSafe
Оптимизация и проверки безопасности включены
-Doptimize=ReleaseFast
Оптимизация включена, проверки безопасности отключены
-Doptimize=ReleaseSmall
Оптимизация размера включена, проверки безопасности отключены

Debug

Командная строка
$ zig build-exe example.zig
  • Быстрая компиляция
  • Проверки безопасности включены
  • Низкая производительность во время выполнения
  • Большой размер двоичного файла
  • Воспроизводимая сборка не требуется

ReleaseFast

Командная строка
$ zig build-exe example.zig -O ReleaseFast
  • Высокая производительность во время выполнения
  • Проверки безопасности отключены
  • Медленная компиляция
  • Большой размер двоичного файла
  • Воспроизводимая сборка

ReleaseSafe

Командная строка
$ zig build-exe example.zig -O ReleaseSafe
  • Средняя производительность во время выполнения
  • Проверки безопасности включены
  • Медленная компиляция
  • Большой размер двоичного файла
  • Воспроизводимая сборка

ReleaseSmall

Командная строка
$ zig build-exe example.zig -O ReleaseSmall
  • Средняя производительность во время выполнения
  • Проверки безопасности отключены
  • Медленная компиляция
  • Малый размер двоичного файла
  • Воспроизводимая сборка

См. также:

  • Переменные компиляции
  • Система сборки Zig
  • Недопустимое поведение

Однопоточные сборки

В Zig есть параметр компиляции -fsingle-threaded, который приводит к следующим последствиям:

  • Все переменные локального потока рассматриваются как обычные переменные уровня контейнера.
  • Накладные расходы асинхронных функций становятся эквивалентны накладным расходам вызова функции.
  • @import("builtin").single_threaded становится true, поэтому различные пользовательские API, читающие эту переменную, становятся более эффективными. Например, std.Mutex превращается в пустую структуру данных, а все её функции становятся пустыми операциями.

Недопустимое поведение

Многие операции в Zig вызывают то, что называется «недопустимым поведением» (IB). Если недопустимое поведение обнаружено во время компиляции, Zig выдаёт ошибку компиляции и отказывается продолжать работу. Если недопустимое поведение не обнаружено во время компиляции, оно относится к одной из двух категорий.

Некоторые виды недопустимого поведения проверяются на безопасность: это означает, что компилятор вставляет «проверки безопасности» во все места, где недопустимое поведение может возникнуть во время выполнения, чтобы определить, вот-вот ли оно произойдёт. Если это так, «проверка безопасности» завершается неудачно и вызывает панику.

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

Большинство видов недопустимого поведения проверяются на безопасность. Однако для оптимизации проверки безопасности по умолчанию отключены в режимах оптимизации ReleaseFast и ReleaseSmall. Проверки безопасности также можно включать или отключать для каждого блока отдельно, переопределяя значение по умолчанию для текущего режима оптимизации с помощью @setRuntimeSafety. Когда проверки безопасности отключены, проверяемое на безопасность недопустимое поведение ведёт себя как непроверяемое недопустимое поведение; то есть его вызов может привести к любому поведению.

При сбое проверки безопасности обработчик паники Zig по умолчанию завершает программу с трассировкой стека, например:

test_illegal_behavior.zig
test "safety check" {
    unreachable;
}
Командная строка
$ zig test test_illegal_behavior.zig
1/1 test_illegal_behavior.test.safety check...thread 926313 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_illegal_behavior.zig:2:5: 0x1234e2c in test.safety check (test_illegal_behavior.zig)
    unreachable;
    ^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/072ba49a7ed3af767b4ac6cae5d991d5/test --seed=0x56efd66b

Достижение недостижимого кода

Во время компиляции:

test_comptime_reaching_unreachable.zig
comptime {
    assert(false);
}
fn assert(ok: bool) void {
    if (!ok) unreachable; // assertion failure
}
Командная строка
$ zig test test_comptime_reaching_unreachable.zig
/home/andy/src/zig/doc/langref/test_comptime_reaching_unreachable.zig:5:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_comptime_reaching_unreachable.zig:2:11: note: called at comptime here
    assert(false);
    ~~~~~~^~~~~~~

Во время выполнения:

runtime_reaching_unreachable.zig
const std = @import("std");

pub fn main() void {
    std.debug.assert(false);
}
Командная строка
$ zig build-exe runtime_reaching_unreachable.zig
$ ./runtime_reaching_unreachable
thread 930209 panic: reached unreachable code
/home/andy/src/zig/lib/std/debug.zig:421:14: 0x1025eb9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/home/andy/src/zig/doc/langref/runtime_reaching_unreachable.zig:4:21: 0x11d57fe in main (runtime_reaching_unreachable.zig)
    std.debug.assert(false);
                    ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Индекс за пределами диапазона

Во время компиляции:

test_comptime_index_out_of_bounds.zig
comptime {
    const array: [5]u8 = "hello".*;
    const garbage = array[5];
    _ = garbage;
}
Командная строка
$ zig test test_comptime_index_out_of_bounds.zig
/home/andy/src/zig/doc/langref/test_comptime_index_out_of_bounds.zig:3:27: error: index 5 outside array of length 5
    const garbage = array[5];
                          ^

Во время выполнения:

runtime_index_out_of_bounds.zig
pub fn main() void {
    const x = foo("hello");
    _ = x;
}

fn foo(x: []const u8) u8 {
    return x[5];
}
Командная строка
$ zig build-exe runtime_index_out_of_bounds.zig
$ ./runtime_index_out_of_bounds
thread 931962 panic: index out of bounds: index 5, len 5
/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:7:13: 0x11d58de in foo (runtime_index_out_of_bounds.zig)
    return x[5];
            ^
/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:2:18: 0x11d580a in main (runtime_index_out_of_bounds.zig)
    const x = foo("hello");
                 ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Преобразование отрицательного числа в беззнаковое целое

Во время компиляции:

test_comptime_invalid_cast.zig
comptime {
    const value: i32 = -1;
    const unsigned: u32 = @intCast(value);
    _ = unsigned;
}
Командная строка
$ zig test test_comptime_invalid_cast.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_cast.zig:3:36: error: type 'u32' cannot represent integer value '-1'
    const unsigned: u32 = @intCast(value);
                                   ^~~~~

Во время выполнения:

runtime_invalid_cast.zig
const std = @import("std");

pub fn main() void {
    var value: i32 = -1; // runtime-known
    _ = &value;
    const unsigned: u32 = @intCast(value);
    std.debug.print("value: {}\n", .{unsigned});
}
Командная строка
$ zig build-exe runtime_invalid_cast.zig
$ ./runtime_invalid_cast
thread 933932 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/runtime_invalid_cast.zig:6:27: 0x11d580f in main (runtime_invalid_cast.zig)
    const unsigned: u32 = @intCast(value);
                          ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Чтобы получить максимальное значение беззнакового целого числа, используйте std.math.maxInt.

Преобразование с усечением данных

Во время компиляции:

test_comptime_invalid_cast_truncate.zig
comptime {
    const spartan_count: u16 = 300;
    const byte: u8 = @intCast(spartan_count);
    _ = byte;
}
Командная строка
$ zig test test_comptime_invalid_cast_truncate.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_cast_truncate.zig:3:31: error: type 'u8' cannot represent integer value '300'
    const byte: u8 = @intCast(spartan_count);
                              ^~~~~~~~~~~~~

Во время выполнения:

runtime_invalid_cast_truncate.zig
const std = @import("std");

pub fn main() void {
    var spartan_count: u16 = 300; // runtime-known
    _ = &spartan_count;
    const byte: u8 = @intCast(spartan_count);
    std.debug.print("value: {}\n", .{byte});
}
Командная строка
$ zig build-exe runtime_invalid_cast_truncate.zig
$ ./runtime_invalid_cast_truncate
thread 930996 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/runtime_invalid_cast_truncate.zig:6:22: 0x11d5810 in main (runtime_invalid_cast_truncate.zig)
    const byte: u8 = @intCast(spartan_count);
                     ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Для усечения битов используйте @truncate.

Переполнение целого числа

Операции по умолчанию

Следующие операторы могут вызвать переполнение целого числа:

  • + (сложение)
  • - (вычитание)
  • - (унарный минус)
  • * (умножение)
  • / (деление)
  • @divTrunc (деление)
  • @divFloor (деление)
  • @divExact (деление)

Пример сложения во время компиляции:

test_comptime_overflow.zig
comptime {
    var byte: u8 = 255;
    byte += 1;
}
Командная строка
$ zig test test_comptime_overflow.zig
/home/andy/src/zig/doc/langref/test_comptime_overflow.zig:3:10: error: overflow of integer type 'u8' with value '256'
    byte += 1;
    ~~~~~^~~~

Во время выполнения:

runtime_overflow.zig
const std = @import("std");

pub fn main() void {
    var byte: u8 = 255;
    byte += 1;
    std.debug.print("value: {}\n", .{byte});
}
Командная строка
$ zig build-exe runtime_overflow.zig
$ ./runtime_overflow
thread 934575 panic: integer overflow
/home/andy/src/zig/doc/langref/runtime_overflow.zig:5:10: 0x11d5825 in main (runtime_overflow.zig)
    byte += 1;
         ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Математические функции стандартной библиотеки

Эти функции стандартной библиотеки возвращают возможные ошибки.

  • @import("std").math.add
  • @import("std").math.sub
  • @import("std").math.mul
  • @import("std").math.divTrunc
  • @import("std").math.divFloor
  • @import("std").math.divExact
  • @import("std").math.shl

Пример обработки переполнения при сложении:

math_add.zig
const math = @import("std").math;
const print = @import("std").debug.print;
pub fn main() !void {
    var byte: u8 = 255;

    byte = if (math.add(u8, byte, 1)) |result| result else |err| {
        print("unable to add one: {s}\n", .{@errorName(err)});
        return err;
    };

    print("result: {}\n", .{byte});
}
Командная строка
$ zig build-exe math_add.zig
$ ./math_add
unable to add one: Overflow
error: Overflow
/home/andy/src/zig/lib/std/math.zig:565:21: 0x1139244 in add__anon_22508 (std.zig)
    if (ov[1] != 0) return error.Overflow;
                    ^
/home/andy/src/zig/doc/langref/math_add.zig:8:9: 0x11d4c44 in main (math_add.zig)
        return err;
        ^

Встроенные функции обработки переполнения

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

  • @addWithOverflow
  • @subWithOverflow
  • @mulWithOverflow
  • @shlWithOverflow

Пример использования @addWithOverflow:

addWithOverflow_builtin.zig
const print = @import("std").debug.print;
pub fn main() void {
    const byte: u8 = 255;

    const ov = @addWithOverflow(byte, 10);
    if (ov[1] != 0) {
        print("overflowed result: {}\n", .{ov[0]});
    } else {
        print("result: {}\n", .{ov[0]});
    }
}
Командная строка
$ zig build-exe addWithOverflow_builtin.zig
$ ./addWithOverflow_builtin
overflowed result: 9

Операции с циклическим переполнением

Для этих операций гарантирована семантика циклического переполнения.

  • +% (сложение с циклическим переполнением)
  • -% (вычитание с циклическим переполнением)
  • -% (унарный минус с циклическим переполнением)
  • *% (умножение с циклическим переполнением)
test_wraparound_semantics.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const minInt = std.math.minInt;
const maxInt = std.math.maxInt;

test "wraparound addition and subtraction" {
    const x: i32 = maxInt(i32);
    const min_val = x +% 1;
    try expectEqual(minInt(i32), min_val);
    const max_val = min_val -% 1;
    try expectEqual(maxInt(i32), max_val);
}
Командная строка
$ zig test test_wraparound_semantics.zig
1/1 test_wraparound_semantics.test.wraparound addition and subtraction...OK
All 1 tests passed.

Переполнение при точном сдвиге влево

Во время компиляции:

test_comptime_shlExact_overflow.zig
comptime {
    const x = @shlExact(@as(u8, 0b01010101), 2);
    _ = x;
}
Командная строка
$ zig test test_comptime_shlExact_overflow.zig
/home/andy/src/zig/doc/langref/test_comptime_shlExact_overflow.zig:2:15: error: overflow of integer type 'u8' with value '340'
    const x = @shlExact(@as(u8, 0b01010101), 2);
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Во время выполнения:

runtime_shlExact_overflow.zig
const std = @import("std");

pub fn main() void {
    var x: u8 = 0b01010101; // runtime-known
    _ = &x;
    const y = @shlExact(x, 2);
    std.debug.print("value: {}\n", .{y});
}
Командная строка
$ zig build-exe runtime_shlExact_overflow.zig
$ ./runtime_shlExact_overflow
thread 934568 panic: left shift overflowed bits
/home/andy/src/zig/doc/langref/runtime_shlExact_overflow.zig:6:5: 0x11d5831 in main (runtime_shlExact_overflow.zig)
    const y = @shlExact(x, 2);
    ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Переполнение при точном сдвиге вправо

Во время компиляции:

test_comptime_shrExact_overflow.zig
comptime {
    const x = @shrExact(@as(u8, 0b10101010), 2);
    _ = x;
}
Командная строка
$ zig test test_comptime_shrExact_overflow.zig
/home/andy/src/zig/doc/langref/test_comptime_shrExact_overflow.zig:2:15: error: exact shift shifted out 1 bits
    const x = @shrExact(@as(u8, 0b10101010), 2);
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Во время выполнения:

runtime_shrExact_overflow.zig
const builtin = @import("builtin");
const std = @import("std");

pub fn main() void {
    var x: u8 = 0b10101010; // runtime-known
    _ = &x;
    const y = @shrExact(x, 2);
    std.debug.print("value: {}\n", .{y});

    if ((builtin.cpu.arch.isPowerPC() or builtin.cpu.arch.isRISCV() or builtin.cpu.arch.isLoongArch() or builtin.cpu.arch == .s390x) and builtin.zig_backend == .stage2_llvm) @panic("https://github.com/ziglang/zig/issues/24304");
}
Командная строка
$ zig build-exe runtime_shrExact_overflow.zig
$ ./runtime_shrExact_overflow
thread 935584 panic: right shift overflowed bits
/home/andy/src/zig/doc/langref/runtime_shrExact_overflow.zig:7:5: 0x11d581a in main (runtime_shrExact_overflow.zig)
    const y = @shrExact(x, 2);
    ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Деление на ноль

Во время компиляции:

test_comptime_division_by_zero.zig
comptime {
    const a: i32 = 1;
    const b: i32 = 0;
    const c = a / b;
    _ = c;
}
Командная строка
$ zig test test_comptime_division_by_zero.zig
/home/andy/src/zig/doc/langref/test_comptime_division_by_zero.zig:4:19: error: division by zero here causes illegal behavior
    const c = a / b;
                  ^

Во время выполнения:

runtime_division_by_zero.zig
const std = @import("std");

pub fn main() void {
    var a: u32 = 1;
    var b: u32 = 0;
    _ = .{ &a, &b };
    const c = a / b;
    std.debug.print("value: {}\n", .{c});
}
Командная строка
$ zig build-exe runtime_division_by_zero.zig
$ ./runtime_division_by_zero
thread 928583 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_division_by_zero.zig:7:17: 0x11d5820 in main (runtime_division_by_zero.zig)
    const c = a / b;
                ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Вычисление остатка при делении на ноль

Во время компиляции:

test_comptime_remainder_division_by_zero.zig
comptime {
    const a: i32 = 10;
    const b: i32 = 0;
    const c = a % b;
    _ = c;
}
Командная строка
$ zig test test_comptime_remainder_division_by_zero.zig
/home/andy/src/zig/doc/langref/test_comptime_remainder_division_by_zero.zig:4:19: error: division by zero here causes illegal behavior
    const c = a % b;
                  ^

Во время выполнения:

runtime_remainder_division_by_zero.zig
const std = @import("std");

pub fn main() void {
    var a: u32 = 10;
    var b: u32 = 0;
    _ = .{ &a, &b };
    const c = a % b;
    std.debug.print("value: {}\n", .{c});
}
Командная строка
$ zig build-exe runtime_remainder_division_by_zero.zig
$ ./runtime_remainder_division_by_zero
thread 931002 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_remainder_division_by_zero.zig:7:17: 0x11d5820 in main (runtime_remainder_division_by_zero.zig)
    const c = a % b;
                ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

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

Во время компиляции:

test_comptime_divExact_remainder.zig
comptime {
    const a: u32 = 10;
    const b: u32 = 3;
    const c = @divExact(a, b);
    _ = c;
}
Командная оболочка
$ zig test test_comptime_divExact_remainder.zig
/home/andy/src/zig/doc/langref/test_comptime_divExact_remainder.zig:4:15: error: exact division produced remainder
    const c = @divExact(a, b);
              ^~~~~~~~~~~~~~~

Во время выполнения:

runtime_divExact_remainder.zig
const std = @import("std");

pub fn main() void {
    var a: u32 = 10;
    var b: u32 = 3;
    _ = .{ &a, &b };
    const c = @divExact(a, b);
    std.debug.print("value: {}\n", .{c});
}
Командная оболочка
$ zig build-exe runtime_divExact_remainder.zig
$ ./runtime_divExact_remainder
thread 930219 panic: exact division produced remainder
/home/andy/src/zig/doc/langref/runtime_divExact_remainder.zig:7:15: 0x11d5855 in main (runtime_divExact_remainder.zig)
    const c = @divExact(a, b);
              ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Попытка распаковать null

Во время компиляции:

test_comptime_unwrap_null.zig
comptime {
    const optional_number: ?i32 = null;
    const number = optional_number.?;
    _ = number;
}
Командная оболочка
$ zig test test_comptime_unwrap_null.zig
/home/andy/src/zig/doc/langref/test_comptime_unwrap_null.zig:3:35: error: unable to unwrap null
    const number = optional_number.?;
                   ~~~~~~~~~~~~~~~^~

Во время выполнения:

runtime_unwrap_null.zig
const std = @import("std");

pub fn main() void {
    var optional_number: ?i32 = null;
    _ = &optional_number;
    const number = optional_number.?;
    std.debug.print("value: {}\n", .{number});
}
Командная оболочка
$ zig build-exe runtime_unwrap_null.zig
$ ./runtime_unwrap_null
thread 930212 panic: attempt to use null value
/home/andy/src/zig/doc/langref/runtime_unwrap_null.zig:6:35: 0x11d5824 in main (runtime_unwrap_null.zig)
    const number = optional_number.?;
                                  ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Один из способов избежать этого сбоя — проверить, равно ли значение null, а не предполагать, что оно не равно null, используя выражение if:

testing_null_with_if.zig
const print = @import("std").debug.print;
pub fn main() void {
    const optional_number: ?i32 = null;

    if (optional_number) |number| {
        print("got number: {}\n", .{number});
    } else {
        print("it's null\n", .{});
    }
}
Командная оболочка
$ zig build-exe testing_null_with_if.zig
$ ./testing_null_with_if
it's null

См. также:

  • Опциональные типы

Попытка распаковать ошибку

Во время компиляции:

test_comptime_unwrap_error.zig
comptime {
    const number = getNumberOrFail() catch unreachable;
    _ = number;
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}
Командная оболочка
$ zig test test_comptime_unwrap_error.zig
/home/andy/src/zig/doc/langref/test_comptime_unwrap_error.zig:2:44: error: caught unexpected error 'UnableToReturnNumber'
    const number = getNumberOrFail() catch unreachable;
                                           ^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_comptime_unwrap_error.zig:7:18: note: error returned here
    return error.UnableToReturnNumber;
                 ^~~~~~~~~~~~~~~~~~~~

Во время выполнения:

runtime_unwrap_error.zig
const std = @import("std");

pub fn main() void {
    const number = getNumberOrFail() catch unreachable;
    std.debug.print("value: {}\n", .{number});
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}
Командная оболочка
$ zig build-exe runtime_unwrap_error.zig
$ ./runtime_unwrap_error
thread 935569 panic: attempt to unwrap error: UnableToReturnNumber
error return context:
/home/andy/src/zig/doc/langref/runtime_unwrap_error.zig:9:5: 0x11d57fc in getNumberOrFail (runtime_unwrap_error.zig)
    return error.UnableToReturnNumber;
    ^

stack trace:
/home/andy/src/zig/doc/langref/runtime_unwrap_error.zig:4:44: 0x11d5863 in main (runtime_unwrap_error.zig)
    const number = getNumberOrFail() catch unreachable;
                                           ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Один из способов избежать этого сбоя — проверить, произошла ли ошибка, а не предполагать, что результат успешен, используя выражение if:

testing_error_with_if.zig
const print = @import("std").debug.print;

pub fn main() void {
    const result = getNumberOrFail();

    if (result) |number| {
        print("got number: {}\n", .{number});
    } else |err| {
        print("got error: {s}\n", .{@errorName(err)});
    }
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}
Командная оболочка
$ zig build-exe testing_error_with_if.zig
$ ./testing_error_with_if
got error: UnableToReturnNumber

См. также:

  • Ошибки

Недопустимый код ошибки

Во время компиляции:

test_comptime_invalid_error_code.zig
comptime {
    _ = @errorFromInt(12345);
}
Командная оболочка
$ zig test test_comptime_invalid_error_code.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_error_code.zig:2:23: error: integer value '12345' represents no error
    _ = @errorFromInt(12345);
                      ^~~~~

Во время выполнения:

runtime_invalid_error_code.zig
const std = @import("std");

pub fn main() void {
    const err = error.AnError;
    var number = @intFromError(err) + 500;
    _ = &number;
    const invalid_err = @errorFromInt(number);
    std.debug.print("value: {}\n", .{invalid_err});
}
Командная оболочка
$ zig build-exe runtime_invalid_error_code.zig
$ ./runtime_invalid_error_code
thread 926286 panic: invalid error code
/home/andy/src/zig/doc/langref/runtime_invalid_error_code.zig:7:5: 0x11d5837 in main (runtime_invalid_error_code.zig)
    const invalid_err = @errorFromInt(number);
    ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Недопустимое преобразование перечисления

Во время компиляции:

test_comptime_invalid_enum_cast.zig
const Foo = enum {
    a,
    b,
    c,
};
comptime {
    const a: u2 = 3;
    const b: Foo = @enumFromInt(a);
    _ = b;
}
Командная оболочка
$ zig test test_comptime_invalid_enum_cast.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_enum_cast.zig:8:20: error: enum 'test_comptime_invalid_enum_cast.Foo' has no tag with value '3'
    const b: Foo = @enumFromInt(a);
                   ^~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_comptime_invalid_enum_cast.zig:1:13: note: enum declared here
const Foo = enum {
            ^~~~

Во время выполнения:

runtime_invalid_enum_cast.zig
const std = @import("std");

const Foo = enum {
    a,
    b,
    c,
};

pub fn main() void {
    var a: u2 = 3;
    _ = &a;
    const b: Foo = @enumFromInt(a);
    std.debug.print("value: {s}\n", .{@tagName(b)});
}
Командная оболочка
$ zig build-exe runtime_invalid_enum_cast.zig
$ ./runtime_invalid_enum_cast
thread 926295 panic: invalid enum value
/home/andy/src/zig/doc/langref/runtime_invalid_enum_cast.zig:12:20: 0x11d5880 in main (runtime_invalid_enum_cast.zig)
    const b: Foo = @enumFromInt(a);
                   ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Недопустимое преобразование набора ошибок

Во время компиляции:

test_comptime_invalid_error_set_cast.zig
const Set1 = error{
    A,
    B,
};
const Set2 = error{
    A,
    C,
};
comptime {
    _ = @as(Set2, @errorCast(Set1.B));
}
Командная оболочка
$ zig test test_comptime_invalid_error_set_cast.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_error_set_cast.zig:10:19: error: 'error.B' not a member of error set 'error{A,C}'
    _ = @as(Set2, @errorCast(Set1.B));
                  ^~~~~~~~~~~~~~~~~~

Во время выполнения:

runtime_invalid_error_set_cast.zig
const std = @import("std");

const Set1 = error{
    A,
    B,
};
const Set2 = error{
    A,
    C,
};
pub fn main() void {
    foo(Set1.B);
}
fn foo(set1: Set1) void {
    const x: Set2 = @errorCast(set1);
    std.debug.print("value: {}\n", .{x});
}
Командная оболочка
$ zig build-exe runtime_invalid_error_set_cast.zig
$ ./runtime_invalid_error_set_cast
thread 927253 panic: invalid error code
/home/andy/src/zig/doc/langref/runtime_invalid_error_set_cast.zig:15:21: 0x11d58fc in foo (runtime_invalid_error_set_cast.zig)
    const x: Set2 = @errorCast(set1);
                    ^
/home/andy/src/zig/doc/langref/runtime_invalid_error_set_cast.zig:12:8: 0x11d5807 in main (runtime_invalid_error_set_cast.zig)
    foo(Set1.B);
       ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Неправильное выравнивание указателя

Во время компиляции:

test_comptime_incorrect_pointer_alignment.zig
comptime {
    const ptr: *align(1) i32 = @ptrFromInt(0x1);
    const aligned: *align(4) i32 = @alignCast(ptr);
    _ = aligned;
}
Командная оболочка
$ zig test test_comptime_incorrect_pointer_alignment.zig
/home/andy/src/zig/doc/langref/test_comptime_incorrect_pointer_alignment.zig:3:47: error: pointer address 0x1 is not aligned to 4 bytes
    const aligned: *align(4) i32 = @alignCast(ptr);
                                              ^~~

Во время выполнения:

runtime_incorrect_pointer_alignment.zig
const mem = @import("std").mem;
pub fn main() !void {
    var array align(4) = [_]u32{ 0x11111111, 0x11111111 };
    const bytes = mem.sliceAsBytes(array[0..]);
    if (foo(bytes) != 0x11111111) return error.Wrong;
}
fn foo(bytes: []u8) u32 {
    const slice4 = bytes[1..5];
    const int_slice = mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
    return int_slice[0];
}
Командная оболочка
$ zig build-exe runtime_incorrect_pointer_alignment.zig
$ ./runtime_incorrect_pointer_alignment
thread 927819 panic: incorrect alignment
/home/andy/src/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:9:64: 0x11d6176 in foo (runtime_incorrect_pointer_alignment.zig)
    const int_slice = mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
                                                               ^
/home/andy/src/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:5:12: 0x11d4bde in main (runtime_incorrect_pointer_alignment.zig)
    if (foo(bytes) != 0x11111111) return error.Wrong;
           ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d51cc in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Неправильный доступ к полю объединения

Во время компиляции:

test_comptime_wrong_union_field_access.zig
comptime {
    var f = Foo{ .int = 42 };
    f.float = 12.34;
}

const Foo = union {
    float: f32,
    int: u32,
};
Командная оболочка
$ zig test test_comptime_wrong_union_field_access.zig
/home/andy/src/zig/doc/langref/test_comptime_wrong_union_field_access.zig:3:6: error: access of union field 'float' while field 'int' is active
    f.float = 12.34;
    ~^~~~~~
/home/andy/src/zig/doc/langref/test_comptime_wrong_union_field_access.zig:6:13: note: union declared here
const Foo = union {
            ^~~~~

Во время выполнения:

runtime_wrong_union_field_access.zig
const std = @import("std");

const Foo = union {
    float: f32,
    int: u32,
};

pub fn main() void {
    var f = Foo{ .int = 42 };
    bar(&f);
}

fn bar(f: *Foo) void {
    f.float = 12.34;
    std.debug.print("value: {}\n", .{f.float});
}
Командная оболочка
$ zig build-exe runtime_wrong_union_field_access.zig
$ ./runtime_wrong_union_field_access
thread 932701 panic: access of union field 'float' while field 'int' is active
/home/andy/src/zig/doc/langref/runtime_wrong_union_field_access.zig:14:6: 0x11d58ce in bar (runtime_wrong_union_field_access.zig)
    f.float = 12.34;
     ^
/home/andy/src/zig/doc/langref/runtime_wrong_union_field_access.zig:10:8: 0x11d580e in main (runtime_wrong_union_field_access.zig)
    bar(&f);
       ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Эта проверка безопасности недоступна для объединений extern или packed.

Чтобы изменить активное поле объединения, присвойте значение всему объединению, например:

change_active_union_field.zig
const std = @import("std");

const Foo = union {
    float: f32,
    int: u32,
};

pub fn main() void {
    var f = Foo{ .int = 42 };
    bar(&f);
}

fn bar(f: *Foo) void {
    f.* = Foo{ .float = 12.34 };
    std.debug.print("value: {}\n", .{f.float});
}
Командная оболочка
$ zig build-exe change_active_union_field.zig
$ ./change_active_union_field
value: 12.34

Чтобы изменить активное поле объединения, когда неизвестно подходящее значение для этого поля, используйте undefined, например:

undefined_active_union_field.zig
const std = @import("std");

const Foo = union {
    float: f32,
    int: u32,
};

pub fn main() void {
    var f = Foo{ .int = 42 };
    f = Foo{ .float = undefined };
    bar(&f);
    std.debug.print("value: {}\n", .{f.float});
}

fn bar(f: *Foo) void {
    f.float = 12.34;
}
Командная оболочка
$ zig build-exe undefined_active_union_field.zig
$ ./undefined_active_union_field
value: 12.34

См. также:

  • union
  • extern union

Преобразование числа с плавающей точкой в целое число вне допустимого диапазона

Это происходит при преобразовании числа с плавающей точкой в целое, если его значение выходит за пределы диапазона целочисленного типа.

Во время компиляции:

test_comptime_out_of_bounds_float_to_integer_cast.zig
comptime {
    const float: f32 = 4294967296;
    const int: i32 = @intFromFloat(float);
    _ = int;
}
Командная оболочка
$ zig test test_comptime_out_of_bounds_float_to_integer_cast.zig
/home/andy/src/zig/doc/langref/test_comptime_out_of_bounds_float_to_integer_cast.zig:3:36: error: float value '4294967296' cannot be stored in integer type 'i32'
    const int: i32 = @intFromFloat(float);
                                   ^~~~~

Во время выполнения:

runtime_out_of_bounds_float_to_integer_cast.zig
pub fn main() void {
    var float: f32 = 4294967296; // runtime-known
    _ = &float;
    const int: i32 = @intFromFloat(float);
    _ = int;
}
Командная оболочка
$ zig build-exe runtime_out_of_bounds_float_to_integer_cast.zig
$ ./runtime_out_of_bounds_float_to_integer_cast
thread 935571 panic: integer part of floating point value out of bounds
/home/andy/src/zig/doc/langref/runtime_out_of_bounds_float_to_integer_cast.zig:4:22: 0x11d583f in main (runtime_out_of_bounds_float_to_integer_cast.zig)
    const int: i32 = @intFromFloat(float);
                     ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Недопустимое преобразование указателя с нулевым адресом

Это происходит при преобразовании указателя с адресом 0 в указатель, которому адрес 0 недопустим. Например, указатели C, опциональные указатели и указатели allowzero допускают нулевой адрес, а обычные указатели — нет.

Во время компиляции:

test_comptime_invalid_null_pointer_cast.zig
comptime {
    const opt_ptr: ?*i32 = null;
    const ptr: *i32 = @ptrCast(opt_ptr);
    _ = ptr;
}
Командная оболочка
$ zig test test_comptime_invalid_null_pointer_cast.zig
/home/andy/src/zig/doc/langref/test_comptime_invalid_null_pointer_cast.zig:3:32: error: null pointer casted to type '*i32'
    const ptr: *i32 = @ptrCast(opt_ptr);
                               ^~~~~~~

Во время выполнения:

runtime_invalid_null_pointer_cast.zig
pub fn main() void {
    var opt_ptr: ?*i32 = null;
    _ = &opt_ptr;
    const ptr: *i32 = @ptrCast(opt_ptr);
    _ = ptr;
}
Командная оболочка
$ zig build-exe runtime_invalid_null_pointer_cast.zig
$ ./runtime_invalid_null_pointer_cast
thread 931960 panic: cast causes pointer to be null
/home/andy/src/zig/doc/langref/runtime_invalid_null_pointer_cast.zig:4:23: 0x11d581a in main (runtime_invalid_null_pointer_cast.zig)
    const ptr: *i32 = @ptrCast(opt_ptr);
                      ^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

Память

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

Где находятся байты?

Как и в Zig, в языке программирования C управление памятью выполняется вручную. Однако, в отличие от Zig, в C есть аллокатор по умолчанию — malloc, realloc и free. При компоновке с libc Zig предоставляет этот аллокатор через std.heap.c_allocator. Однако по соглашению в Zig нет аллокатора по умолчанию. Вместо этого функции, которым требуется выделение памяти, принимают параметр Allocator. Аналогично, некоторые структуры данных принимают параметр Allocator в функциях инициализации:

test_allocator.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
const expectEqualStrings = std.testing.expectEqualStrings;

test "using an allocator" {
    var buffer: [100]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();
    const result = try concat(allocator, "foo", "bar");
    try expectEqualStrings("foobar", result);
}

fn concat(allocator: Allocator, a: []const u8, b: []const u8) ![]u8 {
    const result = try allocator.alloc(u8, a.len + b.len);
    @memcpy(result[0..a.len], a);
    @memcpy(result[a.len..], b);
    return result;
}
Командная оболочка
$ zig test test_allocator.zig
1/1 test_allocator.test.using an allocator...OK
All 1 tests passed.

В приведённом выше примере для инициализации FixedBufferAllocator используется 100 байт стековой памяти, после чего он передаётся функции. Для удобства доступен глобальный FixedBufferAllocator для быстрых проверок в std.testing.allocator, который также выполняет базовое обнаружение утечек.

В Zig доступен аллокатор общего назначения, который можно импортировать с помощью std.heap.DebugAllocator. Однако рекомендуется следовать руководству Выбор аллокатора.

Выбор аллокатора

Выбор аллокатора зависит от ряда факторов. Ниже приведена блок-схема, которая поможет вам принять решение:

  1. Вы пишете библиотеку? В этом случае лучше принимать Allocator в качестве параметра и позволить пользователям вашей библиотеки самим выбирать аллокатор.
  2. Вы компонуете программу с libc? В этом случае std.heap.c_allocator, скорее всего, будет подходящим выбором, по крайней мере для основного аллокатора.
  3. Ограничено ли максимальное количество байтов, которое вам потребуется, числом, известным во время comptime? В этом случае используйте std.heap.FixedBufferAllocator.
  4. Является ли ваша программа приложением командной строки, которое выполняется от начала до конца без принципиального циклического шаблона (например, основного цикла видеоигры или обработчика запросов веб-сервера), так что имеет смысл освободить всю память сразу в конце? В этом случае рекомендуется использовать следующий подход:
    cli_allocation.zig
    const std = @import("std");
    
    pub fn main() !void {
        var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
        defer arena.deinit();
    
        const allocator = arena.allocator();
    
        const ptr = try allocator.create(i32);
        std.debug.print("ptr={*}\n", .{ptr});
    }
    Командная оболочка
    $ zig build-exe cli_allocation.zig
    $ ./cli_allocation
    ptr=i32@7ff19946e018
    
    При использовании аллокатора такого типа не нужно вручную освобождать память. Вся память освобождается одновременно при вызове arena.deinit().
  5. Относится ли выделение памяти к циклическому шаблону, например к основному циклу видеоигры или обработчику запросов веб-сервера? Если всю память можно освободить сразу в конце цикла — например, после полной отрисовки кадра видеоигры или обработки запроса веб-сервера, — то std.heap.ArenaAllocator станет отличным кандидатом. Как показано в предыдущем пункте, это позволяет освобождать целые арены памяти одновременно. Также учтите, что если можно установить верхнюю границу объёма памяти, то std.heap.FixedBufferAllocator можно использовать для дополнительной оптимизации.
  6. Вы пишете тест и хотите убедиться, что error.OutOfMemory обрабатывается правильно? В этом случае используйте std.testing.FailingAllocator.
  7. Вы пишете тест? В этом случае используйте std.testing.allocator.
  8. Наконец, если ни один из перечисленных вариантов не подходит, вам нужен аллокатор общего назначения. В режиме Debug доступен std.heap.DebugAllocator — функция, которая принимает comptime структуру с параметрами конфигурации и возвращает тип. Обычно вы создаёте ровно один такой аллокатор в главной функции, а затем передаёте его или подаллокаторы различным частям приложения.
  9. Если вы компилируете в режиме ReleaseFast, std.heap.smp_allocator — надёжный выбор аллокатора общего назначения.
  10. Также можно рассмотреть возможность реализации собственного аллокатора.

Где находятся байты?

Строковые литералы, такие как "hello", находятся в глобальной секции константных данных. Поэтому передавать строковый литерал изменяемому срезу нельзя — это ошибка, например:

test_string_literal_to_slice.zig
fn foo(s: []u8) void {
    _ = s;
}

test "string literal to mutable slice" {
    foo("hello");
}
Командная оболочка
$ zig test test_string_literal_to_slice.zig
/home/andy/src/zig/doc/langref/test_string_literal_to_slice.zig:6:9: error: expected type '[]u8', found '*const [5:0]u8'
    foo("hello");
        ^~~~~~~
/home/andy/src/zig/doc/langref/test_string_literal_to_slice.zig:6:9: note: cast discards const qualifier
/home/andy/src/zig/doc/langref/test_string_literal_to_slice.zig:1:11: note: parameter type declared here
fn foo(s: []u8) void {
          ^~~~

Однако если сделать срез константным, всё будет работать:

test_string_literal_to_const_slice.zig
fn foo(s: []const u8) void {
    _ = s;
}

test "string literal to constant slice" {
    foo("hello");
}
Командная оболочка
$ zig test test_string_literal_to_const_slice.zig
1/1 test_string_literal_to_const_slice.test.string literal to constant slice...OK
All 1 tests passed.

Как и строковые литералы, объявления const, если их значение известно во время comptime, хранятся в глобальной секции константных данных. В глобальной секции константных данных также хранятся переменные времени компиляции.

Объявления var внутри функций хранятся в стековом кадре функции. После возврата из функции любые указатели на переменные в её стековом кадре становятся недействительными ссылками, а их разыменование приводит к непроверяемому недопустимому поведению.

Объявления var на верхнем уровне или внутри объявлений struct хранятся в глобальной секции данных.

Место хранения памяти, выделенной с помощью allocator.alloc или allocator.create, определяется реализацией аллокатора.

TODO: переменные локального потока

Сбой выделения памяти в куче

Многие языки программирования при сбое выделения памяти в куче безусловно завершают работу с аварией. По соглашению программисты на Zig не считают это удовлетворительным решением. Вместо этого error.OutOfMemory обозначает сбой выделения памяти в куче, и библиотеки Zig возвращают этот код ошибки, если сбой выделения памяти в куче не позволил успешно завершить операцию.

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

  • Функция чрезмерного выделения памяти есть лишь в некоторых операционных системах.
    • В Linux она включена по умолчанию, но её можно настроить.
    • Windows не использует чрезмерное выделение памяти.
    • Во встраиваемых системах чрезмерного выделения памяти нет.
    • В любительских операционных системах чрезмерное выделение памяти может быть как доступно, так и недоступно.
  • В системах реального времени не только отсутствует чрезмерное выделение памяти, но и максимальный объём памяти для каждого приложения обычно определяется заранее.
  • Одна из главных целей создания библиотеки — повторное использование кода. Если библиотека правильно обрабатывает сбои выделения памяти, её можно использовать в большем числе ситуаций.
  • Хотя некоторые программы стали зависеть от включённого чрезмерного выделения памяти, его существование стало причиной бесчисленных катастроф с точки зрения удобства использования. Когда система с включённым чрезмерным выделением памяти, например Linux с настройками по умолчанию, почти исчерпывает память, она зависает и становится непригодной к использованию. В этот момент OOM Killer выбирает приложение для завершения на основе эвристик. Это недетерминированное решение часто приводит к завершению важного процесса и зачастую не позволяет вернуть систему к рабочему состоянию.

Рекурсия

Рекурсия — основополагающий инструмент моделирования в программном обеспечении. Однако у неё есть часто упускаемая из виду проблема: неограниченное выделение памяти.

Рекурсия — область активных экспериментов в Zig, поэтому приведённая здесь документация ещё не окончательна. Ознакомьтесь с кратким описанием состояния рекурсии в примечаниях к выпуску 0.3.0.

Если вкратце, сейчас рекурсия работает так, как и следовало ожидать. Хотя код Zig пока не защищён от переполнения стека, планируется, что будущая версия Zig обеспечит такую защиту, при этом от кода Zig потребуется определённая степень взаимодействия.

Время жизни и владение

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

Чтобы избежать ошибок при работе с указателями, полезно следовать нескольким соглашениям. Как правило, если функция возвращает указатель, её документация должна пояснять, кто является «владельцем» указателя. Это помогает программисту определить, когда следует освободить память, на которую он указывает, если такое освобождение вообще требуется.

Например, в документации функции может быть сказано: «возвращённая память принадлежит вызывающему коду». В таком случае код, вызывающий функцию, должен определить, когда освободить эту память. Вероятно, в такой ситуации функция принимает параметр Allocator.

Иногда время жизни указателя может быть сложнее. Например, срез std.ArrayList(T).items остаётся действительным до следующего изменения размера списка, например при добавлении новых элементов.

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

Переменные времени компиляции

Доступ к переменным времени компиляции можно получить, импортировав пакет "builtin", который компилятор делает доступным для каждого исходного файла Zig. Он содержит константы времени компиляции, такие как текущая целевая платформа, порядок байтов и режим выпуска.

compile_variables.zig
const builtin = @import("builtin");
const separator = if (builtin.os.tag == .windows) '\\' else '/';

Пример того, что импортируется с помощью @import("builtin"):

@import("builtin")
const std = @import("std");
/// Zig version. When writing code that supports multiple versions of Zig, prefer
/// feature detection (i.e. with `@hasDecl` or `@hasField`) over version checks.
pub const zig_version = std.SemanticVersion.parse(zig_version_string) catch unreachable;
pub const zig_version_string = "0.16.0-dev.3132+fd2718f82";
pub const zig_backend = std.builtin.CompilerBackend.stage2_x86_64;

pub const output_mode: std.builtin.OutputMode = .Exe;
pub const link_mode: std.builtin.LinkMode = .static;
pub const unwind_tables: std.builtin.UnwindTables = .async;
pub const is_test = false;
pub const single_threaded = false;
pub const abi: std.Target.Abi = .gnu;
pub const cpu: std.Target.Cpu = .{
    .arch = .x86_64,
    .model = &std.Target.x86.cpu.znver4,
    .features = std.Target.x86.featureSet(&.{
        .@"64bit",
        .adx,
        .aes,
        .allow_light_256_bit,
        .avx,
        .avx2,
        .avx512bf16,
        .avx512bitalg,
        .avx512bw,
        .avx512cd,
        .avx512dq,
        .avx512f,
        .avx512ifma,
        .avx512vbmi,
        .avx512vbmi2,
        .avx512vl,
        .avx512vnni,
        .avx512vpopcntdq,
        .bmi,
        .bmi2,
        .branchfusion,
        .clflushopt,
        .clwb,
        .clzero,
        .cmov,
        .crc32,
        .cx16,
        .cx8,
        .evex512,
        .f16c,
        .fast_15bytenop,
        .fast_bextr,
        .fast_dpwssd,
        .fast_imm16,
        .fast_lzcnt,
        .fast_movbe,
        .fast_scalar_fsqrt,
        .fast_scalar_shift_masks,
        .fast_variable_perlane_shuffle,
        .fast_vector_fsqrt,
        .fma,
        .fsgsbase,
        .fsrm,
        .fxsr,
        .gfni,
        .idivq_to_divl,
        .invpcid,
        .lzcnt,
        .macrofusion,
        .mmx,
        .movbe,
        .mwaitx,
        .nopl,
        .pclmul,
        .pku,
        .popcnt,
        .prfchw,
        .rdpid,
        .rdpru,
        .rdrnd,
        .rdseed,
        .sahf,
        .sbb_dep_breaking,
        .sha,
        .shstk,
        .smap,
        .smep,
        .sse,
        .sse2,
        .sse3,
        .sse4_1,
        .sse4_2,
        .sse4a,
        .ssse3,
        .vaes,
        .vpclmulqdq,
        .vzeroupper,
        .wbnoinvd,
        .x87,
        .xsave,
        .xsavec,
        .xsaveopt,
        .xsaves,
    }),
};
pub const os: std.Target.Os = .{
    .tag = .linux,
    .version_range = .{ .linux = .{
        .range = .{
            .min = .{
                .major = 6,
                .minor = 18,
                .patch = 9,
            },
            .max = .{
                .major = 6,
                .minor = 18,
                .patch = 9,
            },
        },
        .glibc = .{
            .major = 2,
            .minor = 39,
            .patch = 0,
        },
        .android = 29,
    }},
};
pub const target: std.Target = .{
    .cpu = cpu,
    .os = os,
    .abi = abi,
    .ofmt = object_format,
    .dynamic_linker = .init("/nix/store/vr7ds8vwbl2fz7pr221d5y0f8n9a5wda-glibc-2.40-218/lib/ld-linux-x86-64.so.2"),
};
pub const object_format: std.Target.ObjectFormat = .elf;
pub const mode: std.builtin.OptimizeMode = .Debug;
pub const link_libc = false;
pub const link_libcpp = false;
pub const have_error_return_tracing = true;
pub const valgrind_support = true;
pub const sanitize_thread = false;
pub const fuzz = false;
pub const position_independent_code = false;
pub const position_independent_executable = false;
pub const strip_debug_info = false;
pub const code_model: std.builtin.CodeModel = .default;
pub const omit_frame_pointer = false;

См. также:

  • Режим сборки

Модель компиляции

Компиляция Zig разделена на модули. Каждый модуль — это набор исходных файлов Zig, один из которых является корневым исходным файлом модуля. Каждый модуль может зависеть от любого количества других модулей, образуя ориентированный граф (циклические зависимости между модулями допускаются). Если модуль A зависит от модуля B, любой исходный файл Zig в модуле A может импортировать корневой исходный файл модуля B с помощью @import и имени модуля. По сути, модуль служит псевдонимом для импорта исходного файла Zig (который может находиться в совершенно другой части файловой системы).

Простая программа на Zig, скомпилированная с помощью zig build-exe, состоит из двух основных модулей: модуля с вашим кодом, известного как «главный» или «корневой», и стандартной библиотеки. Ваш модуль зависит от модуля стандартной библиотеки под именем «std», что позволяет писать @import("std")! Фактически каждый модуль в компиляции Zig — включая саму стандартную библиотеку — неявно зависит от модуля стандартной библиотеки под именем «std».

У «корневого модуля» (того, который вы указали в примере с zig build-exe) есть особое свойство. Как и стандартная библиотека, он неявно доступен всем модулям (включая себя), но уже под именем «root». Таким образом, @import("root") всегда будет эквивалентно @import вашего «главного» исходного файла (который часто, но не обязательно, называется main.zig).

Структуры исходных файлов

Каждый исходный файл Zig неявно является объявлением struct; можно представить, что содержимое файла буквально окружено struct { ... }. Это означает, что на верхнем уровне файла разрешены не только объявления, но и поля:

TopLevelFields.zig
//! Because this file contains fields, it is a type which is intended to be instantiated, and so
//! is named in TitleCase instead of snake_case by convention.

foo: u32,
bar: u64,

/// `@This()` can be used to refer to this struct type. In files with fields, it is quite common to
/// name the type here, so it can be easily referenced by other declarations in this file.
const TopLevelFields = @This();

pub fn init(val: u32) TopLevelFields {
    return .{
        .foo = val,
        .bar = val * 10,
    };
}

Такие файлы можно создавать так же, как и любые другие типы struct. На тип корневой структуры файла можно ссылаться внутри этого файла с помощью @This.

Обнаружение файлов и объявлений

В Zig важно, подвергается ли какая-либо часть кода семантическому анализу; по сути, заглядывает ли в неё компилятор. Анализируемый код определяется тем, какие файлы и объявления «обнаруживаются» из определённой точки. Процесс «обнаружения» основан на простом наборе рекурсивных правил:

  • Если анализируется вызов @import, анализируется импортируемый файл.
  • Если анализируется тип (включая файл), анализируются все объявления comptime и export внутри него.
  • Если анализируется тип (включая файл), компиляция выполняется для тестирования, а модуль, содержащий тип, является корневым модулем компиляции, то анализируются также все объявления test внутри него.
  • Если анализируется ссылка на именованное объявление (то есть его использование), анализируется объявление, на которое ссылаются. Порядок объявлений не имеет значения: ссылка может находиться до или после объявления, на которое она ссылается, и даже в другом файле.

Вот и всё! Эти правила определяют, как обнаруживаются файлы и объявления Zig. Осталось понять, с чего начинается этот процесс.

Ответ: с корня стандартной библиотеки. Каждая компиляция Zig начинается с анализа файла lib/std/std.zig. Этот файл содержит объявление comptime, которое импортирует lib/std/start.zig, а тот, в свою очередь, использует @import("root") для ссылки на «корневой модуль». Поэтому файл, указанный вами в качестве корневого исходного файла главного модуля, фактически тоже является корневым, поскольку стандартная библиотека всегда ссылается на него.

Часто требуется убедиться, что определённые объявления — особенно объявления test или export — обнаружены. С учётом приведённых выше правил для этого обычно используют @import внутри блока comptime или test:

force_file_discovery.zig
comptime {
    // This will ensure that the file 'api.zig' is always discovered (as long as this file is discovered).
    // It is useful if 'api.zig' contains important exported declarations.
    _ = @import("api.zig");

    // We could also have a file which contains declarations we only want to export depending on a comptime
    // condition. In that case, we can use an `if` statement here:
    if (builtin.os.tag == .windows) {
        _ = @import("windows_api.zig");
    }
}

test {
    // This will ensure that the file 'tests.zig' is always discovered (as long as this file is discovered),
    // if this compilation is a test. It is useful if 'tests.zig' contains tests we want to ensure are run.
    _ = @import("tests.zig");

    // We could also have a file which contains tests we only want to run depending on a comptime condition.
    // In that case, we can use an `if` statement here:
    if (builtin.os.tag == .windows) {
        _ = @import("windows_tests.zig");
    }
}

const builtin = @import("builtin");

Специальные корневые объявления

Поскольку корневой исходный файл корневого модуля всегда доступен через @import("root"), библиотеки, в том числе стандартная библиотека Zig, иногда используют его как место, где программа предоставляет этой библиотеке некоторую «глобальную» информацию. Стандартная библиотека Zig ищет в этом файле несколько объявлений.

Точка входа

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

В качестве альтернативы наличие объявления с именем _start (например, pub const _start = {};) отключит логику std.start по умолчанию, позволяя экспортировать из корневого исходного файла низкоуровневую точку входа по необходимости.

entry_point.zig
/// `std.start` imports this file using `@import("root")`, and uses this declaration as the program's
/// user-provided entry point. It can return any of the following types:
/// * `void`
/// * `E!void`, for any error set `E`
/// * `u8`
/// * `E!u8`, for any error set `E`
/// Returning a `void` value from this function will exit with code 0.
/// Returning a `u8` value from this function will exit with the given status code.
/// Returning an error value from this function will print an Error Return Trace and exit with code 1.
pub fn main() void {
    std.debug.print("Hello, World!\n", .{});
}

// If uncommented, this declaration would suppress the usual std.start logic, causing
// the `main` declaration above to be ignored.
//pub const _start = {};

const std = @import("std");
Командная оболочка
$ zig build-exe entry_point.zig
$ ./entry_point
Hello, World!

Если компиляция Zig выполняется с компоновкой с libc, функция main может быть объявлением export fn, сигнатура которого соответствует сигнатуре функции C main:

libc_export_entry_point.zig
pub export fn main(argc: c_int, argv: [*]const [*:0]const u8) c_int {
    const args = argv[0..@intCast(argc)];
    std.debug.print("Hello! argv[0] is '{s}'\n", .{args[0]});
    return 0;
}

const std = @import("std");
Командная оболочка
$ zig build-exe libc_export_entry_point.zig -lc
$ ./libc_export_entry_point
Hello! argv[0] is './libc_export_entry_point'

В определённых ситуациях std.start может использовать и другие объявления точек входа, например wWinMain или EfiMain. Подробности об этих объявлениях см. в логике lib/std/start.zig.

Параметры стандартной библиотеки

Стандартная библиотека также ищет в корневом исходном файле корневого модуля объявление с именем std_options. Если оно присутствует, ожидается, что это будет структура типа std.Options; она позволяет программе настраивать некоторые функции стандартной библиотеки, например реализацию std.log.

std_options.zig
/// The presence of this declaration allows the program to override certain behaviors of the standard library.
/// For a full list of available options, see the documentation for `std.Options`.
pub const std_options: std.Options = .{
    // By default, in safe build modes, the standard library will attach a segfault handler to the program to
    // print a helpful stack trace if a segmentation fault occurs. Here, we can disable this, or even enable
    // it in unsafe build modes.
    .enable_segfault_handler = true,
    // This is the logging function used by `std.log`.
    .logFn = myLogFn,
};

fn myLogFn(
    comptime level: std.log.Level,
    comptime scope: @EnumLiteral(),
    comptime format: []const u8,
    args: anytype,
) void {
    // We could do anything we want here!
    // ...but actually, let's just call the default implementation.
    std.log.defaultLog(level, scope, format, args);
}

const std = @import("std");

Обработчик паники

Стандартная библиотека Zig ищет в корневом исходном файле корневого модуля объявление с именем panic. Если оно присутствует, ожидается, что это будет пространство имён (тип-контейнер) с объявлениями, предоставляющими разные обработчики паники.

Базовую реализацию этого пространства имён см. в std.debug.simple_panic.

Переопределить способ вывода сообщений обработчиком паники, сохранив при этом форматированные сообщения о паниках безопасности, включённые по умолчанию, можно с помощью std.debug.FullPanic:

panic_handler.zig
pub fn main() void {
    @setRuntimeSafety(true);
    var x: u8 = 255;
    // Let's overflow this integer!
    x += 1;
}

pub const panic = std.debug.FullPanic(myPanic);

fn myPanic(msg: []const u8, first_trace_addr: ?usize) noreturn {
    _ = first_trace_addr;
    std.debug.print("Panic! {s}\n", .{msg});
    std.process.exit(1);
}

const std = @import("std");
Командная оболочка
$ zig build-exe panic_handler.zig
$ ./panic_handler
Panic! integer overflow

Система сборки Zig

Система сборки Zig предоставляет кроссплатформенный способ без зависимостей для описания логики, необходимой для сборки проекта. При использовании этой системы логика сборки проекта задаётся в файле build.zig с помощью API системы сборки Zig, предназначенного для объявления и настройки артефактов сборки и других задач.

Вот некоторые задачи, с которыми помогает система сборки:

  • Выполнение задач параллельно и кэширование результатов.
  • Зависимость от других проектов.
  • Предоставление пакета, от которого могут зависеть другие проекты.
  • Создание артефактов сборки с помощью компилятора Zig. Это включает сборку исходного кода Zig, а также исходного кода C и C++.
  • Получение настроенных пользователем параметров и их использование для настройки сборки.
  • Предоставление конфигурации сборки в виде значений comptime посредством файла, который можно импортировать в коде Zig.
  • Кэширование артефактов сборки, чтобы избежать ненужного повторения этапов.
  • Запуск артефактов сборки или установленных в системе инструментов.
  • Запуск тестов и проверка того, что вывод при выполнении артефакта сборки соответствует ожидаемому значению.
  • Запуск zig fmt для всей кодовой базы или её части.
  • Пользовательские задачи.

Чтобы воспользоваться системой сборки, выполните команду zig build --help, чтобы просмотреть справку по использованию командной строки. В ней будут указаны параметры конкретного проекта, объявленные в скрипте build.zig.

Пока что документация по системе сборки размещена на внешнем ресурсе: Документация по системе сборки

C

Хотя Zig не зависит от C и, в отличие от большинства других языков, не зависит от libc, Zig признаёт важность взаимодействия с существующим кодом на C.

Zig предоставляет несколько способов взаимодействия с C.

Примитивные типы C

Для этих типов гарантирована совместимость с ABI языка C, и их можно использовать как любые другие типы.

  • c_char
  • c_short
  • c_ushort
  • c_int
  • c_uint
  • c_long
  • c_ulong
  • c_longlong
  • c_ulonglong
  • c_longdouble

Для взаимодействия с типом C void используйте anyopaque.

См. также:

  • Примитивные типы

Импорт из заголовочного файла C

Встроенную функцию @cImport можно использовать для непосредственного импорта символов из файлов .h:

cImport_builtin.zig
const c = @cImport({
    // See https://github.com/ziglang/zig/issues/515
    @cDefine("_NO_CRT_STDIO_INLINE", "1");
    @cInclude("stdio.h");
});
pub fn main() void {
    if (@import("builtin").os.tag == .netbsd) return; // https://github.com/Vexu/arocc/issues/960
    _ = c.printf("hello\n");
}
Командная оболочка
$ zig build-exe cImport_builtin.zig -lc
$ ./cImport_builtin
hello

Функция @cImport принимает выражение в качестве параметра. Это выражение вычисляется во время компиляции и используется для управления директивами препроцессора и включения нескольких файлов .h:

Выражение @cImport
const builtin = @import("builtin");

const c = @cImport({
    @cDefine("NDEBUG", builtin.mode == .ReleaseFast);
    if (something) {
        @cDefine("_GNU_SOURCE", {});
    }
    @cInclude("stdlib.h");
    if (something) {
        @cUndef("_GNU_SOURCE");
    }
    @cInclude("soundio.h");
});

См. также:

  • @cImport
  • @cInclude
  • @cDefine
  • @cUndef
  • @import

Интерфейс командной строки для перевода C

Возможность перевода кода C в Zig доступна в виде инструмента командной строки zig translate-c. В качестве аргумента ему требуется одно имя файла. Также ему можно передать набор необязательных флагов, которые будут перенаправлены в clang. Переведённый файл выводится в stdout.

Флаги командной строки

  • -I: указать каталог для поиска включаемых файлов. Этот флаг можно использовать несколько раз. Эквивалент флага -I clang. Текущий каталог не включается по умолчанию; чтобы добавить его, используйте -I..
  • -D: определить макрос препроцессора. Эквивалент флага -D clang.
  • -cflags [flags] --: передать clang произвольные дополнительные флаги командной строки. Примечание: список флагов должен завершаться --
  • -target: целевая тройка для переведённого кода Zig. Если целевая платформа не указана, будет использоваться текущая целевая платформа хоста.

Использование -target и -cflags

Важно! При переводе кода C с помощью zig translate-c необходимо использовать тот же тройной код -target, который будет использоваться при компиляции переведённого кода. Кроме того, необходимо убедиться, что используемые флаги -cflags, если они есть, совпадают с флагами cflags, используемыми кодом в целевой системе. Неверные значения -target или -cflags могут привести к ошибкам разбора в clang или Zig либо к трудноуловимым несовместимостям ABI при компоновке с кодом C.

varytarget.h
long FOO = __LONG_MAX__;
Оболочка
$ zig translate-c -target thumb-freestanding-gnueabihf varytarget.h|grep FOO
pub export var FOO: c_long = 2147483647;
$ zig translate-c -target x86_64-macos-gnu varytarget.h|grep FOO
pub export var FOO: c_long = 9223372036854775807;
varycflags.h
enum FOO { BAR };
int do_something(enum FOO foo);
Оболочка
$ zig translate-c varycflags.h|grep -B1 do_something
pub const enum_FOO = c_uint;
pub extern fn do_something(foo: enum_FOO) c_int;
$ zig translate-c -cflags -fshort-enums -- varycflags.h|grep -B1 do_something
pub const enum_FOO = u8;
pub extern fn do_something(foo: enum_FOO) c_int;

@cImport и translate-c

@cImport и zig translate-c используют одну и ту же базовую функциональность перевода C, поэтому с технической точки зрения они эквивалентны. На практике @cImport удобно использовать для быстрого и простого доступа к числовым константам, typedef и типам записей без дополнительной настройки. Если нужно передать clang флаги cflags или отредактировать переведённый код, рекомендуется использовать zig translate-c и сохранить результат в файл. К распространённым причинам редактирования сгенерированного кода относятся: изменение параметров anytype в макросах, похожих на функции, на более конкретные типы; изменение указателей [*c]T на указатели [*]T или *T для повышения безопасности типов; а также включение или отключение проверки безопасности во время выполнения в отдельных функциях.

См. также:

  • Целевые платформы
  • Примитивные типы C
  • Указатели
  • Указатели C
  • Импорт из заголовочного файла C
  • @cInclude
  • @cImport
  • @setRuntimeSafety

Кэширование перевода C

Функция перевода C (как при использовании zig translate-c, так и @cImport) интегрирована с системой кэширования Zig. При последующих запусках с тем же исходным файлом, целевой платформой и флагами cflags будет использоваться кэш, а не повторно выполняться перевод того же кода.

Чтобы узнать, где хранятся кэшированные файлы при компиляции кода, использующего @cImport, используйте флаг --verbose-cimport:

verbose_cimport_flag.zig
const c = @cImport({
    @cDefine("_NO_CRT_STDIO_INLINE", "1");
    @cInclude("stdio.h");
});
pub fn main() void {
    if (@import("builtin").os.tag == .netbsd) return; // https://github.com/Vexu/arocc/issues/960
    _ = c;
}
Оболочка
$ zig build-exe verbose_cimport_flag.zig -lc --verbose-cimport
info(compilation): writing C import source to /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h
--zig-integration -x c -isystem /home/andy/src/zig/lib/compiler/aro/include -fno-PIE -fPIC -gdwarf-4 -gdwarf32 -fno-lto -MD -MV -MF /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h.d -fhosted -nostdinc -D__GLIBC_MINOR__=39 -isystem /home/andy/src/zig/lib/include -isystem /nix/store/1k8f0lkj10gsbb1fh0idr1423l37yj69-glibc-2.40-218-dev/include -O0 -target x86_64-linux.6.18.9...6.18.9-gnu.2.39 -mcpu=znver4-16bit_mode-32bit_mode-3dnow-3dnowa+64bit+adx+aes+allow_light_256_bit-amx_avx512-amx_bf16-amx_complex-amx_fp16-amx_fp8-amx_int8-amx_movrs-amx_tf32-amx_tile-amx_transpose+avx-avx10_1-avx10_2+avx2+avx512bf16+avx512bitalg+avx512bw+avx512cd+avx512dq-avx512er+avx512f-avx512fp16+avx512ifma-avx512pf+avx512vbmi+avx512vbmi2+avx512vl+avx512vnni-avx512vp2intersect+avx512vpopcntdq-avxifma-avxneconvert-avxvnni-avxvnniint16-avxvnniint8+bmi+bmi2-branch_hint+branchfusion-bsf_bsr_0_clobbers_result-ccmp-cf-cldemote+clflushopt+clwb+clzero+cmov-cmpccxadd+crc32+cx16+cx8-egpr-enqcmd-ermsb+evex512+f16c-false_deps_getmant-false_deps_lzcnt_tzcnt-false_deps_mulc-false_deps_mullq-false_deps_perm-false_deps_popcnt-false_deps_range-fast_11bytenop+fast_15bytenop-fast_7bytenop+fast_bextr+fast_dpwssd-fast_gather-fast_hops+fast_imm16+fast_lzcnt+fast_movbe+fast_scalar_fsqrt+fast_scalar_shift_masks-fast_shld_rotate-fast_variable_crosslane_shuffle+fast_variable_perlane_shuffle+fast_vector_fsqrt-fast_vector_shift_masks-faster_shift_than_shuffle+fma-fma4+fsgsbase+fsrm+fxsr+gfni-harden_sls_ijmp-harden_sls_ret-hreset-idivl_to_divb+idivq_to_divl-inline_asm_use_gpr32+invpcid-kl-lea_sp-lea_uses_ag-lvi_cfi-lvi_load_hardening-lwp+lzcnt+macrofusion+mmx+movbe-movdir64b-movdiri-movrs+mwaitx-ndd-nf-no_bypass_delay-no_bypass_delay_blend-no_bypass_delay_mov-no_bypass_delay_shuffle+nopl-pad_short_functions+pclmul-pconfig+pku+popcnt-ppx-prefer_128_bit-prefer_256_bit-prefer_mask_registers-prefer_movmsk_over_vtest-prefer_no_gather-prefer_no_scatter-prefetchi-prefetchwt1+prfchw-ptwrite-push2pop2-raoint+rdpid+rdpru+rdrnd+rdseed-retpoline-retpoline_external_thunk-retpoline_indirect_branches-retpoline_indirect_calls-rtm+sahf+sbb_dep_breaking-serialize-seses-sgx+sha-sha512+shstk-slow_3ops_lea-slow_incdec-slow_lea-slow_pmaddwd-slow_pmulld-slow_shld-slow_two_mem_ops-slow_unaligned_mem_16-slow_unaligned_mem_32-sm3-sm4+smap+smep-soft_float+sse+sse2+sse3+sse4_1+sse4_2+sse4a-sse_unaligned_mem+ssse3-tagged_globals-tbm-tsxldtrk-tuning_fast_imm_vector_shift-uintr-use_glm_div_sqrt_costs-use_slm_arith_costs-usermsr+vaes+vpclmulqdq+vzeroupper-waitpkg+wbnoinvd-widekl+x87-xop+xsave+xsavec+xsaveopt+xsaves-zu /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h -o /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.zig
info(compilation): processing dep file at /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h.d
info(compilation): renaming tmp/34a9bf8b6692601c to o/590f08cd17612e54b9a188a9dec151be
$ ./verbose_cimport_flag

cimport.h содержит файл для перевода (сформированный вызовами @cInclude, @cDefine и @cUndef), cimport.h.d — список файлов-зависимостей, а cimport.zig содержит результат перевода.

См. также:

  • Импорт из заголовочного файла C
  • Интерфейс командной строки для перевода C
  • @cInclude
  • @cImport

Ошибки перевода

Некоторые конструкции C невозможно перевести на Zig — например, goto, структуры с битовыми полями и макросы со склеиванием токенов. Zig использует понижение, чтобы продолжать перевод при наличии непереводимых сущностей.

Понижение бывает трёх видов: opaque, extern и @compileError. Структуры и объединения C, которые невозможно корректно перевести, будут преобразованы в opaque{}. Функции, содержащие непрозрачные типы или непереводимые конструкции кода, будут понижены до объявлений extern. Таким образом, непереводимые типы по-прежнему можно использовать как указатели, а непереводимые функции — вызывать, если компоновщику известна скомпилированная функция.

@compileError используется, когда определения верхнего уровня (глобальные переменные, прототипы функций, макросы) невозможно перевести или понизить. Поскольку Zig использует ленивый анализ объявлений верхнего уровня, непереводимые сущности не вызовут ошибку компиляции в вашем коде, пока вы действительно не используете их.

См. также:

  • opaque
  • extern
  • @compileError

Макросы C

Средство перевода C пытается по возможности преобразовать макросы, похожие на функции, в эквивалентные функции Zig. Поскольку макросы C работают на уровне лексических токенов, не все макросы C можно перевести на Zig. Макросы, которые невозможно перевести, будут понижены до @compileError. Обратите внимание: код C, который использует макросы, переводится без дополнительных проблем (поскольку Zig обрабатывает предварительно обработанный исходный код с раскрытыми макросами). Непереводимыми могут быть только сами макросы.

Рассмотрим следующий пример:

macro.c
#define MAKELOCAL(NAME, INIT) int NAME = INIT
int foo(void) {
   MAKELOCAL(a, 1);
   MAKELOCAL(b, 2);
   return a + b;
}
Оболочка
$ zig translate-c macro.c > macro.zig
macro.zig
pub export fn foo() c_int {
    var a: c_int = 1;
    _ = &a;
    var b: c_int = 2;
    _ = &b;
    return a + b;
}
pub const MAKELOCAL = @compileError("unable to translate C expr: unexpected token .Equal"); // macro.c:1:9

Обратите внимание, что foo был переведён корректно, несмотря на использование непереводимого макроса. MAKELOCAL был понижен до @compileError, поскольку его невозможно выразить как функцию Zig; это означает лишь, что вы не можете напрямую использовать MAKELOCAL из Zig.

См. также:

  • @compileError

Указатели C

По возможности следует избегать этого типа. Единственная обоснованная причина использовать указатель C — его наличие в автоматически сгенерированном коде при переводе кода C.

При импорте заголовочных файлов C неоднозначно, следует ли переводить указатели как указатели на один элемент (*T) или как указатели на множество элементов ([*]T). Указатели C — это компромисс, позволяющий коду Zig напрямую использовать переведённые заголовочные файлы.

[*c]T — указатель C.

  • Поддерживает весь синтаксис двух других типов указателей (*T) и ([*]T).
  • Преобразуется в другие типы указателей, а также в опциональные указатели. Если указатель C преобразуется в необязательный указатель, при нулевом адресе возникает проверяемое на безопасность недопустимое поведение.
  • Допускает нулевой адрес. На целевых платформах, отличных от freestanding, разыменование нулевого адреса является проверяемым на безопасность недопустимым поведением. Опциональные указатели C используют дополнительный бит для отслеживания значения null, как и ?usize. Обратите внимание, что создавать опциональный указатель C необязательно, поскольку можно использовать обычные опциональные указатели.
  • Поддерживает приведение типов к целым числам и обратно.
  • Поддерживает сравнение с целыми числами.
  • Не поддерживает атрибуты указателей, доступные только в Zig, например выравнивание. Используйте обычные указатели!

Если указатель C указывает на одну структуру (а не на массив), разыменуйте указатель C, чтобы получить доступ к полям или элементам данных структуры. Синтаксис выглядит так:

ptr_to_struct.*.struct_member

Это аналогично записи -> на C.

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

ptr_to_struct_array[index].struct_member

Вариативные функции C

Zig поддерживает вариативные функции extern.

test_variadic_function.zig
const std = @import("std");
const testing = std.testing;

pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;

test "variadic function" {
    try testing.expectEqual(14, printf("Hello, world!\n"));
    try testing.expect(@typeInfo(@TypeOf(printf)).@"fn".is_var_args);
}
Оболочка
$ zig test test_variadic_function.zig -lc
1/1 test_variadic_function.test.variadic function...OK
All 1 tests passed.
Hello, world!

Вариативные функции можно реализовать с помощью @cVaStart, @cVaEnd, @cVaArg и @cVaCopy.

test_defining_variadic_function.zig
const std = @import("std");
const testing = std.testing;
const builtin = @import("builtin");

fn add(count: c_int, ...) callconv(.c) c_int {
    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    var i: usize = 0;
    var sum: c_int = 0;
    while (i < count) : (i += 1) {
        sum += @cVaArg(&ap, c_int);
    }
    return sum;
}

test "defining a variadic function" {
    if (builtin.cpu.arch == .aarch64 and builtin.os.tag != .macos) {
        // https://github.com/ziglang/zig/issues/14096
        return error.SkipZigTest;
    }
    if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) {
        // https://github.com/ziglang/zig/issues/16961
        return error.SkipZigTest;
    }
    if (builtin.cpu.arch == .s390x) {
        // https://github.com/ziglang/zig/issues/21350#issuecomment-3543006475
        return error.SkipZigTest;
    }

    try std.testing.expectEqual(@as(c_int, 0), add(0));
    try std.testing.expectEqual(@as(c_int, 1), add(1, @as(c_int, 1)));
    try std.testing.expectEqual(@as(c_int, 3), add(2, @as(c_int, 1), @as(c_int, 2)));
}
Оболочка
$ zig test test_defining_variadic_function.zig
1/1 test_defining_variadic_function.test.defining a variadic function...OK
All 1 tests passed.

Экспорт библиотеки C

Один из основных сценариев использования Zig — экспорт библиотеки с ABI C, к которой могут обращаться другие языки программирования. Ключевое слово export перед функциями, переменными и типами включает их в API библиотеки:

mathtest.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}

Чтобы создать статическую библиотеку:

Оболочка
$ zig build-lib mathtest.zig

Чтобы создать динамическую библиотеку:

Оболочка
$ zig build-lib mathtest.zig -dynamic

Пример с использованием системы сборки Zig:

test.c
// This header is generated by zig from mathtest.zig
#include "mathtest.h"
#include <stdio.h>

int main(int argc, char **argv) {
    int32_t result = add(42, 1337);
    printf("%d\n", result);
    return 0;
}
build_c.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const lib = b.addLibrary(.{
        .linkage = .dynamic,
        .name = "mathtest",
        .root_module = b.createModule(.{
            .root_source_file = b.path("mathtest.zig"),
        }),
        .version = .{ .major = 1, .minor = 0, .patch = 0 },
    });
    const exe = b.addExecutable(.{
        .name = "test",
        .root_module = b.createModule(.{
            .link_libc = true,
        }),
    });
    exe.root_module.addCSourceFile(.{ .file = b.path("test.c"), .flags = &.{"-std=c99"} });
    exe.root_module.linkLibrary(lib);

    b.default_step.dependOn(&exe.step);

    const run_cmd = exe.run();

    const test_step = b.step("test", "Test the program");
    test_step.dependOn(&run_cmd.step);
}
Оболочка
$ zig build test
1379

См. также:

  • export

Объединение объектных файлов

Объектные файлы Zig можно объединять с любыми другими объектными файлами, совместимыми с ABI C. Пример:

base64.zig
const base64 = @import("std").base64;

export fn decode_base_64(
    dest_ptr: [*]u8,
    dest_len: usize,
    source_ptr: [*]const u8,
    source_len: usize,
) usize {
    const src = source_ptr[0..source_len];
    const dest = dest_ptr[0..dest_len];
    const base64_decoder = base64.standard.Decoder;
    const decoded_size = base64_decoder.calcSizeForSlice(src) catch unreachable;
    base64_decoder.decode(dest[0..decoded_size], src) catch unreachable;
    return decoded_size;
}
test.c
// This header is generated by zig from base64.zig
#include "base64.h"

#include <string.h>
#include <stdio.h>

int main(int argc, char **argv) {
    const char *encoded = "YWxsIHlvdXIgYmFzZSBhcmUgYmVsb25nIHRvIHVz";
    char buf[200];

    size_t len = decode_base_64(buf, 200, encoded, strlen(encoded));
    buf[len] = 0;
    puts(buf);

    return 0;
}
build_object.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const obj = b.addObject(.{
        .name = "base64",
        .root_module = b.createModule(.{
            .root_source_file = b.path("base64.zig"),
        }),
    });

    const exe = b.addExecutable(.{
        .name = "test",
        .root_module = b.createModule(.{
            .link_libc = true,
        }),
    });
    exe.root_module.addCSourceFile(.{ .file = b.path("test.c"), .flags = &.{"-std=c99"} });
    exe.root_module.addObject(obj);
    b.installArtifact(exe);
}
Оболочка
$ zig build
$ ./zig-out/bin/test
all your base are belong to us

См. также:

  • Целевые платформы
  • Система сборки Zig

WebAssembly

Zig поддерживает сборку для WebAssembly без дополнительной настройки.

Freestanding

Для хост-сред, таких как веб-браузер и nodejs, собирайте исполняемый файл, указав целевую ОС freestanding. Ниже приведён пример запуска кода Zig, скомпилированного в WebAssembly, с помощью nodejs.

math.zig
extern fn print(i32) void;

export fn add(a: i32, b: i32) void {
    print(a + b);
}
Оболочка
$ zig build-exe math.zig -target wasm32-freestanding -fno-entry --export=add
test.js
const fs = require('fs');
const source = fs.readFileSync("./math.wasm");
const typedArray = new Uint8Array(source);

WebAssembly.instantiate(typedArray, {
  env: {
    print: (result) => { console.log(`The result is ${result}`); }
  }}).then(result => {
  const add = result.instance.exports.add;
  add(1, 2);
});
Оболочка
$ node test.js
The result is 3

WASI

Стандартная библиотека Zig имеет полноценную поддержку WebAssembly System Interface.

wasi_args.zig
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    for (0.., args) |i, arg| {
        std.debug.print("{d}: {s}\n", .{ i, arg });
    }
}
Оболочка
$ zig build-exe wasi_args.zig -target wasm32-wasi
Оболочка
$ wasmtime wasi_args.wasm 123 hello
0: wasi_args.wasm
1: 123
2: hello

Более интересный пример — получение из среды выполнения списка предварительно открытых каталогов. Теперь стандартная библиотека поддерживает это с помощью std.fs.wasi.Preopens:

wasi_preopens.zig
const std = @import("std");

pub fn main(init: std.process.Init) void {
    for (init.preopens.map.keys(), 0..) |preopen, i| {
        std.log.info("{d}: {s}", .{ i, preopen });
    }
}
Оболочка
$ zig build-exe wasi_preopens.zig -target wasm32-wasi
Оболочка
$ wasmtime --dir=. wasi_preopens.wasm
0: stdin
1: stdout
2: stderr
3: .

Целевые платформы

Целевая платформа — это компьютер, на котором будет запускаться исполняемый файл. Она определяется архитектурой ЦП, набором включённых функций ЦП, операционной системой, минимальной и максимальной версиями операционной системы, ABI и версией ABI.

Zig — язык программирования общего назначения, то есть он предназначен для создания оптимального кода для широкого набора целевых платформ. Команда zig targets выводит сведения обо всех целевых платформах, известных компилятору.

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

Стандартная библиотека Zig (@import("std")) содержит кроссплатформенные абстракции, благодаря которым один и тот же исходный код можно использовать на многих целевых платформах. Одни фрагменты кода более переносимы, чем другие. В целом код Zig чрезвычайно переносим по сравнению с кодом на других языках программирования.

Для работы кроссплатформенных абстракций Zig на каждой платформе требуются собственные реализации. Эти реализации находятся на разных стадиях готовности. Каждый помеченный выпуск компилятора сопровождается примечаниями к выпуску с полной таблицей поддержки целевых платформ.

Руководство по стилю

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

Избегайте избыточности в именах

Избегайте следующих слов в именах типов:

  • Value
  • Data
  • Context
  • Manager
  • State
  • utils, misc или чьи-либо инициалы

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

Желание использовать слова «utilities», «miscellaneous» или чьи-либо инициалы говорит о неспособности классифицировать сущности или, что бывает чаще, о чрезмерной классификации. Такие объявления могут находиться в корне нуждающегося в них модуля, без отдельного пространства имён.

Избегайте избыточных имён в полных пространствах имён

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

redundant_fqn.zig
const std = @import("std");

pub const json = struct {
    pub const JsonValue = union(enum) {
        number: f64,
        boolean: bool,
        // ...
    };
};

pub fn main() void {
    std.debug.print("{s}\n", .{@typeName(json.JsonValue)});
}
Оболочка
$ zig build-exe redundant_fqn.zig
$ ./redundant_fqn
redundant_fqn.json.JsonValue

В этом примере «json» повторяется в полном пространстве имён. Решение — удалить Json из JsonValue. В этом примере у нас есть пустая структура с именем json, однако помните, что файлы также являются частью полного пространства имён.

Этот пример представляет собой исключение из правила, описанного в разделе Избегайте избыточности в именах. Значение типа сведено к его сути: это значение json. Более конкретное имя было бы неверным.

Не используйте подчёркивания в начале имён

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

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

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

Подчёркивание не помогает объяснить различие между двумя одинаковыми во всём остальном именами. Если их невозможно перепутать, это руководство рекомендует использовать более развёрнутые имена во внешних областях видимости и более краткие — во внутренних.

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

Пробельные символы

  • Отступ — 4 пробела
  • Открывающие фигурные скобки размещайте в той же строке, если только не требуется перенос.
  • Если список содержит более двух элементов, размещайте каждый элемент на отдельной строке и не забывайте, что в конце можно поставить дополнительную запятую.
  • Длина строки: стремитесь к 100 символам; руководствуйтесь здравым смыслом.

Имена

В общих чертах: camelCaseFunctionName, TitleCaseTypeName, snake_case_variable_name. Точнее:

  • Если x — это struct без полей, которое никогда не предполагается создавать, то x считается «пространством имён» и должно быть snake_case.
  • Если x — это type или псевдоним type, то x должно быть TitleCase.
  • Если x можно вызвать, а возвращаемый тип x — type, то x должно быть TitleCase.
  • Если x можно вызвать и при этом не выполняется предыдущее условие, то x должно быть camelCase.
  • В противном случае x должно быть snake_case.

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

Имена файлов делятся на две категории: типы и пространства имён. Если файл (неявно являющийся структурой) содержит поля верхнего уровня, его следует именовать как любую другую структуру с полями, используя TitleCase. В противном случае следует использовать snake_case. Имена каталогов должны соответствовать snake_case.

Это общие рекомендации; если имеет смысл поступить иначе, поступайте так, как имеет смысл. Например, если существует устоявшееся соглашение, такое как ENOENT, следуйте ему.

Примеры

style_example.zig
const namespace_name = @import("dir_name/file_name.zig");
const TypeName = @import("dir_name/TypeName.zig");
var global_var: i32 = undefined;
const const_name = 42;
const PrimitiveTypeAlias = f32;

const StructName = struct {
    field: i32,
};
const StructAlias = StructName;

fn functionName(param_name: TypeName) void {
    var functionPointer = functionName;
    functionPointer();
    functionPointer = otherFunction;
    functionPointer();
}
const functionAlias = functionName;

fn ListTemplateFunction(comptime ChildType: type, comptime fixed_size: usize) type {
    return List(ChildType, fixed_size);
}

fn ShortList(comptime T: type, comptime n: usize) type {
    return struct {
        field_name: [n]T,
        fn methodName() void {}
    };
}

// The word XML loses its casing when used in Zig identifiers.
const xml_document =
    \\<?xml version="1.0" encoding="UTF-8"?>
    \\<document>
    \\</document>
;
const XmlParser = struct {
    field: i32,
};

// The initials BE (Big Endian) are just another word in Zig identifier names.
fn readU32Be() u32 {}

Другие примеры см. в стандартной библиотеке Zig.

Рекомендации по комментариям документации

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

Кодировка исходного кода

Исходный код Zig кодируется в UTF-8. Некорректная последовательность байтов UTF-8 приводит к ошибке компиляции.

Во всём исходном коде Zig (включая комментарии) запрещены некоторые кодовые точки:

  • Управляющие символы ASCII, кроме U+000a (LF), U+000d (CR) и U+0009 (HT): U+0000 — U+0008, U+000b — U+000c, U+000e — U+001f, U+007f.
  • Не-ASCII символы Unicode, обозначающие конец строки: U+0085 (NEL), U+2028 (LS), U+2029 (PS).

LF (значение байта 0x0a, кодовая точка U+000a, '\n') — это символ конца строки в исходном коде Zig. Это значение байта завершает каждую строку исходного кода Zig, кроме последней строки файла. Рекомендуется завершать непустые исходные файлы пустой строкой, то есть последним байтом должен быть 0x0a (LF).

Перед каждым LF может непосредственно стоять один CR (значение байта 0x0d, кодовая точка U+000d, '\r'), образуя окончание строки в стиле Windows, однако это не рекомендуется. Обратите внимание: в многострочных строках последовательности CRLF при компиляции в программу Zig будут преобразованы в LF. В любом другом контексте CR запрещён.

Жёсткие табуляции HT (значение байта 0x09, кодовая точка U+0009, '\t') можно использовать вместо пробелов SP (значение байта 0x20, кодовая точка U+0020, ' ') в качестве разделителей токенов, однако использование жёстких табуляций не рекомендуется. См. раздел Грамматика.

Для совместимости с другими инструментами компилятор игнорирует метку порядка байтов, закодированную в UTF-8 (U+FEFF), если она является первой кодовой точкой Unicode в исходном тексте. В других местах исходного кода метка порядка байтов запрещена.

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

Учтите, что инструмент, читающий исходный код Zig, может делать предположения, если считает его корректным кодом Zig. Например, при определении концов строк инструмент может использовать простой поиск, например /\n/, или расширенный поиск, например /\r\n?|[\n\u0085\u2028\u2029]/; в обоих случаях концы строк будут определены правильно. Другой пример: при определении пробельных символов перед первым токеном в строке инструмент может использовать простой поиск, например /[ \t]/, или расширенный поиск, например /\s/; в обоих случаях пробельные символы будут определены правильно.

Справочник по ключевым словам

Ключевое слово Описание
addrspace
Ключевое слово addrspace.
  • TODO: добавить документацию для addrspace
align
align можно использовать для указания выравнивания указателя. Его также можно использовать после объявления переменной или функции, чтобы указать выравнивание указателей на эту переменную или функцию.
  • См. также Выравнивание
allowzero
Атрибут указателя allowzero позволяет указателю иметь нулевой адрес.
  • См. также allowzero
and
Булев оператор and.
  • См. также Операторы
anyframe
anyframe можно использовать как тип переменных, хранящих указатели на кадры функций.
  • См. также Асинхронные функции
anytype
Параметры функции можно объявить с помощью anytype вместо типа. Тип будет выведен в месте вызова функции.
  • См. также Выведение типа параметра функции
asm
asm начинает выражение встроенного ассемблера. Это позволяет напрямую управлять машинным кодом, генерируемым при компиляции.
  • См. также Ассемблер
break
break можно использовать с меткой блока, чтобы вернуть значение из блока. Его также можно использовать для выхода из цикла до его естественного завершения.
  • См. также Блоки, while, for
callconv
callconv можно использовать для указания соглашения о вызовах в типе функции.
  • См. также Функции
catch
catch можно использовать для вычисления выражения, если предшествующее ему выражение вычисляется с ошибкой. Выражение после catch может захватывать значение ошибки.
  • См. также catch, Операторы
comptime
comptime перед объявлением можно использовать, чтобы пометить переменные или параметры функции как известные во время компиляции. Его также можно использовать, чтобы гарантировать вычисление выражения во время компиляции.
  • См. также comptime
const
const объявляет переменную, которую нельзя изменить. В качестве атрибута указателя он указывает, что значение, на которое ссылается указатель, нельзя изменить.
  • См. также Переменные
continue
continue можно использовать в цикле, чтобы перейти к его началу.
  • См. также while, for
defer
defer выполнит выражение при выходе потока управления из текущего блока.
  • См. также defer
else
else можно использовать для задания альтернативной ветви выражений if, switch, while и for.
  • Если оно используется после выражения if, ветвь else будет выполнена, если проверяемое значение равно false, null или является ошибкой.
  • Если оно используется внутри выражения switch, ветвь else будет выполнена, если проверяемое значение не соответствует ни одному другому варианту.
  • Если оно используется после выражения цикла, ветвь else будет выполнена, если цикл завершится без выхода через break.
  • См. также if, switch, while, for
enum
enum определяет тип перечисления.
  • См. также enum
errdefer
errdefer выполнит выражение при выходе потока управления из текущего блока, если функция возвращает ошибку; выражение errdefer может захватить извлеченное значение.
  • См. также errdefer
error
error определяет тип ошибки.
  • См. также Ошибки
export
export делает функцию или переменную внешне видимой в сгенерированном объектном файле. Для экспортируемых функций по умолчанию используется соглашение о вызовах C.
  • См. также Функции
extern
extern можно использовать для объявления функции или переменной, ссылка на которую будет разрешена во время компоновки при статической компоновке или во время выполнения при динамической компоновке.
  • См. также Функции
fn
fn объявляет функцию.
  • См. также Функции
for
Выражение for можно использовать для перебора элементов среза, массива или кортежа.
  • См. также for
if
Выражение if может проверять булевы выражения, опциональные значения или объединения ошибок. Для опциональных значений и объединений ошибок выражение if может захватывать извлеченное значение.
  • См. также if
inline
inline можно использовать для пометки выражения цикла, чтобы развернуть его во время компиляции. Его также можно использовать, чтобы принудительно встроить функцию во все места вызова.
  • См. также inline while, inline for, Функции
linksection
Ключевое слово linksection можно использовать, чтобы указать секцию, в которую будет помещена функция или глобальная переменная (например, .text).
noalias
Ключевое слово noalias.
  • TODO: добавить документацию для noalias
noinline
noinline запрещает встраивать функцию во все места вызова.
  • См. также Функции
nosuspend
Ключевое слово nosuspend можно использовать перед блоком, оператором или выражением, чтобы обозначить область, в которой не достигаются точки приостановки. В частности, внутри области nosuspend:
  • Использование ключевого слова suspend приводит к ошибке компиляции.
  • Использование await для кадра функции, выполнение которой еще не завершено, приводит к поведению, проверяемому на безопасность, — недопустимому поведению.
  • Вызов асинхронной функции может привести к поведению, проверяемому на безопасность, — недопустимому поведению, поскольку он эквивалентен await async some_async_fn(), содержащему await.
Код внутри области nosuspend не приводит к тому, что охватывающая функция становится асинхронной функцией.
  • См. также Асинхронные функции
opaque
opaque определяет непрозрачный тип.
  • См. также opaque
or
Булев оператор or.
  • См. также Операторы
orelse
orelse можно использовать для вычисления выражения, если предшествующее ему выражение вычисляется в null.
  • См. также Опциональные типы, Операторы
packed
Ключевое слово packed перед определением структуры меняет расположение структуры в памяти на гарантированное расположение packed.
  • См. также packed struct
pub
pub перед объявлением верхнего уровня делает это объявление доступным для использования из другого файла.
  • См. также import
resume
resume продолжит выполнение кадра функции с того места, где функция была приостановлена.
return
return завершает функцию, возвращая значение.
  • См. также Функции
struct
struct определяет структуру.
  • См. также struct
suspend
suspend приводит к возврату потока управления в место вызова или возобновления функции. suspend также можно использовать перед блоком внутри функции, чтобы предоставить функции доступ к ее кадру до возврата потока управления в место вызова.
switch
Выражение switch можно использовать для проверки значений общего типа. Варианты switch могут захватывать значения полей помеченного объединения.
  • См. также switch
test
Ключевое слово test можно использовать для обозначения блока кода верхнего уровня, который гарантирует соответствие поведения ожиданиям.
  • См. также Тестирование в Zig
threadlocal
threadlocal можно использовать, чтобы объявить переменную локальной для потока.
  • См. также Переменные, локальные для потока
try
try вычисляет выражение объединения ошибок. Если результатом является ошибка, функция возвращает эту же ошибку. В противном случае результатом выражения становится извлеченное значение.
  • См. также try
union
union определяет объединение.
  • См. также union
unreachable
unreachable можно использовать, чтобы утверждать, что поток управления никогда не достигнет определенного места. В зависимости от режима сборки unreachable может вызвать панику.
  • Вызывает панику в режимах Debug и ReleaseSafe, а также при использовании zig test.
  • Не вызывает панику в режимах ReleaseFast и ReleaseSmall.
  • См. также unreachable
var
var объявляет переменную, которую можно изменять.
  • См. также Переменные
volatile
volatile можно использовать, чтобы указать, что загрузка или сохранение через указатель имеет побочные эффекты. Он также может изменить выражение встроенного ассемблера, обозначив наличие у него побочных эффектов.
  • См. также volatile, Ассемблер
while
Выражение while можно использовать для многократной проверки булева выражения, опционального значения или выражения объединения ошибок и прекращения цикла, когда это выражение соответственно принимает значение false, null или ошибку.
  • См. также while

Приложение

Контейнеры

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

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

Грамматика

grammar.peg
Root <- skip ContainerMembers eof

# *** Top level ***
ContainerMembers <- container_doc_comment? ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*)

ContainerDeclaration <- TestDecl / ComptimeDecl / doc_comment? KEYWORD_pub? Decl

TestDecl <- KEYWORD_test (STRINGLITERALSINGLE / IDENTIFIER)? Block

ComptimeDecl <- KEYWORD_comptime Block

Decl
    <- (KEYWORD_export / KEYWORD_inline / KEYWORD_noinline)? FnProto (SEMICOLON / Block)
     / KEYWORD_extern STRINGLITERALSINGLE? FnProto SEMICOLON
     / (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? GlobalVarDecl

FnProto <- KEYWORD_fn IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr !ExprSuffix

VarDeclProto <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection?

GlobalVarDecl <- VarDeclProto (EQUAL Expr)? SEMICOLON

ContainerField <- doc_comment? (KEYWORD_comptime / !KEYWORD_comptime) !KEYWORD_fn (IDENTIFIER COLON / !(IDENTIFIER COLON))? TypeExpr ByteAlign? (EQUAL Expr)?

# *** Block Level ***
BlockStatement
    <- Statement
     / KEYWORD_defer BlockExprStatement
     / KEYWORD_errdefer Payload? BlockExprStatement
     / !ExprStatement (KEYWORD_comptime !BlockExpr)? VarAssignStatement

Statement
    <- ExprStatement
     / KEYWORD_suspend BlockExprStatement
     / !ExprStatement (KEYWORD_comptime !BlockExpr)? AssignExpr SEMICOLON

ExprStatement
    <- IfStatement
     / LabeledStatement
     / KEYWORD_nosuspend BlockExprStatement
     / KEYWORD_comptime BlockExpr

IfStatement
    <- IfPrefix BlockExpr ( KEYWORD_else Payload? Statement )?
     / IfPrefix !BlockExpr AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )

LabeledStatement <- BlockLabel? (Block / LoopStatement / SwitchExpr)

LoopStatement <- KEYWORD_inline? (ForStatement / WhileStatement)

ForStatement
    <- ForPrefix BlockExpr ( KEYWORD_else Statement / !KEYWORD_else )
     / ForPrefix !BlockExpr AssignExpr ( SEMICOLON / KEYWORD_else Statement )

WhileStatement
    <- WhilePrefix BlockExpr ( KEYWORD_else Payload? Statement )?
     / WhilePrefix !BlockExpr AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )

BlockExprStatement
    <- BlockExpr
     / !BlockExpr AssignExpr SEMICOLON

BlockExpr <- BlockLabel? Block

# An assignment or a destructure whose LHS are all lvalue expressions or variable declarations.
VarAssignStatement <- (VarDeclProto / Expr) (COMMA (VarDeclProto / Expr))* EQUAL Expr SEMICOLON

# *** Expression Level ***

# An assignment or a destructure whose LHS are all lvalue expressions.
AssignExpr <- Expr (AssignOp Expr / (COMMA Expr)+ EQUAL Expr)?

SingleAssignExpr <- Expr (AssignOp Expr)?

Expr <- BoolOrExpr

BoolOrExpr <- BoolAndExpr (KEYWORD_or BoolAndExpr)*

BoolAndExpr <- CompareExpr (KEYWORD_and CompareExpr)*

CompareExpr <- BitwiseExpr (CompareOp BitwiseExpr)?

BitwiseExpr <- BitShiftExpr (BitwiseOp BitShiftExpr)*

BitShiftExpr <- AdditionExpr (BitShiftOp AdditionExpr)*

AdditionExpr <- MultiplyExpr (AdditionOp MultiplyExpr)*

MultiplyExpr <- PrefixExpr (MultiplyOp PrefixExpr)*

PrefixExpr <- PrefixOp* PrimaryExpr

PrimaryExpr
    <- AsmExpr
     / IfExpr
     / KEYWORD_break (BreakLabel / !BreakLabel) (Expr !ExprSuffix / !SinglePtrTypeStart)
     / KEYWORD_comptime Expr !ExprSuffix
     / KEYWORD_nosuspend Expr !ExprSuffix
     / KEYWORD_continue (BreakLabel / !BreakLabel) (Expr !ExprSuffix / !SinglePtrTypeStart)
     / KEYWORD_resume Expr !ExprSuffix
     / KEYWORD_return (Expr !ExprSuffix / !SinglePtrTypeStart)
     / BlockLabel? LoopExpr
     / Block
     / CurlySuffixExpr

IfExpr <- IfPrefix Expr (KEYWORD_else Payload? Expr)? !ExprSuffix

Block <- LBRACE BlockStatement* RBRACE

LoopExpr <- KEYWORD_inline? (ForExpr / WhileExpr)

ForExpr <- ForPrefix Expr (KEYWORD_else Expr / !KEYWORD_else) !ExprSuffix

WhileExpr <- WhilePrefix Expr (KEYWORD_else Payload? Expr)? !ExprSuffix

CurlySuffixExpr <- TypeExpr InitList?

InitList
    <- LBRACE FieldInit (COMMA FieldInit)* COMMA? RBRACE
     / LBRACE Expr (COMMA Expr)* COMMA? RBRACE
     / LBRACE RBRACE

TypeExpr <- PrefixTypeOp* ErrorUnionExpr

ErrorUnionExpr <- SuffixExpr (EXCLAMATIONMARK TypeExpr)?

SuffixExpr
    <- PrimaryTypeExpr (SuffixOp / FnCallArguments)*

PrimaryTypeExpr
    <- BUILTINIDENTIFIER FnCallArguments
     / CHAR_LITERAL
     / ContainerDecl
     / DOT IDENTIFIER
     / DOT InitList
     / ErrorSetDecl
     / FLOAT
     / FnProto
     / GroupedExpr
     / LabeledTypeExpr
     / IDENTIFIER !(COLON LabelableExpr)
     / IfTypeExpr
     / INTEGER
     / KEYWORD_comptime TypeExpr !ExprSuffix
     / KEYWORD_error DOT IDENTIFIER
     / KEYWORD_anyframe
     / KEYWORD_unreachable
     / STRINGLITERAL

ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto

ErrorSetDecl <- KEYWORD_error LBRACE IdentifierList RBRACE

GroupedExpr <- LPAREN Expr RPAREN

IfTypeExpr <- IfPrefix TypeExpr (KEYWORD_else Payload? TypeExpr)? !ExprSuffix

LabeledTypeExpr
    <- BlockLabel Block
     / BlockLabel? LoopTypeExpr
     / BlockLabel? SwitchExpr

LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr)

ForTypeExpr <- ForPrefix TypeExpr (KEYWORD_else TypeExpr / !KEYWORD_else) !ExprSuffix

WhileTypeExpr <- WhilePrefix TypeExpr (KEYWORD_else Payload? TypeExpr)? !ExprSuffix

SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE

# *** Assembly ***
AsmExpr <- KEYWORD_asm KEYWORD_volatile? LPAREN Expr AsmOutput? RPAREN

AsmOutput <- COLON AsmOutputList AsmInput?

AsmOutputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERALSINGLE LPAREN (MINUSRARROW TypeExpr / IDENTIFIER) RPAREN

AsmInput <- COLON AsmInputList AsmClobbers?

AsmInputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERALSINGLE LPAREN Expr RPAREN

AsmClobbers <- COLON Expr

# *** Helper grammar ***
BreakLabel <- COLON IDENTIFIER

BlockLabel <- IDENTIFIER COLON

FieldInit <- DOT IDENTIFIER EQUAL Expr

WhileContinueExpr <- COLON LPAREN AssignExpr RPAREN

LinkSection <- KEYWORD_linksection LPAREN Expr RPAREN

AddrSpace <- KEYWORD_addrspace LPAREN Expr RPAREN

# Fn specific
CallConv <- KEYWORD_callconv LPAREN Expr RPAREN

ParamDecl <- doc_comment? (KEYWORD_noalias / KEYWORD_comptime / !KEYWORD_comptime) (IDENTIFIER COLON / !(IDENTIFIER_COLON)) ParamType

ParamType
    <- KEYWORD_anytype
     / TypeExpr

# Control flow prefixes
IfPrefix <- KEYWORD_if LPAREN Expr RPAREN PtrPayload?

WhilePrefix <- KEYWORD_while LPAREN Expr RPAREN PtrPayload? WhileContinueExpr?

ForPrefix <- KEYWORD_for LPAREN ForArgumentsList RPAREN PtrListPayload

# Payloads
Payload <- PIPE IDENTIFIER PIPE

PtrPayload <- PIPE ASTERISK? IDENTIFIER PIPE

PtrIndexPayload <- PIPE ASTERISK? IDENTIFIER (COMMA IDENTIFIER)? PIPE

PtrListPayload <- PIPE ASTERISK? IDENTIFIER (COMMA ASTERISK? IDENTIFIER)* COMMA? PIPE

# Switch specific
SwitchProng <- KEYWORD_inline? SwitchCase EQUALRARROW PtrIndexPayload? SingleAssignExpr

SwitchCase
    <- SwitchItem (COMMA SwitchItem)* COMMA?
     / KEYWORD_else

SwitchItem <- Expr (DOT3 Expr)?

# For specific
ForArgumentsList <- ForItem (COMMA ForItem)* COMMA?

ForItem <- Expr (DOT2 Expr?)?

# Operators
AssignOp
    <- ASTERISKEQUAL
     / ASTERISKPIPEEQUAL
     / SLASHEQUAL
     / PERCENTEQUAL
     / PLUSEQUAL
     / PLUSPIPEEQUAL
     / MINUSEQUAL
     / MINUSPIPEEQUAL
     / LARROW2EQUAL
     / LARROW2PIPEEQUAL
     / RARROW2EQUAL
     / AMPERSANDEQUAL
     / CARETEQUAL
     / PIPEEQUAL
     / ASTERISKPERCENTEQUAL
     / PLUSPERCENTEQUAL
     / MINUSPERCENTEQUAL
     / EQUAL

CompareOp
    <- EQUALEQUAL
     / EXCLAMATIONMARKEQUAL
     / LARROW
     / RARROW
     / LARROWEQUAL
     / RARROWEQUAL

BitwiseOp
    <- AMPERSAND
     / CARET
     / PIPE
     / KEYWORD_orelse
     / KEYWORD_catch Payload?

BitShiftOp
    <- LARROW2
     / RARROW2
     / LARROW2PIPE

AdditionOp
    <- PLUS
     / MINUS
     / PLUS2
     / PLUSPERCENT
     / MINUSPERCENT
     / PLUSPIPE
     / MINUSPIPE

MultiplyOp
    <- PIPE2
     / ASTERISK
     / SLASH
     / PERCENT
     / ASTERISK2
     / ASTERISKPERCENT
     / ASTERISKPIPE

PrefixOp
    <- EXCLAMATIONMARK
     / MINUS
     / TILDE
     / MINUSPERCENT
     / AMPERSAND
     / KEYWORD_try

PrefixTypeOp
    <- QUESTIONMARK
     / KEYWORD_anyframe MINUSRARROW
     / (ManyPtrTypeStart / SliceTypeStart) KEYWORD_allowzero? ByteAlign? AddrSpace? KEYWORD_const? KEYWORD_volatile?
     / SinglePtrTypeStart KEYWORD_allowzero? BitAlign? AddrSpace? KEYWORD_const? KEYWORD_volatile?
     / ArrayTypeStart

SuffixOp
    <- LBRACKET Expr (DOT2 (Expr? (COLON Expr)?)?)? RBRACKET
     / DOT IDENTIFIER
     / DOTASTERISK
     / DOTQUESTIONMARK

FnCallArguments <- LPAREN ExprList RPAREN

ExprSuffix
    <- KEYWORD_or
     / KEYWORD_and
     / CompareOp
     / BitwiseOp
     / BitShiftOp
     / AdditionOp
     / MultiplyOp
     / EXCLAMATIONMARK
     / SuffixOp
     / FnCallArguments

LabelableExpr
    <- Block
     / SwitchExpr
     / LoopExpr

# Ptr specific
SliceTypeStart <- LBRACKET (COLON Expr)? RBRACKET

SinglePtrTypeStart <- ASTERISK / ASTERISK2

ManyPtrTypeStart <- LBRACKET ASTERISK (LETTERC / COLON Expr)? RBRACKET

ArrayTypeStart <- LBRACKET Expr !(ASTERISK / ASTERISK2) (COLON Expr)? RBRACKET

# ContainerDecl specific
ContainerDeclAuto <- ContainerDeclType LBRACE ContainerMembers RBRACE

ContainerDeclType
    <- KEYWORD_struct (LPAREN Expr RPAREN)?
     / KEYWORD_opaque
     / KEYWORD_enum (LPAREN Expr RPAREN)?
     / KEYWORD_union (LPAREN (KEYWORD_enum (LPAREN Expr RPAREN)? / !KEYWORD_enum Expr) RPAREN)?

# Alignment
ByteAlign <- KEYWORD_align LPAREN Expr RPAREN

BitAlign <- KEYWORD_align LPAREN Expr (COLON Expr COLON Expr)? RPAREN

# Lists
IdentifierList <- (doc_comment? IDENTIFIER COMMA)* (doc_comment? IDENTIFIER)?

SwitchProngList <- (SwitchProng COMMA)* SwitchProng?

AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem?

AsmInputList <- (AsmInputItem COMMA)* AsmInputItem?

ParamDeclList <- (ParamDecl COMMA)* (ParamDecl / DOT3 COMMA?)?

ExprList <- (Expr COMMA)* Expr?

# *** Tokens ***
eof <- !.
bin <- [01]
bin_ <- '_'? bin
oct <- [0-7]
oct_ <- '_'? oct
hex <- [0-9a-fA-F]
hex_ <- '_'? hex
dec <- [0-9]
dec_ <- '_'? dec

bin_int <- bin bin_*
oct_int <- oct oct_*
dec_int <- dec dec_*
hex_int <- hex hex_*

ox80_oxBF <- [\200-\277]
oxF4 <- '\364'
ox80_ox8F <- [\200-\217]
oxF1_oxF3 <- [\361-\363]
oxF0 <- '\360'
ox90_0xBF <- [\220-\277]
oxEE_oxEF <- [\356-\357]
oxED <- '\355'
ox80_ox9F <- [\200-\237]
oxE1_oxEC <- [\341-\354]
oxE0 <- '\340'
oxA0_oxBF <- [\240-\277]
oxC2_oxDF <- [\302-\337]

# From https://lemire.me/blog/2018/05/09/how-quickly-can-you-check-that-a-string-is-valid-unicode-utf-8/
# First Byte      Second Byte     Third Byte      Fourth Byte
# [0x00,0x7F]
# [0xC2,0xDF]     [0x80,0xBF]
#    0xE0         [0xA0,0xBF]     [0x80,0xBF]
# [0xE1,0xEC]     [0x80,0xBF]     [0x80,0xBF]
#    0xED         [0x80,0x9F]     [0x80,0xBF]
# [0xEE,0xEF]     [0x80,0xBF]     [0x80,0xBF]
#    0xF0         [0x90,0xBF]     [0x80,0xBF]     [0x80,0xBF]
# [0xF1,0xF3]     [0x80,0xBF]     [0x80,0xBF]     [0x80,0xBF]
#    0xF4         [0x80,0x8F]     [0x80,0xBF]     [0x80,0xBF]

multibyte_utf8 <-
       oxF4      ox80_ox8F ox80_oxBF ox80_oxBF
     / oxF1_oxF3 ox80_oxBF ox80_oxBF ox80_oxBF
     / oxF0      ox90_0xBF ox80_oxBF ox80_oxBF
     / oxEE_oxEF ox80_oxBF ox80_oxBF
     / oxED      ox80_ox9F ox80_oxBF
     / oxE1_oxEC ox80_oxBF ox80_oxBF
     / oxE0      oxA0_oxBF ox80_oxBF
     / oxC2_oxDF ox80_oxBF

non_control_ascii <- [\040-\176]
non_control_utf8 <- [\040-\377]

char_escape
    <- "\\x" hex hex
     / "\\u{" hex+ "}"
     / "\\" [nr\\t'"]
char_char
    <- multibyte_utf8
     / char_escape
     / ![\\'\n] non_control_ascii

string_char
    <- multibyte_utf8
     / char_escape
     / ![\\"\n] non_control_ascii

container_doc_comment <- ('//!' non_control_utf8* [ \n]* skip)+
doc_comment <- ('///' non_control_utf8* [ \n]* skip)+
line_comment <- '//' ![!/] non_control_utf8* / '////' non_control_utf8*
line_string <- '\\\\' non_control_utf8* [ \n]*
skip <- ([ \n] / line_comment)*

CHAR_LITERAL <- ['] char_char ['] skip
FLOAT
    <- '0x' hex_int '.' hex_int ([pP] [-+]? dec_int)? skip
     /      dec_int '.' dec_int ([eE] [-+]? dec_int)? skip
     / '0x' hex_int [pP] [-+]? dec_int skip
     /      dec_int [eE] [-+]? dec_int skip
INTEGER
    <- '0b' bin_int skip
     / '0o' oct_int skip
     / '0x' hex_int skip
     /      dec_int   skip
STRINGLITERALSINGLE <- ["] string_char* ["] skip
STRINGLITERAL
    <- STRINGLITERALSINGLE
     / (line_string                 skip)+
IDENTIFIER
    <- !keyword [A-Za-z_] [A-Za-z0-9_]* skip
     / '@' STRINGLITERALSINGLE
BUILTINIDENTIFIER <- '@'[A-Za-z_][A-Za-z0-9_]* skip


AMPERSAND            <- '&'      ![=]      skip
AMPERSANDEQUAL       <- '&='               skip
ASTERISK             <- '*'      ![*%=|]   skip
ASTERISK2            <- '**'               skip
ASTERISKEQUAL        <- '*='               skip
ASTERISKPERCENT      <- '*%'     ![=]      skip
ASTERISKPERCENTEQUAL <- '*%='              skip
ASTERISKPIPE         <- '*|'     ![=]      skip
ASTERISKPIPEEQUAL    <- '*|='              skip
CARET                <- '^'      ![=]      skip
CARETEQUAL           <- '^='               skip
COLON                <- ':'                skip
COMMA                <- ','                skip
DOT                  <- '.'      ![*.?]    skip
DOT2                 <- '..'     ![.]      skip
DOT3                 <- '...'              skip
DOTASTERISK          <- '.*'               skip
DOTQUESTIONMARK      <- '.?'               skip
EQUAL                <- '='      ![>=]     skip
EQUALEQUAL           <- '=='               skip
EQUALRARROW          <- '=>'               skip
EXCLAMATIONMARK      <- '!'      ![=]      skip
EXCLAMATIONMARKEQUAL <- '!='               skip
LARROW               <- '<'      ![<=]     skip
LARROW2              <- '<<'     ![=|]     skip
LARROW2EQUAL         <- '<<='              skip
LARROW2PIPE          <- '<<|'    ![=]      skip
LARROW2PIPEEQUAL     <- '<<|='             skip
LARROWEQUAL          <- '<='               skip
LBRACE               <- '{'                skip
LBRACKET             <- '['                skip
LPAREN               <- '('                skip
MINUS                <- '-'      ![%=>|]   skip
MINUSEQUAL           <- '-='               skip
MINUSPERCENT         <- '-%'     ![=]      skip
MINUSPERCENTEQUAL    <- '-%='              skip
MINUSPIPE            <- '-|'     ![=]      skip
MINUSPIPEEQUAL       <- '-|='              skip
MINUSRARROW          <- '->'               skip
PERCENT              <- '%'      ![=]      skip
PERCENTEQUAL         <- '%='               skip
PIPE                 <- '|'      ![|=]     skip
PIPE2                <- '||'               skip
PIPEEQUAL            <- '|='               skip
PLUS                 <- '+'      ![%+=|]   skip
PLUS2                <- '++'               skip
PLUSEQUAL            <- '+='               skip
PLUSPERCENT          <- '+%'     ![=]      skip
PLUSPERCENTEQUAL     <- '+%='              skip
PLUSPIPE             <- '+|'     ![=]      skip
PLUSPIPEEQUAL        <- '+|='              skip
LETTERC              <- 'c'                skip
QUESTIONMARK         <- '?'                skip
RARROW               <- '>'      ![>=]     skip
RARROW2              <- '>>'     ![=]      skip
RARROW2EQUAL         <- '>>='              skip
RARROWEQUAL          <- '>='               skip
RBRACE               <- '}'                skip
RBRACKET             <- ']'                skip
RPAREN               <- ')'                skip
SEMICOLON            <- ';'                skip
SLASH                <- '/'      ![=]      skip
SLASHEQUAL           <- '/='               skip
TILDE                <- '~'                skip

end_of_word <- ![a-zA-Z0-9_] skip
KEYWORD_addrspace   <- 'addrspace'   end_of_word
KEYWORD_align       <- 'align'       end_of_word
KEYWORD_allowzero   <- 'allowzero'   end_of_word
KEYWORD_and         <- 'and'         end_of_word
KEYWORD_anyframe    <- 'anyframe'    end_of_word
KEYWORD_anytype     <- 'anytype'     end_of_word
KEYWORD_asm         <- 'asm'         end_of_word
KEYWORD_break       <- 'break'       end_of_word
KEYWORD_callconv    <- 'callconv'    end_of_word
KEYWORD_catch       <- 'catch'       end_of_word
KEYWORD_comptime    <- 'comptime'    end_of_word
KEYWORD_const       <- 'const'       end_of_word
KEYWORD_continue    <- 'continue'    end_of_word
KEYWORD_defer       <- 'defer'       end_of_word
KEYWORD_else        <- 'else'        end_of_word
KEYWORD_enum        <- 'enum'        end_of_word
KEYWORD_errdefer    <- 'errdefer'    end_of_word
KEYWORD_error       <- 'error'       end_of_word
KEYWORD_export      <- 'export'      end_of_word
KEYWORD_extern      <- 'extern'      end_of_word
KEYWORD_fn          <- 'fn'          end_of_word
KEYWORD_for         <- 'for'         end_of_word
KEYWORD_if          <- 'if'          end_of_word
KEYWORD_inline      <- 'inline'      end_of_word
KEYWORD_noalias     <- 'noalias'     end_of_word
KEYWORD_nosuspend   <- 'nosuspend'   end_of_word
KEYWORD_noinline    <- 'noinline'    end_of_word
KEYWORD_opaque      <- 'opaque'      end_of_word
KEYWORD_or          <- 'or'          end_of_word
KEYWORD_orelse      <- 'orelse'      end_of_word
KEYWORD_packed      <- 'packed'      end_of_word
KEYWORD_pub         <- 'pub'         end_of_word
KEYWORD_resume      <- 'resume'      end_of_word
KEYWORD_return      <- 'return'      end_of_word
KEYWORD_linksection <- 'linksection' end_of_word
KEYWORD_struct      <- 'struct'      end_of_word
KEYWORD_suspend     <- 'suspend'     end_of_word
KEYWORD_switch      <- 'switch'      end_of_word
KEYWORD_test        <- 'test'        end_of_word
KEYWORD_threadlocal <- 'threadlocal' end_of_word
KEYWORD_try         <- 'try'         end_of_word
KEYWORD_union       <- 'union'       end_of_word
KEYWORD_unreachable <- 'unreachable' end_of_word
KEYWORD_var         <- 'var'         end_of_word
KEYWORD_volatile    <- 'volatile'    end_of_word
KEYWORD_while       <- 'while'       end_of_word

keyword <- KEYWORD_addrspace / KEYWORD_align / KEYWORD_allowzero / KEYWORD_and
         / KEYWORD_anyframe / KEYWORD_anytype / KEYWORD_asm
         / KEYWORD_break / KEYWORD_callconv / KEYWORD_catch
         / KEYWORD_comptime / KEYWORD_const / KEYWORD_continue / KEYWORD_defer
         / KEYWORD_else / KEYWORD_enum / KEYWORD_errdefer / KEYWORD_error / KEYWORD_export
         / KEYWORD_extern / KEYWORD_fn / KEYWORD_for / KEYWORD_if
         / KEYWORD_inline / KEYWORD_noalias / KEYWORD_nosuspend / KEYWORD_noinline
         / KEYWORD_opaque / KEYWORD_or / KEYWORD_orelse / KEYWORD_packed
         / KEYWORD_pub / KEYWORD_resume / KEYWORD_return / KEYWORD_linksection
         / KEYWORD_struct / KEYWORD_suspend / KEYWORD_switch / KEYWORD_test
         / KEYWORD_threadlocal / KEYWORD_try / KEYWORD_union / KEYWORD_unreachable
         / KEYWORD_var / KEYWORD_volatile / KEYWORD_while

Дзен

  • Выражайте намерения точно.
  • Крайние случаи важны.
  • Отдавайте предпочтение чтению кода, а не его написанию.
  • Для каждой задачи должен быть только один очевидный способ решения.
  • Сбои во время выполнения лучше, чем ошибки.
  • Ошибки компиляции лучше, чем сбои во время выполнения.
  • Улучшайте постепенно.
  • Избегайте локальных максимумов.
  • Сокращайте объем того, что нужно помнить.
  • Сосредоточьтесь на коде, а не на стиле.
  • Выделение ресурсов может завершиться неудачей; освобождение ресурсов должно завершаться успешно.
  • Память — это ресурс.
  • Вместе мы служим пользователям.

© 2015–2026, Zig contributors
Licensed under the MIT License.
https://ziglang.org/documentation/0.16.0/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API