Откуда меня позвали? — CallerLoc и caller_loc()
Помощник для диагностики, сообщающий о себе, бесполезен. Напишите обёртку
над panic — и каждый отказ будет указывать на строку самой обёртки, одну и
ту же всякий раз, не говоря ничего о том, какой из сотни её вызовов пошёл не
так.
Nova отдаёт функции место, откуда её позвали, обычным параметром:
export fn ice(msg str, loc CallerLoc = caller_loc()) -> never {
report(msg, loc.file, loc.line)
}
// resolve.nv:412
ice("unresolved symbol") // loc = { file: "resolve.nv", line: 412 }
Ни атрибута, ни скрытого канала; тип ice — обычный тип функции. loc —
такой же параметр, как остальные, просто со значением по умолчанию.
Почему умолчание даёт строку вызывающего
caller_loc() значит ровно одно: место этого выражения. Написанная прямо
в теле, она вернёт свою собственную строку — тот самый __FILE__/__LINE__,
которого вы и ждёте:
fn here() -> CallerLoc => caller_loc() // this function's own line
А умолчание в Nova подставляется на месте вызова, поэтому caller_loc()
в позиции умолчания физически оказывается в теле вызывающего — и «место этого
выражения» есть его строка. Одно правило, без режимов.
В рантайме это не стоит ничего. Вызова нет: компилятор подставляет константный указатель на статическую запись, по одной на различающуюся пару «файл, строка».
Контракты, assert, panic и throw берут его сами
Внутри функции, которая объявила параметр CallerLoc, место подхватывается
автоматически — повторять его не нужно:
fn half(n int, loc CallerLoc = caller_loc()) -> int
requires n > 0, "half wants a positive"
{
n / 2
}
half(-1) // panic names the CALLER's line, not the clause's
Это касается requires, assert (в том числе #debug assert), panic и throw
(последний записывает место в поле site отказа). Два параметра CallerLoc
отвергаются по имени: выбрать между ними было бы нечем, а выбрать молча — хуже,
чем отказать.
ensures — сознательное исключение
Нарушенное постусловие называет свою собственную строку даже в функции с
таким параметром. Это не недосмотр: вызывающий не может сломать ваше
постусловие даже теоретически — он управляет только аргументами. Если результат
неверен при аргументах, прошедших requires, то сломано тело (или сам
requires слишком слаб, чтобы такие аргументы исключить), и то и другое —
дело самой функции.
Так назначают вину все контрактные языки — Eiffel, D, Ada, C#. Одна фраза
вместо таблицы: что говорит о входе или наружу — указывает на вызывающего;
ensures говорит о собственном выходе.
Цепочка пробрасывается руками
Обёртка, которая хочет обвинить своего вызывающего, передаёт место дальше. Обратите внимание на имя: параметр с умолчанием keyword-only, позиционная форма не компилируется.
fn ensure(ok bool, loc CallerLoc = caller_loc()) -> () {
if !ok { ice("check failed", loc: loc) } // by NAME
}
// typing.nv:88
ensure(t.is_known()) // ice() reports typing.nv:88, through both wrappers
Не передать loc: loc — и ice возьмёт своё умолчание, то есть строку внутри
ensure. Обе записи читаются буквально, и разница видна в тексте.
Это цена, и она выбрана сознательно. Здесь Nova идёт за C# и Swift, а не за Rust: неявного проброса нет, а значит нет и скрытого параметра, который пришлось бы нести системе типов. Проброс — дисциплина автора, компилятор о нём не напомнит. Что на это куплено: функцию можно взять значением, передать в высшую функцию или позвать через протокол — всё продолжает работать, потому что её тип не изменился.
Сообщение несёт оба места
caller.nv:17: assert failed: text (cond) [in ice at diag.nv:129]
Сначала нужное вам место; в хвосте — где проверка сработала физически, чтобы баг самой обёртки остался находимым.
Справка
type CallerLoc { file str, line int } и fn caller_loc() -> ro CallerLoc
живут в прелюдии — импорт не нужен. В поле file лежит путь ровно в том виде,
в каком его получил CLI, — та же конвенция, по которой их уже печатают
нарушения контрактов.
Замысел и причины каждого выбора, включая неявную форму, которую рассмотрели и отвергли: D468.