Как Bun переписали с Zig на Rust с помощью Claude Code за 11 дней

Как Bun переписали с Zig на Rust с помощью Claude Code за 11 дней

Bun, это JS/TS/CSS-транспилятор, минификатор и бандлер, npm-совместимый пакетный менеджер, Jest-подобный тест-раннер, слой совместимого с Node.js и TypeScript разрешения модулей, HTTP/1.1- и WebSocket-клиент и набор реализаций части Node.js API (fs, net, tls и десятки других модулей), всё в одном инструменте. Автор поста, тот же человек, что написал первую версию Bun: сделал это в одиночку за год, ещё до появления LLM, в тесной съёмной квартире в Окленде, начав с построчного переноса JS/TS-транспилятора esbuild с Go на Zig (первая строка Zig-кода, 16 апреля 2021 года). Сегодня CLI Bun скачивают более 22 млн раз в месяц, на нём как на рантайме работают Claude Code и OpenCode, а Vercel, Railway и DigitalOcean поддерживают его как платформу первого класса.

Дисклеймер из поста: в декабре 2025 года Bun приобрела Anthropic, автор и часть команды Bun теперь работают там же, а для описанного ниже переписывания на Rust использовалась ещё не выпущенная версия модели Claude Fable 5.

Повод для рерайта, не абстрактный, а конкретный список багов, которые чинили в версии Bun v1.3.14: use-after-free (обращение к уже освобождённой памяти) при вызове .reset() на zlib/Brotli/Zstd-потоке во время фонового .write(); use-after-free в реализации HTTP/2, когда JS-колбэк запускал сам себя повторно во время выполнения (например, вызов session.request() прямо из обработчика таймера) и тем самым провоцировал перестроение внутренней хеш-таблицы, обнулявшее указатели на потоки; повреждение памяти в UDPSocket.send()/sendMany(), когда колбэки valueOf()/toString() в пользовательском коде открепляли ArrayBuffer прямо во время отправки; утечка в crypto.scrypt, когда не удавалось выделить буфер вывода; утечка около 6,5 КБ памяти на каждый вызов tlsSocket.setSession() из-за неосвобождённой SSL-сессии; вотчеры fs.watch(), которые вообще никогда не собирались сборщиком мусора после .close() из-за счётчика ссылок, ушедшего в минус; двойное освобождение памяти в CSS-парсере при разборе background-clip с вендорными префиксами; гонка потоков в MessageEvent, когда поток GC-маркировки мог увидеть неконсистентные данные при одновременном обращении через BroadcastChannel или MessagePort. По словам автора, чинить такие баги по одному можно бесконечно, но команда обязана дать пользователям системное решение, а не заплатки.

У команды уже были серьёзные защитные меры: патч Zig-компилятора для поддержки Address Sanitizer и прогон тестов с ASan на каждый коммит, безопасные сборки Zig ReleaseSafe для Windows, круглосуточный фаззинг рантайм-API движком Fuzzilli (тем же, что используют для V8 и JavaScriptCore) и большой набор end-to-end тестов на утечки памяти, «это больше, чем делают многие проекты», пишет автор, но и этого не хватало. Он подчёркивает, что не винит сам Zig: другие проекты на Zig с таким количеством багов не сталкивались, а без Zig он вообще не смог бы в одиночку построить Bun за год. Корень проблемы, в архитектуре: Zig, как и C, не управляет памятью за программиста, не имеет конструкторов и деструкторов, а очистка ресурсов вручную прописывается оператором defer в каждом месте вызова; при этом движок JavaScriptCore (тот же, что в Safari), на котором работает Bun, это сборщик мусора со своими строгими правилами обработки исключений. Смешение GC и ручного управления памятью, редкий случай, под который почти ни один язык не проектировался специально, и каждое выделение памяти в Bun приходилось вручную перепроверять: где и как ровно один раз оно освобождается, проверены ли JS-исключения, видно ли GC-объект консервативному сканеру стека.

Из альтернатив рассматривали стиль-гайды по образцу TigerStyle (для Zig, у TigerBeetle) или C++-гайда Google объёмом 31 000 слов, но их слабое место, обеспечение соблюдения: исторически это ложится на код-ревью и разрозненные линтеры и статические анализаторы. Пробовали идею Rust-подобных «умных указателей» (обёртки SharedPtr) с контролем владения через систему типов, но это заметно портит эргономику, вместо простого fn foo(a: *TCPSocket) пришлось бы писать код с явными .get()/.deref() на каждый вызов. Рассматривали и переход на C++, в Bun и так около 20% кода на нём, и рантайм уже встраивает C++/C-библиотеки JavaScriptCore, uWebSockets/usockets, lshpack и lsquic (HPACK и HTTP/3), BoringSSL и SQLite; C++ дал бы конструкторы и деструкторы и избавил бы от части обёрток extern «C», но всё равно опирался бы на стиль-гайды и код-ревью, а память всё равно бы утекала и портилась даже с ASan.

Большая часть багов из списка, use-after-free, двойное освобождение и «забыли освободить» на ветке обработки ошибок. В безопасном Rust это ошибки этапа компиляции, а очистка происходит автоматически по RAII-подобной модели через Drop. Дело ещё и во времени получения сигнала: фаззинг срабатывает уже после того, как код смержен, CI, после того, как его запушили, а санитайзер и рантайм-проверки, только когда код реально выполняется; ошибка компилятора ловит проблему сразу, в момент написания кода, это куда надёжнее обратной связи, чем стиль-гайд, который держится на дисциплине и ревью.

Проблема, масштаб: без учёта комментариев Bun, это 535 496 строк Zig-кода. Классическое переписывание такого объёма небольшой командой заняло бы около года с заморозкой багфиксов, security-патчей и новых фич, совершенно нереалистичный вариант. Запасным планом было форсировать дисциплину через самодельные умные указатели, но, по признанию автора, он не хотел идти этим путём: подобные обёртки собственной разработки дают худшую эргономику, чем Rust, и при этом не дают его гарантий.

Решение созрело как эксперимент: потратить неделю и проверить, способна ли новая модель Anthropic переписать Bun на Rust. Поначалу автор не ждал успеха. Через несколько дней большая часть тестового набора Bun (он написан на TypeScript и не завязан на язык самого рантайма) начала проходить, а получившийся Rust-код по структуре совпадал с исходным Zig-кодом, оценка сменилась с «стоит попробовать» на «я собираюсь это смержить».

В посте этот этап назван «Claude, перепиши Bun на Rust», но автор сразу оговаривается: буквально попросить ИИ «перепиши всё и не ошибись» и понадеяться на удачу, это не то, что он сделал; вместо этого он продумал процесс так, как это сделал бы человек, отталкиваясь всего от двух принципиальных вопросов (всё остальное, тактика). Первый: переписывать всё и сразу, а не поэтапно, по опыту прежнего переноса esbuild с Go на Zig, поэтапный рерайт оставляет временный код, который потом больно вычищать. Второй: как сохранить архитектуру, производительность и набор функций Bun, получив при этом язык с borrow checker, и как сделать так, чтобы команда могла поддерживать код после рерайта, ответ был: делать перевод максимально буквальным, «как будто код Zig просто оттранспилировали» в Rust, а уже после выхода Bun v1.4 постепенно рефакторить его в сторону идиоматичного Rust и снижать долю unsafe.

Собственно перенос выполнили около 50 автономных процессов (в терминологии поста, workflow) в Claude Code, работавших непрерывно 11 дней подряд. Каждый такой процесс, это цикл «задача → результат → два параллельных автоматических ревью → применение правок». Среди конкретных процессов: составление гайда соответствий между паттернами и типами Zig и Rust (файл PORTING.md); механический перенос каждого .zig-файла в .rs-файл строго по этому гайду и таблице времени жизни объектов LIFETIMES.tsv; исправление ошибок компиляции в каждом крейте; доведение до рабочего состояния подкоманд вроде bun test и bun build; прогон каждого теста из полного тестового набора Bun; и несколько больших проходов рефакторинга и чистки кода. Почти всё это время (и позже) автор вручную отслеживал эти процессы, читал их вывод, искал баги и на ходу правил промпты, которыми управлялся цикл.

Доступный фрагмент источника обрывается на полуслове, на вопросе, как ревьюить PR объёмом больше миллиона [строк изменений]; сам процесс финального ревью такого масштаба в переданном тексте не описан.

Ключевые факты

  • Bun, JS/TS-рантайм с более чем 22 млн загрузок CLI в месяц (на нём работают Claude Code и OpenCode); с декабря 2025 года им владеет Anthropic, и для рерайта использовали её ещё не выпущенную модель Claude Fable 5.
  • Причина рерайта, системные баги памяти в Zig-версии Bun (use-after-free, двойные освобождения, утечки вплоть до ~6,5 КБ на вызов tlsSocket.setSession()), которые не устраняли до конца даже ASan на каждый коммит, круглосуточный фаззинг Fuzzilli и e2e-тесты на утечки.
  • Корень проблемы, Zig не управляет памятью автоматически и требует ручной очистки через defer, тогда как движок JavaScriptCore, на котором работает Bun, сборщик мусора; в безопасном Rust use-after-free и двойное освобождение, ошибки компиляции, а не рантайма.
  • 535 496 строк Zig-кода (без комментариев) механически перевели в Rust силами около 50 автономных процессов в Claude Code, отработавших непрерывно 11 дней, против примерно года работы команды инженеров при классическом переписывании.
  • Стратегия, переписывать всё сразу, а не поэтапно, и делать перевод максимально буквальным («как будто оттранспилировали»), сохраняя архитектуру; переход к идиоматичному Rust и снижение доли unsafe запланированы уже после релиза Bun v1.4.

Почему это важно

Это один из первых подробно описанных публичных кейсов, где команда реального продакшен-проекта, рантайма Bun с более чем 22 млн загрузок CLI в месяц, не просто написала код с помощью ИИ, а поручила агентам механически переписать всю кодовую базу (535 496 строк) с одного языка на другой. Мотивация конкретная: смешение сборщика мусора JavaScriptCore с ручным управлением памятью в Zig систематически порождало use-after-free, двойные освобождения и утечки, а точечные патчи, ASan, фаззинг Fuzzilli и e2e-тесты на утечки эту проблему на уровне класса ошибок не решали. Rust с его компиляторными гарантиями (RAII-модель через Drop) снимает именно этот класс багов, и здесь описано, как около 50 автономных процессов в Claude Code перенесли весь код за 11 дней вместо года ручной работы команды.

Кому это важно

Разработчикам на JavaScript/TypeScript, которые используют Bun как рантайм, пакетный менеджер или тест-раннер, стабильность их инструмента напрямую зависит от исхода этого рерайта. Компаниям и проектам с завязкой на Bun, Vercel, Railway, DigitalOcean с поддержкой первого класса, а также Claude Code и OpenCode, которые используют Bun как рантайм. Инженерным командам, которые оценивают, годятся ли ИИ-агенты для механического переноса легаси-кода промышленного масштаба, а не только для мелких патчей. Наконец, самой Anthropic, с декабря 2025 года Bun принадлежит ей, и рерайт заодно стал витриной для её ещё не выпущенной на тот момент модели Claude Fable 5.

Как это применить

В посте описана воспроизводимая методика, а не разовый трюк. Сначала, два стратегических решения: переписывать всё сразу одним махом, а не по частям (по опыту прежнего переноса esbuild с Go на Zig инкрементальный путь оставляет временный код, который потом дорого выпиливать), и делать перевод максимально буквальным, «как будто оттранспилировали» Zig-код в Rust, сохранив архитектуру и поведение, с расчётом перейти к идиоматичному Rust и снизить долю unsafe отдельным этапом уже после выхода Bun v1.4. Дальше, тактика: составить гайд соответствий Zig и Rust (PORTING.md) и таблицу времени жизни объектов (LIFETIMES.tsv), затем прогнать порядка 50 автономных процессов в Claude Code, каждый, цикл «задача → результат → два параллельных автоматических ревью → применение правок»: механический перенос каждого .zig-файла в .rs, исправление ошибок компиляции по каждому крейту, восстановление работоспособности подкоманд bun test/bun build, прогон всего тестового набора Bun (он на TypeScript и не завязан на язык рантайма) и финальные проходы рефакторинга. На всё ушло 11 дней непрерывной работы при постоянном ручном контроле автора, читавшего вывод процессов и на ходу правившего промпты.

Можно ли доверять

Пост написан от первого лица человеком, который лично написал первую версию Bun, и открыто предупреждает о конфликте интересов: с декабря 2025 года Bun принадлежит Anthropic, автор и часть команды теперь её сотрудники, а для рерайта использовалась ещё не выпущенная модель самой Anthropic. В пользу доверия, обилие конкретики: точный подсчёт строк кода (535 496), детальный список реальных багов вплоть до размера утечки на вызов, перечисленные существующие практики контроля качества (ASan, Fuzzilli, ReleaseSafe-сборки). Против, это блог компании о своём же продукте и своей же модели без независимой проверки результата, а доступный фрагмент источника обрывается на полуслове (на вопросе, как ревьюить PR объёмом больше миллиона строк), так что итоговые метрики качества готового Rust-кода в переданном тексте не приведены.

Риски и подводные камни

Главный риск для читателя, конфликт интересов: компания, принадлежащая Anthropic, рассказывает об успехе модели Anthropic, и цифры стоит воспринимать с этой поправкой. Сама переписка нарочно не идиоматична и, по словам автора, всё ещё содержит много unsafe-кода, переход к идиоматичному Rust запланирован только после релиза Bun v1.4, то есть на момент публикации рерайт не завершён в полном смысле. Процесс не был полностью автономным: автор прямо говорит, что почти всё время (и после) вручную читал вывод этих процессов, ловил баги и на ходу правил промпты, человеческий контроль оставался обязательным условием, а не опцией. Одномоментная переписка полумиллиона строк резко увеличивает «радиус поражения» на случай скрытой ошибки, которую не поймал тестовый набор, а насколько этот набор реально покрывает те граничные случаи, из-за которых возникали баги памяти в оригинале, источник не раскрывает. Наконец, доступный текст обрывается до объяснения самого процесса ревью такого масштаба, эта часть методики остаётся неизвестной.

«Что если вместо этого я потрачу неделю на то, чтобы проверить, способна ли новая модель Anthropic переписать Bun на Rust?»

— автор поста в блоге Bun, создатель исходной версии рантайма