Вычисления на этапе компиляции
This content is not available in your language yet.
В прошлой главе мы выяснили, что компилятор умеет выполнять ваш код. Теперь разберёмся, что именно он готов посчитать, где проходит граница и что делать, когда посчитанное нужно перенести в работающую программу.
Считается почти любой код
Заголовок раздела «Считается почти любой код»Компилятору не нужны специальные функции. Годится обычная — с циклами, ветвлениями, рекурсией:
def triangular(n: Int) -> Int: var total = 0 for value in range(1, n + 1): total += value return total
comptime SIZE = triangular(4)
def main(): print(SIZE)10
Строки тоже считаются:
def join_names() -> String: var result = String("") for name in ["раз", "два"]: result += name + " " return result
comptime GREETING = join_names()
def main(): print(GREETING)раз два
Таблица, посчитанная при сборке
Заголовок раздела «Таблица, посчитанная при сборке»Классический приём: заранее заполненная таблица вместо вычислений во время работы.
def build_powers() -> Array[Int, 8]: var table = Array[Int, 8](fill=0) var value = 1 for index in range(8): table[index] = value value *= 2 return table^
comptime POWERS = build_powers()comptime LARGEST = POWERS[7]
def main(): print(LARGEST)128
Ни цикла, ни умножений в готовой программе нет — там стоит 128.
comptime assert: проверка, которая не доживает до запуска
Заголовок раздела «comptime assert: проверка, которая не доживает до запуска»Обычный assert проверяет условие во время работы. comptime assert
проверяет его при сборке — и если условие ложно, программа просто
не соберётся:
def main(): comptime N = 5 comptime assert N > 0, "N должно быть положительным" print(N)5
Поменяйте 5 на -5, и компилятор откажется собирать:
error: function instantiation failed
Вычисление при сборке не удалось. Само по себе это сообщение
ничего не объясняет — причину называет строка note: под ним.
Для проваленной проверки там будет ваше сообщение:
note: constraint failed: N должно быть положительнымПлюс предупреждение о том, что код после проверки недостижим.
Ошибка не в проверке, а в значении: либо исправьте его, либо ослабьте условие, если оно оказалось слишком строгим.
Обратите внимание на текст сообщения: он ваш. Это главная причина писать
comptime assert — читатель ошибки получает объяснение на человеческом
языке, а не разбирается во внутренностях шаблонов.
Граница: чего компилятор считать не будет
Заголовок раздела «Граница: чего компилятор считать не будет»На этапе сборки не существует внешнего мира. Официальное руководство перечисляет четыре вещи, которые компилятор выполнять отказывается:
| Что нельзя | Почему |
|---|---|
| файловый ввод-вывод | файлов на этапе сборки нет |
| вызовы внешних функций (FFI) | чужую библиотеку компилятор не загружает |
функции, помеченные raises | обрабатывать исключение при сборке некому |
| выполнение на GPU | comptime-код в GPU-программе считается на CPU |
Первые две категории пересекаются с третьей, но не сводятся к ней — и ошибки у них разные. Это важно: по тексту сообщения вы поймёте, на что именно наткнулись.
raises отсекается сразу, ещё при разборе кода:
error: cannot call raising function in comptime initializer
Функция помечена raises, а на этапе сборки обрабатывать исключения
некому — компилятор отказывается её выполнять, даже не пытаясь.
Если функция и правда может не справиться — считайте её во время
работы программы. Если raises попало в сигнатуру случайно, уберите
его: часто достаточно не вызывать внутри Int(String(...)) или
обращение к словарю по ключу.
Сюда же попадает почти весь ввод-вывод: open() бросает, разбор строки
в число бросает.
FFI разбирается нормально, но спотыкается при вычислении. Функция
с external_call не помечена raises, поэтому до первой ошибки дело
доходит позже — когда компилятор пытается её выполнить:
error: function instantiation failednote: failed to compile-time evaluate function callnote: failed to interpret function @try_ffi()note: failed to interpret operation pop.external_call{func: "abs" ...}Последняя строка и есть диагноз: дошли до чужого кода, а исполнить его нечем.
Считать можно то, что зависит только от самих значений.
Материализация: перенос значения в работающую программу
Заголовок раздела «Материализация: перенос значения в работающую программу»Пока значение живёт только на этапе сборки, оно ничего не стоит. Но иногда его нужно отдать в обычный код — например, использовать таблицу как изменяемый массив. Это называется материализацией.
Критерий у неё ровно один — трейт ImplicitlyCopyable. Что копируется
неявно, то и материализуется само: Int, Float64, Bool и, что важно,
String. Всё остальное — Array, List, ваши собственные
Copyable-типы — компилятор потребует перенести явно:
error: cannot materialize comptime value of type 'Array[Int, Int(8)]' to runtime because it is not 'ImplicitlyCopyable'
Вы попросили перенести в работающую программу значение, которое неявно не копируется. Компилятор не делает такого молча — по той же причине, по которой не копирует списки молча.
Компилятор подсказывает решение прямо в тексте:
note: use 'materialize' to explicitly materialize the valueПишем var table = materialize[POWERS]() — и дальше это обычный
массив, живущий во время работы программы.
Материализация не бесплатна
Заголовок раздела «Материализация не бесплатна»Требование писать materialize явно — не формальность. Руководство
объясняет замысел прямо: язык заставляет вас явно материализовать то,
что материализовать дорого, «чтобы вы управляли моментом, когда программа
выделяет ресурсы».
Смотрите, что происходит с таблицей:
def peek(index: Int) -> Int: var table = materialize[POWERS]() # копия — при каждом вызове return table[index]Функцию вызвали миллион раз — таблицу собрали миллион раз. Смысл предвычисления при этом теряется целиком.
global_constant: таблица, которую переносить не нужно
Заголовок раздела «global_constant: таблица, которую переносить не нужно»Для таблиц поиска есть правильный инструмент. global_constant кладёт
посчитанное значение в статическую память программы — один раз, при
сборке, — и возвращает на него ссылку:
from std.builtin.globals import global_constant
def peek(index: Int) -> Int: ref table = global_constant[POWERS]() return table[index]Ключевое слово здесь — ref. Оно связывает именно ссылку; напишете var,
и копия вернётся обратно.
Ещё три формы
Заголовок раздела «Ещё три формы»Подвыражение прямо в коде. Если хочется посчитать что-то при сборке, не заводя отдельного имени:
def triangular(n: Int) -> Int: var total = 0 for value in range(1, n + 1): total += value return total
def main(): var precomputed = comptime (triangular(100)) print(precomputed)5050
Параметризованное значение. Своё для каждого аргумента, вычисляется при сборке:
comptime Doubled[value: Int]: Int = value * 2
def main(): print(Doubled[21])42
Это похоже на функцию, но живёт целиком на этапе компиляции: в программе от неё остаётся только результат. Подробный разговор о параметрах — в следующей главе, Параметризация.
Имя для типа. comptime объявляет не только числа и строки. Тип —
тоже значение этапа сборки, и ему можно дать короткое имя:
comptime Pair = Tuple[Int, String]
def main(): var p = Pair(1, String("раз")) print(p[0], p[1])1 раз
Это ровно то, чем в Python занимается TypeAlias, — но здесь имя исчезает
при сборке, а не живёт в памяти как объект.
Всё вместе
Заголовок раздела «Всё вместе»Одна маленькая задача: перевести число байт в человекочитаемый вид. Таблица множителей — степени 1024 — считается при сборке, а в программе живёт как статическая константа, к которой обращаются по ссылке.
# Одна маленькая задача: перевести число байт в человекочитаемый вид.# Вся таблица множителей считается на сборке — в готовой программе# от неё остаются только готовые числа.
from std.builtin.globals import global_constant
comptime UNITS = 5
def build_scale() -> Array[Int, UNITS]: """Степени 1024: 1, 1024, 1048576, ...""" var table = Array[Int, UNITS](fill=1) for index in range(1, UNITS): table[index] = table[index - 1] * 1024 return table^
comptime SCALE = build_scale()comptime NAMES: Array[StaticString, UNITS] = ["Б", "КБ", "МБ", "ГБ", "ТБ"]
# Параметризованное значение: своё для каждого аргумента.comptime Doubled[value: Int]: Int = value * 2
def human(size: Int) -> String: """Подбирает ступень и форматирует размер.""" # global_constant кладёт таблицу в статическую память программы, # а ref связывает ссылку на неё — копии не возникает. ref scale = global_constant[SCALE]() ref names = global_constant[NAMES]()
var index = 0 while index + 1 < UNITS and size >= scale[index + 1]: index += 1
return String(size // scale[index]) + " " + String(names[index])
def main(): comptime assert UNITS > 1, "нужна хотя бы одна ступень"
print("таблица посчитана при сборке, в программе — готовые числа") print(" ", human(512)) print(" ", human(2048)) print(" ", human(5 * 1024 * 1024))
print("самая большая ступень:", comptime (SCALE[UNITS - 1])) print("параметризованное значение:", Doubled[21])таблица посчитана при сборке, в программе — готовые числа 512 Б 2 КБ 5 МБ самая большая ступень: 1099511627776 параметризованное значение: 42
В готовой программе от build_scale не осталось ничего: ни цикла,
ни умножений. Только пять чисел в статической памяти и ссылка на них.
🎯 Проверь себя
Какие функции компилятор может выполнить на этапе сборки?
Почти любые — с циклами, рекурсией, строками. Отказывается он от
четырёх вещей: файлового ввода-вывода, вызовов внешних библиотек (FFI),
функций с raises и выполнения на GPU. Причём raises отсекается
сразу при разборе кода, а FFI — уже при вычислении, и сообщения
у них разные.
Зачем `comptime assert`, если есть обычная проверка?
Она срабатывает при сборке, а не при запуске, и печатает ваше
сообщение: note: constraint failed: ваш текст. Тот, кто неправильно
воспользовался вашим кодом, получает объяснение по-человечески
и до того, как программа вообще собралась.
Что такое материализация?
Перенос вычисленного при сборке значения в работающую программу.
Для всего, что копируется неявно, происходит сама — включая String.
Для остального требует явного materialize[...](): компилятор
не переносит молча то, что не копируется неявно.
Почему `var value = POWERS[3]` требует материализации, а `comptime THIRD = POWERS[3]` — нет?
Потому что во втором случае индексация происходит при сборке и в программу попадает готовое число. В первом результат нужен обычному коду — а значит, таблицу приходится переносить в работающую программу.
Функция `loud(n)` печатает «считаю n» и возвращает `n * 2`. Её результат объявлен как `comptime VALUE = loud(21)`, а `main` печатает `результат: VALUE`. Что вы увидите при сборке, а что при запуске?
При сборке — строку считаю 21. При запуске — только
результат: 42. Функция отработала один раз, внутри компилятора,
и в программу попало готовое число: ни вызова, ни умножения там нет.
Проверяется руками: mojo build покажет первую строку, запуск
собранного файла — вторую.
Функция `peek` вызывается миллион раз и внутри делает `materialize[POWERS]()`. В чём беда?
Таблица собирается заново при каждом вызове — предвычисление
не даёт ничего. Для таблиц поиска нужен global_constant:
он кладёт значение в статическую память один раз, а ref связывает
ссылку на него без копии.
Что дальше
Заголовок раздела «Что дальше»Параметризация: параметры функций и структур, автоматическая специализация и то, во что она обходится размеру программы.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0