# Стек: почему именно так Документ фиксирует решения, которые трудно отменить. Для каждого - альтернативы, критерии, цена и что меняет решение. ## 1. Собственный IR, а не API чужой библиотеки **Решение:** пассы работают с `noxis-classfile::model::{Program, ClassFile, Code, Insn}`, а не с типами парсера. **Почему не «просто использовать готовую библиотеку целиком»:** - библиотека диктует API - каждый пасс привязывается к чужим миграциям мажорной версии (наш лимит «≤4 файлов» легко разрывается «транзитивно»); - у нас есть конкретное требование, которого нет ни в одном парсере: `Insn::Mark(Label)` вместо смещений; - мы не знаём заранее, захочет ли пользователь backend'а ещё одну библиотеку (например, для `StackMapTable`) - изоляция через IR даёт смену backend'а без переписывания пассов. **Что IR даёт на практике:** | свойство | эффект | |---|---| | метки вместо смещений | пассы никогда не пересчитывают jump-офсеты | | `Attribute::Raw` | round-trip без потерь: неизвестный атрибут возвращается как есть | | `Program` включает ресурсы архива | переименование и патч ресурсов в одном прогоне | | одна точка записи | пересчёт смещений, `max_stack` и `StackMapTable` - ровно один раз | **Цена:** конвертация `model ↔ backend` на границах (чтение и запись) - примерно один файл на направление. Дёшево. ## 2. Backend для чтения/записи - `ristretto_classfile` **Решение:** `format::parse` / `format::write` реализуются поверх `ristretto_classfile 0.34`, наш IR остаётся единим API для пассов. Реализация - этап P1, до него обе функции возвращают `Unimplemented`. ### Кандидаты | крейт | чтение | запись | верификация | активность | загрузки | MSRV | |---|---|---|---|---|---|---| | **`ristretto_classfile` 0.34** | ✓ | ✓ | ✓ (JVMS §4.10.2) | 2026-09 | 41k | **1.97.1** | | `jclassfile` | ✓ | - (parser) | - | 2026-08 | 28k | - | | `classfile-parser` | ✓ | - (parser) | - | 2024-10 (устарел) | 31k | - | | `java-bytecode` | ✓ | ✓ | ? | 2026-10 | 41 | - | | `jvm-assembler` | - | - | - | **2015, мёртв** | 3k | - | ### Критерии и что подтвердил spike MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (совместима с нашим `deny(unsafe_code)`), запись, а не только чтение, разбор `StackMapTable`, живой апстрим (CI, бенчи, релиз 2026-09). Спайк `tests/spike_ristretto.rs` подтверждает round-trip, `verify()` и конструирование кадров. ### Что отвергнуто - **Писать парсер с нуля** - спецификация JVMS §4 по формату классов плюс MUTF-8, constant pool, модули, records, переполнения - это месяцы работы ради того, что уже написано и покрыто тестами. Это нарушает KISS и не добавляет ни одной защиты. - **`classfile-parser`, `jclassfile`** - только чтение, значит writer всё равно наш; `classfile-parser` не обновлялся с 2024-10. - **`java-bytecode`** - 41 загрузка: у проекта нет инерции и нет экосистемы. - **`jvm-assembler`** - последний коммит в 2015. ## 3. `StackMapTable`: генерируем сами, пользуемся верификатором **Проблема.** Классы версии ≥ 50 требуют `StackMapTable`. Любая техника, меняющая CFG (Flow), и любая, добавляющая ветки (Strings, Constants, Noise), делает сохранённые кадры неверными → `VerifyError`. **Решение:** не считать кадры самим, а **вывести их типовым выводом (JVMS §4.10.2)** из готового байткода: ```text наш IR → Instruction[] → inference (worklist, LUB на merge-точках) → кадры на каждом target'е ветвления → StackMapTable → запись ``` `ristretto_classfile` содержит всё нужное **в исходном виде** - но снаружи, к сожалению, отдаёт не всё (см. результат спайка ниже): - `verifiers/bytecode/inference.rs` - итеративный dataflow, LUB, фикспоинт; - `verifiers/bytecode/control_flow.rs` - `compute_successors`, worklist; - `verifiers/bytecode/stackmap.rs` - декодер/кэш `StackMapTable`; - `attributes/{max_stack,max_locals,instruction/stack_effect}.rs` - пересчёт `max_stack`/`max_locals` из инструкций; - `attributes/{stack_frame,verification_type}.rs` - типы для записи. **Почему это лучше, чем свои кадры:** это ровно та часть JVMS, в которой ошибка невидима в исходниках и всплывает только на `VerifyError` в чужом рантайме. Писать такой верификатор - самая дорогая и самая опасная часть всего проекта. **Альтернативы, которые отвергнуты:** - **Понижать версию класса до 49 и выкинуть `StackMapTable`** - ломает `invokedynamic`/лямбды и включает дорогой fallback-верификатор; на Java 9+ это заметно и противоречит цели. - **Не делать Flow вообще** - лишает технику, которая даёт ★★★★ к читаемости. Не вариант: вычислим кадры. **Результат spike'а (`tests/spike_frames.rs`) - главный вывод раздела:** | что проверяли | что получили | |---|---| | `verify_class` + `FallbackStrategy::AlwaysInference` на классе с ветвлением и без `StackMapTable` | ✅ `success`, `path_used == Inference` - движок вывода типов работает | | `FallbackStrategy::Strict` на том же классе | ✅ `Err("StackMapTable required ... with control flow")` - требование v50+ реально проверяется | | `StackMapTable` с кадром **не на том** сдвиге | ✅ `Err("Merge point at offset N requires StackMapTable frame")` - ошибка локализована точно | | получить сам кадр из крейта | ❌ **нельзя** | Причина последней строки: в `ristretto_classfile/src/verifiers/bytecode.rs` модули `inference`, `diagnostics`, `frame`, `stackmap`, `control_flow` объявлены как `pub(crate)`. Публично отданы только `verify`, `verify_class`, `verify_method`, `VerificationResult`, `VerifierConfig` и `VerificationContext`. Диагностика в `VerificationResult.diagnostic` заполняется лишь на отдельных путях отказа, а в строгом режиме крейт возвращает `Err(VerifyError)` целиком - то есть **ожидаемый кадр до нас не доходит**, только его смещение. **Что это меняет для P1.** Кадры считаем сами, а крейт используем как **приёмочный тест**: ```text наш IR → Instruction[] → наши кадры → StackMapTable → запись ристретто ↓ verify_class(Strict) = Ok → артефакт валиден ``` Три способа получить сами кадры, в порядке предпочтения: 1. ~~**Вендор `verifiers::bytecode::inference`.**~~ - **отвергнут после оценки объёма.** Один модуль не оторвать: в `inference.rs` 14 ссылок `crate::`, включая private-модули `frame`, `type_system`, `handlers`, `control_flow`, `diagnostics`. Вендорить пришлось бы весь `verifiers/bytecode/` (~9 700 строк) или весь крейт (**61 193 строки `src/`**, 2.3 MB) ради одной патчи `pub(crate)` → `pub`. Чужой код становится третьей по объёму частью репозитория, отстаёт от апстрима и не проходит наши правила (250 строк / 4 файла). 2. **Свой вывод на своём IR - принято.** Кадры считаются по `noxis::model`, а не по ристретто: `format::parse` → IR → пассы → `compute_frames(IR)` → `Attribute::StackMapTable` → `format::write`. Ристретто остаётся **парсером, писателем и приёмочным тестом** из crates.io, без форка. 3. ~~Итеративная починка через `diagnostic`~~ - **отвергнута спайком**: поля `expected`/`pre_frame` в этом пути недостижимы. **Почему 2, а не 1.** Наш dataflow живёт на типах, которые и так нужны пассам: `Insn`, метки вместо смещений, иерархия классов из `noxis-analysis::hierarchy` для LUB на merge-точках. Кадры - это ровно те же инструкции, что уже переписывает `rename::rewrite`, поэтому промежуточная конвертация `model → Instruction[] → model` (требование вендора) добавляет ещё один источник расхождений. Цена - своя реализация (~1 000–1 500 строк: границы блоков, worklist, merge, LUB, эмиссия `StackFrame`), но она покрывается двумя независимыми судьями: ```text наш IR → compute_frames → StackMapTable → запись ристретто ↓ ↓ noxis-verify (red-паттерны) verify_class(Strict) = Ok ↓ java -Xverify:all jar ``` Второй судья - ристретто `verify_class` со стратегией `Strict` - ловит ровно тот класс ошибок, ради которого вендор и рассматривался: неверный сдвиг кадра, пропущенная merge-точка, неверный тип. **Критерий готовности этапа:** на всех jar из `fixtures` (см. `todo.md`) `verify_class(Strict) == Ok` и `java -Xverify:all` без `VerifyError`. ## 4. Генерация рантайма, а не вшитая таблица **Решение:** `noxis-runtime` - это **заготовки Rust-кода**, из которых собирается класс-декодер (P2), а не готовый бинарь. **Почему:** ABI декодера - `d(Ljava/lang/String;I)Ljava/lang/String;`, но схем шифрования несколько (`xor-splitmix`, `xtea-ctr`) и их число растёт. Константная таблица селекторов рядом с текстом - ровно та ошибка, которую мы разбирали (`info/evo-weaknesses.md` §1): **ключ рядом с шифротекстом**. Генератор выбирает схемы на этапе сборки артефакта и кодирует выбор в самих вызовах, а мастер-секрет выводится в рантайме. **Что нельзя делать:** вшивать мастер-секрет как константу в `LDC`. ## 5. PRNG: свой, не `rand` **Решение:** `xoshiro256++` + `splitmix64` + доменное разделение по FNV-1a. - **Детерминированность обязана:** один seed на одинаковой версии → байт-в-байт одинаковый jar. Это свойство, а не удобство: без него воспроизводимая сборка, откат и сравнение версий невозможны. - **Доменное разделение:** `ctx.rng("rename/classes")`, `ctx.rng("noise/junk")` - сид деривается из имени домена, поэтому случайности разных пассов не статистически коррелированы. - **Зависимость `rand` тянет** OS-энтропию и меняющееся поведение между версиями. Нам нужен ровно обратный эффект. ## 6. ZIP: `zip` Критерии: чтение, запись, **сохранение порядка и сжатия**, контроль дубликатов. Берём `zip` (стабильный релиз, без `time`/`aes`/`zstd` - лишние фичи только раздувают бинарь; `default-features = false, features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) вырезаем на уровне записей архива - после перепаковки она всё равно невалидна. Альтернатива `zip6` - форк того же кода; при равном API он даёт меньше тестов и меньшую инерцию. Нет причины платить за это. **MSRV воркспейса поднят до 1.97** - это ровно наш тулчейн (rustc 1.97.1) и минимум `ristretto_classfile 0.34`. Заявлять 1.85, когда зависимость требует 1.97, - враньё в `Cargo.toml`. ## 7. CLI: `clap` (derive) Стандарт, стабильный API, поддержка цветов и автодополнения. Альтернативы (`bpaf`, `gumdrop`, ручной парсер) экономят размер сборки, но не решают ни одной нашей задачи. ## 8. Верификация: `java -Xverify:all` первым делом Единственная честная проверка «я ничего не сломал» - запустить настоящую JVM. `noxis-verify` гоняет `java -Xverify:all` на обфусцированном jar и по red-паттернам ищет дыры в защите. **Запасной вариант, если JDK не гарантирован:** `ristretto_vm` - встраиваемый JVM на Rust из того же репозитория. Для нас это означает `noxis-verify` без внешних зависимостей. Стоит проверить в P3, пока используем системный `java` (в окружении есть OpenJDK 21). ## Сводка | решение | альтернатива | цена | |---|---|---| | свой IR + метки | писать пассы поверх чужого API | конвертер на границе | | `ristretto_classfile` | свой парсер / parse-only крейты | зависимость 1 уровня | | inference-кадры | свой верификатор / downgrade версии | spike на публичность API | | генерация декодера | вшитая таблица селекторов | сложнее собирать | | свой PRNG | `rand` | ~1 файл | | `java -Xverify:all` | только красные паттерны | нужен JDK |