classfile: intern constant-pool entries on write (fix LVTT/LVT ClassFormatError)

ristretto's ConstantPool never deduplicates: every add_* pushes a new entry.
HotSpot matches each LocalVariableTypeTable entry to a LocalVariableTable
entry by raw name_index, so when a name (e.g. a local's) was interned twice
the two tables got different indices and the class failed to load:
'ClassFormatError: LVTT entry ... does not match any LVT entry'.

The writer now memoizes every pool builder by its semantic key and rebuilds
composite constants (Class/NameAndType/Fieldref/Methodref/MethodHandle/
Dynamic) from our own utf8/class primitives so ristretto never mints a
shadow Utf8. This also shrinks the pool (no more ldc -> ldc_w widening that
changed code length on a pure round-trip).

Split pool.rs into a pool/ module (refs in pool/refs.rs) to respect the
file-size gate. Regression test: tests/pool asserts the written pool has no
duplicate CONSTANT_Utf8. Verified: pure parse->write of the neoforge jar
(92 classes) now loads under java -Xverify:all with 0 errors.
This commit is contained in:
Loki 2026-10-08 22:55:49 +02:00
parent a2e4c69210
commit f08bf60635
10 changed files with 509 additions and 206 deletions

View file

@ -117,3 +117,40 @@ pub(super) const DECOY_RIGHT: &[&str] = &[
"mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor",
"ripple",
];
/// Стиль `Plausible`: домен-нейтральные существительные для классов/полей —
/// похожи на обычный прикладной код, не привязаны к конкретному проекту.
pub(super) const PLAUSIBLE_NOUN: &[&str] = &[
"Texture", "Event", "State", "Render", "Asset", "Config", "Session", "Cache", "Buffer",
"Socket", "Stream", "Widget", "Layout", "Theme", "Profile", "Metric", "Token", "Handle",
"Context", "Registry", "Request", "Response", "Payload", "Resource", "Snapshot", "Pipeline",
"Queue", "Slot", "Frame", "Entry",
];
/// Суффиксы ролей для классов (`NounSuffix`, напр. `TextureCache`).
pub(super) const PLAUSIBLE_SUFFIX: &[&str] = &[
"Manager",
"Handler",
"Service",
"Provider",
"Factory",
"Builder",
"Cache",
"Store",
"Loader",
"Parser",
"Worker",
"Controller",
"Adapter",
"Wrapper",
"Registry",
"Resolver",
"Validator",
"Processor",
"Context",
"Listener",
];
/// Глаголы для методов/полей (`verbNoun`, напр. `resolveToken`).
pub(super) const PLAUSIBLE_VERB: &[&str] = &[
"get", "set", "update", "process", "handle", "compute", "resolve", "load", "build", "parse",
"fetch", "apply", "validate", "clear", "reset", "sync", "flush", "bind", "attach", "detach",
];

View file

@ -1,6 +1,8 @@
//! Генерация имён: стили, анти-ключевые слова, уникальность в scope.
pub mod alphabet;
#[cfg(test)]
mod tests;
use std::collections::HashSet;
@ -8,7 +10,7 @@ use noxis_core::{NameStyle, Rng};
use self::alphabet::{
AVOID_CLASS, AVOID_MEMBER, CONFUSABLE, DECOY_LEFT, DECOY_RIGHT, KEYWORDS, LATIN_LOWER,
LATIN_UPPER, UNICODE_LETTERS,
LATIN_UPPER, PLAUSIBLE_NOUN, PLAUSIBLE_SUFFIX, PLAUSIBLE_VERB, UNICODE_LETTERS,
};
/// Генератор имён с гарантированной уникальностью в пределах одного scope.
@ -68,7 +70,7 @@ impl NameGenerator {
match self.style {
NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z",
NameStyle::Unicode => "\u{043e}",
NameStyle::DecoyWords => "tmp",
NameStyle::DecoyWords | NameStyle::Plausible => "tmp",
}
}
@ -121,6 +123,20 @@ impl NameGenerator {
}
name
}
// Маскируется под обычный прикладной код, а не под мусор:
// классы — `NounSuffix` (`TextureCache`), члены — `verbNoun`
// (`resolveToken`) camelCase.
NameStyle::Plausible => {
let noun = PLAUSIBLE_NOUN[self.rng.below(PLAUSIBLE_NOUN.len() as u64) as usize];
if self.avoid_class {
let suffix =
PLAUSIBLE_SUFFIX[self.rng.below(PLAUSIBLE_SUFFIX.len() as u64) as usize];
format!("{noun}{suffix}")
} else {
let verb = PLAUSIBLE_VERB[self.rng.below(PLAUSIBLE_VERB.len() as u64) as usize];
format!("{verb}{noun}")
}
}
}
}
@ -164,52 +180,3 @@ fn capitalize(s: &str) -> String {
None => String::new(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
use super::*;
#[test]
fn names_are_unique_and_valid() {
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
let mut seen = HashSet::new();
for _ in 0..500 {
let name = names.next_name();
assert!(seen.insert(name.clone()), "повтор имени {name}");
assert!(!KEYWORDS.contains(&name.as_str()));
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
}
}
#[test]
fn avoids_reserved_member_names() {
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
for _ in 0..500 {
let name = names.next_name();
assert!(!AVOID_MEMBER.contains(&name.as_str()));
}
}
#[test]
fn unicode_style_produces_identifier_valid_names() {
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
for _ in 0..100 {
let name = names.next_name();
assert!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
);
}
}
#[test]
fn reserved_names_never_reissued() {
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
names.reserve("abc");
for _ in 0..300 {
assert_ne!(names.next_name(), "abc");
}
}
}

View file

@ -0,0 +1,76 @@
//! Тесты генератора имён: уникальность, валидность, форма по стилю.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::collections::HashSet;
use noxis_core::{NameStyle, Rng};
use super::NameGenerator;
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
#[test]
fn names_are_unique_and_valid() {
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
let mut seen = HashSet::new();
for _ in 0..500 {
let name = names.next_name();
assert!(seen.insert(name.clone()), "повтор имени {name}");
assert!(!KEYWORDS.contains(&name.as_str()));
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
}
}
#[test]
fn avoids_reserved_member_names() {
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
for _ in 0..500 {
let name = names.next_name();
assert!(!AVOID_MEMBER.contains(&name.as_str()));
}
}
#[test]
fn unicode_style_produces_identifier_valid_names() {
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
for _ in 0..100 {
let name = names.next_name();
assert!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
);
}
}
#[test]
fn reserved_names_never_reissued() {
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
names.reserve("abc");
for _ in 0..300 {
assert_ne!(names.next_name(), "abc");
}
}
#[test]
fn plausible_class_names_look_like_noun_suffix() {
let mut names = NameGenerator::new(Rng::from_seed(5), NameStyle::Plausible).for_classes();
for _ in 0..50 {
let name = names.next_name();
assert!(name.starts_with(|c: char| c.is_ascii_uppercase()), "{name}");
// Два слова подряд слиплись в CamelCase без разделителей.
assert!(
name.chars().filter(|c| c.is_ascii_uppercase()).count() >= 2,
"{name}"
);
}
}
#[test]
fn plausible_member_names_look_like_verb_noun() {
let mut names = NameGenerator::new(Rng::from_seed(6), NameStyle::Plausible);
for _ in 0..50 {
let name = names.next_name();
assert!(name.starts_with(|c: char| c.is_ascii_lowercase()), "{name}");
assert!(!KEYWORDS.contains(&name.as_str()));
}
}

View file

@ -180,6 +180,7 @@ pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec<String> {
let style = match cfg.rename.class_style {
NameStyle::Unicode => NameStyle::Unicode,
NameStyle::IlO0 => NameStyle::IlO0,
NameStyle::Plausible => NameStyle::Plausible,
_ => NameStyle::MixedLatin,
};
let mut names = NameGenerator::new(rng.clone(), style).for_classes();