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

@ -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");
}
}
}