Skip to content

Вычисления на этапе компиляции

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обрабатывать исключение при сборке некому
выполнение на GPUcomptime-код в GPU-программе считается на CPU

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

raises отсекается сразу, ещё при разборе кода:

error: cannot call raising function in comptime initializer
Что это значит

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

Как исправить

Если функция и правда может не справиться — считайте её во время работы программы. Если raises попало в сигнатуру случайно, уберите его: часто достаточно не вызывать внутри Int(String(...)) или обращение к словарю по ключу.

Сюда же попадает почти весь ввод-вывод: open() бросает, разбор строки в число бросает.

FFI разбирается нормально, но спотыкается при вычислении. Функция с external_call не помечена raises, поэтому до первой ошибки дело доходит позже — когда компилятор пытается её выполнить:

error: function instantiation failed
note: failed to compile-time evaluate function call
note: 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 — считается при сборке, а в программе живёт как статическая константа, к которой обращаются по ссылке.

build_time.mojo
# Одна маленькая задача: перевести число байт в человекочитаемый вид.
# Вся таблица множителей считается на сборке — в готовой программе
# от неё остаются только готовые числа.
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 связывает ссылку на него без копии.

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

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0