Откуда меня позвали? — 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.