Error ABI
Статья рассматривает проблемы ABI (Application Binary Interface) при обработке ошибок в программировании. Распространённое мнение, что заполнение информации об ошибках "бесплатно" из-за их редкости, неверно. Наивное составление ошибок из алгебраических типов данных (ADT) ухудшает "счастливый путь" выполнения кода. Объекты ошибок, рекурсивно составленные из перечислений, tend to be large, увеличивая size_of<Result<T, E>>, что заставляет функции по всей стеку вызовов использовать возврат больших структур через память. "Вирусность" ошибок означает, что даже одна большая ошибка на редко выполняемом пути ухудшает производительность везде.
Поэтому зрелые библиотеки обработки ошибок скрывают их за тонким указателем, как в Rust (failure и anyhow), но это требует глобального аллокатора, что тоже не бесплатно. Автор предлагает три подхода к возврату результатов: стандартный (как пользовательский тип), более умный (ABI как у T с зарезервированным регистром для E) и радикальный (полное совпадение ABI с -> T и разворот стека для ошибок). Последний, по мнению автора, может быть оптимальным, несмотря на отсутствие надёжных бенчмарков. Вывод: обработка ошибок должна быть специальной для компилятора, особенно в языках со средним уровнем абстракций.
Комментарии (31)
- Адаптивные ABI для статически линкуемых программ могут оптимизировать производительность за счёт контекстного анализа использования функций.
- Проблема "вирусности" больших типов ошибок: даже редкие большие ошибки могут ухудшить производительность всего стека вызовов.
- Альтернативные подходы к обработке ошибок включают тонкие указатели с vtable (anyhow/failure) и разделение Result<T,E> при значительном различии размеров T и E.
- Добавление исключений в Rust вызывает споры: одни видят в этом угрозу производительности, другие — потенциальное решение проблем обработки ошибок.
- Checked exceptions в Java критикуют за необходимость изменения кода при модификации исключений, хотя другие видят в этом преимущество для надёжности кода.
Go subtleties
Статья представляет собой сборник 15 тонкостей и малоизвестных возможностей языка Go, собранных автором за год работы с языком. Начиная с Go 1.22, можно использовать range с целыми числами для простого создания циклов. Интересно, что оператор ~ позволяет ограничивать универсальные типы, что полезно для типизированных констант. Пакет embed позволяет встраивать файлы прямо в бинарник, упрощая развертывание. Однако есть и подводные камни: len() со строками возвращает количество байтов, а не символов, что может привести к неожиданным результатам при работе с Unicode.
Особенно коварна работа с nil-интерфейсами: даже если значение nil, тип переменной остается ненулевым интерфейсом, что делает проверку a == nil ложной. Это может серьезно затруднить отладку кода, возвращающего интерфейсы. Также стоит отметить возможность переименования целых пакетов через LSP и индексированную строковую интерполяцию для уменьшения повторений. Функция time.After в сочетании с select предоставляет элегантный способ установки таймаутов для горутин.
Комментарии (144)
- Go-разработчики обсуждают, что язык не даёт уверенности в надёжности кода из-за непредсказуемого поведения nil и интерфейсов, а также отсутствия нормального обработчика ошибок.
- Сообщество отмечает, что вместо удобства чтения кода ради скорости компиляции выбрали неинтуитивную интерполяцию строк, что делает отладку тяжелее.
- Разработчики делятся личными историями о том, как нулевые указатели и интерфейсы ведут себя непредсказуемо, и это продолжает подстерегать даже опытных разработчиков.
- Обсуждение также затрагивает, что Go в целом поощряет писать простой код без изощрённых абстракций, что ведёт к быстрому и легкому ПО, но в то же время лишает разработчика выразительных средств.
- Некоторые участники признают, что отсутствие обобщённых дженериков до недавнего времени и отсутствие перечислений кроме как
iotaиerrorв качестве встроенных типов делает язык менее выразителен, чем он мог бы быть.
LLMs are mortally terrified of exceptions 🔥 Горячее
Twitter/X теперь требует включённый JavaScript и блокирует просмотр без него. Пользователи с блокировщиками скриптов или расширений, которые ограничивают JS, теперь видят сообщение о недоступности JavaScript и предлагают «попробовать снова». Это делает невозможным просмотр даже статического контента без JS.
Комментарии (135)
- Обсуждение выявило, что LLM-ы склонны к чрезмерно защитному стилю кода, который, как выясняется, может быть вызван RLHF и набором данных, где «правильный» код — это тот, который перестраховывается на каждом шаге.
- Участники обсудили, что «защитный» код может быть не только избыточным, но и логически неверным, поскольку он может маскировать ошибки, которые в продакшене не должны быть проглочены.
- Некоторые участники отметили, что вместо того, чтобы учить модель писать «правильный» код, стоит сфокусироваться на том, чтобы она училась различать, когда действительно нужна обработка ошибок, а когда можно обойтись без нее.
- Также было отмечено, что вместо того, чтобы писать в защитном стиле, лучше бы научить модель писать код, который не делает глупых ошибок в первую очередь.
Two things LLM coding agents are still bad at 🔥 Горячее 💬 Длинная дискуссия
LLM-агенты пока не умеют копировать и вставлять код — они только «записывают» его заново, что делает невозможным точный рефакторинг. И они не задают вопросов, а сразу делают предположения и бьются об стену. Эти две особенности делают LLM-агентов похожими на самоуверенных стажёров, а не на полноценных разработчиков.
Комментарии (340)
- LLM-агенты не умеют копировать-вставлять код, а только переписывают его из памяти, что может привести к ошибкам.
- Модели не задают уточняющих вопросов, что приводит к тому, что они делают предположения и ошибаются.
- LLM не могут использовать встроенные инструменты рефакторинга и вместо этого пытаются реализовать его самостоятельно, что может привести к ошибкам.
- Агенты не могут взаимодействовать с IDE и другими инструментами, что делает их менее эффективными.
- Модели не могут задавать уточняющие вопросы, что приводит к тому, что они делают предположения и ошибаются.
Generic Containers in C: Safe Division Using Maybe
Показываю, как в C сделать обобщённый контейнер maybe, который безопасно возвращает результат, если он есть, и сообщает об ошибке, если её нет.
#define maybe(T) struct { bool ok; T value; }
#define maybe_just(T,x) { .value = (x), .ok = true }
#define maybe_nothing(T) { .value = (T){}, .ok = false }
static maybe(int) divide(int a, int b) {
return (b != 0) ? maybe_just(int, a / b) : maybe_nothing(int);
}
Вызов:
maybe(int) p = divide(6, d);
if (p.ok) printf("%d\n", p.value);
else puts("division by zero");
Чтобы не забыть проверку, добавляем макрос maybe_value, который при ошибке возвращает нулевой указатель, ловим его санитайзером:
#define maybe_value(x) \
(*({ auto _p = &(x); _p->ok ? &_p->value : (void*)0; }))
Но деление на ноль — не единственная проблема. При делении INT_MIN / -1 возникает переполнение. Исправляем:
maybe(int) safe_divide(int a, int b) {
if (b == 0 || (b == -1 && a == INT_MIN))
return maybe_nothing(int);
return maybe_just(int, a / b);
}
Компилируем с -fsanitize=signed-integer-overflow,integer-divide-by-zero -fsanitize-trap=undefined -O2. В ассемблере не остаётся пути к ud2, то есть оптимизатор доказал: переполнения и деления на ноль нет.
Это не делает весь C «безопасным» (жизненный цикл указателей и арифметика не покрыты), но для ограниченных задач подход работает.
Комментарии (46)
- Критика: реализация не заставляет проверять результат, теряя главное преимущество Maybe.
- Ассемблер: GCC выдаёт почти тот же код, что и для std::optional, но не возвращает результат в регистре.
- UB: «пустой» lvalue в случае ошибки вызывает неопределённое поведение; автор полагается на null-sanitizer.
- Эргономика: предлагают добавить and_then/or_else и сделать тип непрозрачным через макросы.
- Почему не другой язык: встраиваемые/фирменные проекты часто ограничены только C или ASM.
Zig Error Patterns
Введение
Я часто использую отладчик, но привык и к выводной отладке, особенно в юнит-тестах. Хотелось улучшить её и чаще подключать отладчик.
Улучшение выводной отладки
Главная проблема — «шум»: в цикле интересна одна итерация, а печатается всё. Или удобнее читать форматированную структуру, но приходится раскидывать print’ы по коду. В Zig тесты используют error’ы, значит можно печатать только при падении теста через errdefer:
test { errdefer std.debug.print("{f}", .{ast}); // ... }
Так контекст появляется только при ошибке, без засорения лога.
Запуск тестов в отладчике
Просто запустить seergdb или gdb -tui неудобно: тестовые бинарники лежат в zig-cache. Трюк из ziggит: build.zig может запускать команды и передавать путь артефакта:
// seergdb — GUI фронтенд для gdb const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" }); debugger.addArtifactArg(exe_unit_tests);
const debug_step = b.step("debug", "Run unit tests under debugger"); debug_step.dependOn(&debugger.step);
Это запускает правильный бинарник. Но отладчик сработает лишь на брейкпоинте или панике, тогда как раннер тестов «проглатывает» ошибки.
Комбинация трюков
Добавим @breakpoint через errdefer:
test { errdefer @breakpoint(); }
Так мы попадаем в точку ошибки, видим контекст и вывод std.testing.expect*. Минус: при zig build test отчёт показывает падение всего шага тестов, а не отдельных кейсов. Нужна возможность включать брейкпоинты выборочно.
Условная компиляция
Через build options пробрасываем флаг, решающий, вызывать ли @breakpoint в тестах.
Минимальный скрипт сборки, запускающий тесты, дополняем опциями:
const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const lib = b.addModule("zig-test-patterns", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const options = b.addOptions();
options.addOption(bool, "debugger", false);
lib.addImport("config", options.createModule());
const mod_tests = b.addTest(.{ .root_module = lib });
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
В коде тестов:
const std = @import("std"); const config = @import("config");
test "errdefer @breakpoint()" { errdefer if (config.debugger) @breakpoint(); return error.FixMe; }
test "no breakpoint" { return error.FixMe; }
zig build test — без брейкпоинтов. Но менять значение флага так — значит пересобирать build.zig. Добавим опцию прямо в систему сборки:
var options = b.addOptions(); const use_debugger = b.option( bool, "debugger", "Enables code intended to only run under a debugger", ) orelse false; options.addOption(bool, "debugger", use_debugger);
Теперь можно переключать поведением командой:
zig build -Ddebugger test
И, при желании, привязать шаг запуска отладчика к этому флагу.
Комментарии (46)
- Участники хвалят согласованность базовых конструкций Zig: минимализм синтаксиса и мощь comptime позволяют элегантные решения без излишней сложности.
- Особый интерес вызвал errdefer: многие отмечают, что это упрощает тесты и отладку; звучит мнение, что такую возможность «стоит иметь каждому языку».
- Обсуждают практики отладки: полезны советы по интеграции дебаггера в build.zig, что избавляет от ручного поиска исполняемого файла в кэше.
- Поднимается вопрос об ошибках без полезной нагрузки в Zig: при парсинге (например, JSON) типовые ошибки вроде UnexpectedToken недостаточно информативны; интересуются паттернами передачи дополнительного контекста.
- Есть замечание о смешении стилей именования (camelCase в stdlib vs snake_case у автора), что может сбивать с толку.
- Отмечают эстетику сайта и блога: шрифты (Berkeley Mono), цветовую схему и ретро-оформление — «как в старых DOS-играх».
- Проводится параллель с D: аналогичная идея реализована через scope(failure), что подчеркивает общность концепции.