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,155 +0,0 @@
//! Сборка constant pool при записи: строки нашего IR → индексы backend'а.
//!
//! Пул строится с нуля детерминированным обходом (классы, поля, методы,
//! атрибуты, код), поэтому одинаковый IR даёт одинаковые байты. Индексы
//! `Raw`-атрибутов при этом не переносятся — см. `super::attrs`.
use ristretto_classfile::{ConstantPool, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::{Const, InvokeKind};
/// Строитель пула; живёт только внутри одного `write`.
pub(crate) struct Pool {
pool: ConstantPool<'static>,
}
impl Pool {
pub(crate) fn new() -> Self {
Pool {
pool: ConstantPool::default(),
}
}
pub(super) fn utf8(&mut self, value: &str) -> Result<u16, ClassfileError> {
self.pool.add_utf8(value).map_err(write_err)
}
pub(super) fn name_and_type(
&mut self,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_name_and_type(name, descriptor)
.map_err(write_err)
}
pub(super) fn class(&mut self, name: &str) -> Result<u16, ClassfileError> {
self.pool.add_class(name).map_err(write_err)
}
pub(super) fn field_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let owner = self.class(owner)?;
self.pool
.add_field_ref(owner, name, descriptor)
.map_err(write_err)
}
pub(super) fn method_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
kind: InvokeKind,
) -> Result<u16, ClassfileError> {
let owner = self.class(owner)?;
match kind {
InvokeKind::Interface => self
.pool
.add_interface_method_ref(owner, name, descriptor)
.map_err(write_err),
_ => self
.pool
.add_method_ref(owner, name, descriptor)
.map_err(write_err),
}
}
/// Константа для `ldc`/`ldc2_w`; `dynamic` запрещён (только через bootstrap).
pub(super) fn constant(&mut self, value: &Const) -> Result<u16, ClassfileError> {
match value {
Const::Int(v) => self.pool.add_integer(*v).map_err(write_err),
Const::Float(v) => self.pool.add_float(*v).map_err(write_err),
Const::Long(v) => self.pool.add_long(*v).map_err(write_err),
Const::Double(v) => self.pool.add_double(*v).map_err(write_err),
Const::String(v) => self.pool.add_string(v).map_err(write_err),
Const::Class(v) => self.class(v),
Const::MethodType(v) => self.pool.add_method_type(v).map_err(write_err),
Const::MethodHandle {
kind,
owner,
name,
descriptor,
itf,
} => self.method_handle(*kind, owner, name, descriptor, *itf),
Const::Dynamic { .. } => Err(ClassfileError::Write(
"CONSTANT_Dynamic пишется только через BootstrapMethods".into(),
)),
}
}
pub(super) fn method_handle(
&mut self,
kind: u8,
owner: &str,
name: &str,
descriptor: &str,
itf: bool,
) -> Result<u16, ClassfileError> {
let reference_kind = ReferenceKind::try_from(kind).map_err(|e| {
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
})?;
let reference = if itf {
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
} else if matches!(
reference_kind,
ReferenceKind::GetField
| ReferenceKind::GetStatic
| ReferenceKind::PutField
| ReferenceKind::PutStatic
) {
self.field_ref(owner, name, descriptor)?
} else {
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
};
self.pool
.add_method_handle(reference_kind, reference)
.map_err(write_err)
}
pub(super) fn invoke_dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_invoke_dynamic(bootstrap, name, descriptor)
.map_err(write_err)
}
pub(super) fn dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_dynamic(bootstrap, name, descriptor)
.map_err(write_err)
}
pub(super) fn finish(self) -> ConstantPool<'static> {
self.pool
}
}
fn write_err(error: ristretto_classfile::Error) -> ClassfileError {
ClassfileError::Write(error.to_string())
}

View file

@ -0,0 +1,155 @@
//! Сборка constant pool при записи: строки нашего IR → индексы backend'а.
//!
//! Пул строится с нуля детерминированным обходом (классы, поля, методы,
//! атрибуты, код), поэтому одинаковый IR даёт одинаковые байты. Индексы
//! `Raw`-атрибутов при этом не переносятся — см. `super::attrs`.
//!
//! Backend не дедуплицирует: `add_*` всегда толкает новый элемент. Кэшируем
//! семантические ключи сами — иначе `LocalVariableTable` и
//! `LocalVariableTypeTable` получат разные `name_index` для одного имени, и
//! HotSpot отвергнет класс (`LVTT entry ... does not match any LVT entry`).
//! Порядок обхода детерминирован, поэтому кэш меняет только компактность пула.
//! Референсы (поле/метод/хендл/динамик) — в [`refs`].
use std::collections::HashMap;
use ristretto_classfile::{Constant, ConstantPool};
use crate::error::ClassfileError;
use crate::insn::Const;
mod refs;
/// Строитель пула; живёт только внутри одного `write`.
pub(crate) struct Pool {
pub(super) pool: ConstantPool<'static>,
pub(super) utf8s: HashMap<String, u16>,
pub(super) classes: HashMap<String, u16>,
pub(super) name_types: HashMap<(String, String), u16>,
pub(super) strings: HashMap<String, u16>,
pub(super) method_types: HashMap<String, u16>,
pub(super) field_refs: HashMap<(String, String, String), u16>,
pub(super) method_refs: HashMap<(String, String, String, bool), u16>,
pub(super) handles: HashMap<(u8, String, String, String, bool), u16>,
pub(super) dynamics: HashMap<(u16, String, String, bool), u16>,
}
impl Pool {
pub(crate) fn new() -> Self {
Pool {
pool: ConstantPool::default(),
utf8s: HashMap::new(),
classes: HashMap::new(),
name_types: HashMap::new(),
strings: HashMap::new(),
method_types: HashMap::new(),
field_refs: HashMap::new(),
method_refs: HashMap::new(),
handles: HashMap::new(),
dynamics: HashMap::new(),
}
}
pub(super) fn utf8(&mut self, value: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.utf8s.get(value) {
return Ok(index);
}
let index = self.pool.add_utf8(value).map_err(write_err)?;
self.utf8s.insert(value.to_string(), index);
Ok(index)
}
pub(super) fn name_and_type(
&mut self,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let key = (name.to_string(), descriptor.to_string());
if let Some(&index) = self.name_types.get(&key) {
return Ok(index);
}
// Собираем из примитивов, а не `add_name_and_type`: последний толкает
// собственные `Utf8`-строки мимо нашего кэша (дубли в пуле).
let name_index = self.utf8(name)?;
let descriptor_index = self.utf8(descriptor)?;
let index = self
.pool
.add(Constant::NameAndType {
name_index,
descriptor_index,
})
.map_err(write_err)?;
self.name_types.insert(key, index);
Ok(index)
}
pub(super) fn class(&mut self, name: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.classes.get(name) {
return Ok(index);
}
let name_index = self.utf8(name)?;
let index = self
.pool
.add(Constant::Class(name_index))
.map_err(write_err)?;
self.classes.insert(name.to_string(), index);
Ok(index)
}
/// Константа для `ldc`/`ldc2_w`; `dynamic` запрещён (только через bootstrap).
pub(super) fn constant(&mut self, value: &Const) -> Result<u16, ClassfileError> {
match value {
Const::Int(v) => self.pool.add_integer(*v).map_err(write_err),
Const::Float(v) => self.pool.add_float(*v).map_err(write_err),
Const::Long(v) => self.pool.add_long(*v).map_err(write_err),
Const::Double(v) => self.pool.add_double(*v).map_err(write_err),
Const::String(v) => self.string(v),
Const::Class(v) => self.class(v),
Const::MethodType(v) => self.method_type(v),
Const::MethodHandle {
kind,
owner,
name,
descriptor,
itf,
} => self.method_handle(*kind, owner, name, descriptor, *itf),
Const::Dynamic { .. } => Err(ClassfileError::Write(
"CONSTANT_Dynamic пишется только через BootstrapMethods".into(),
)),
}
}
fn string(&mut self, value: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.strings.get(value) {
return Ok(index);
}
let string_index = self.utf8(value)?;
let index = self
.pool
.add(Constant::String(string_index))
.map_err(write_err)?;
self.strings.insert(value.to_string(), index);
Ok(index)
}
fn method_type(&mut self, descriptor: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.method_types.get(descriptor) {
return Ok(index);
}
let descriptor_index = self.utf8(descriptor)?;
let index = self
.pool
.add(Constant::MethodType(descriptor_index))
.map_err(write_err)?;
self.method_types.insert(descriptor.to_string(), index);
Ok(index)
}
pub(super) fn finish(self) -> ConstantPool<'static> {
self.pool
}
}
fn write_err(error: ristretto_classfile::Error) -> ClassfileError {
ClassfileError::Write(error.to_string())
}

View file

@ -0,0 +1,162 @@
//! Референсы constant pool: `Fieldref`/`Methodref`/`MethodHandle`/`Dynamic`.
//!
//! Это `impl Pool` из [`super`]; вынесено отдельно, чтобы материнский файл
//! держался в лимите структуры. Каждый билдер мемоизирован по семантическому
//! ключу (owner/name/descriptor/…) — одинаковая ссылка всегда получает один
//! индекс, а пул не раздувается дублями (раздувание иначе расширяет `ldc`
//! в `ldc_w` и меняет длину кода при переносе).
use ristretto_classfile::{Constant, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::InvokeKind;
use super::{Pool, write_err};
impl Pool {
pub(crate) fn field_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let key = (owner.to_string(), name.to_string(), descriptor.to_string());
if let Some(&index) = self.field_refs.get(&key) {
return Ok(index);
}
let class_index = self.class(owner)?;
let name_and_type_index = self.name_and_type(name, descriptor)?;
let index = self
.pool
.add(Constant::FieldRef {
class_index,
name_and_type_index,
})
.map_err(write_err)?;
self.field_refs.insert(key, index);
Ok(index)
}
pub(crate) fn method_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
kind: InvokeKind,
) -> Result<u16, ClassfileError> {
let itf = matches!(kind, InvokeKind::Interface);
let key = (
owner.to_string(),
name.to_string(),
descriptor.to_string(),
itf,
);
if let Some(&index) = self.method_refs.get(&key) {
return Ok(index);
}
let class_index = self.class(owner)?;
let name_and_type_index = self.name_and_type(name, descriptor)?;
let constant = if itf {
Constant::InterfaceMethodRef {
class_index,
name_and_type_index,
}
} else {
Constant::MethodRef {
class_index,
name_and_type_index,
}
};
let index = self.pool.add(constant).map_err(write_err)?;
self.method_refs.insert(key, index);
Ok(index)
}
pub(crate) fn method_handle(
&mut self,
kind: u8,
owner: &str,
name: &str,
descriptor: &str,
itf: bool,
) -> Result<u16, ClassfileError> {
let key = (
kind,
owner.to_string(),
name.to_string(),
descriptor.to_string(),
itf,
);
if let Some(&index) = self.handles.get(&key) {
return Ok(index);
}
let reference_kind = ReferenceKind::try_from(kind).map_err(|e| {
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
})?;
let reference = if itf {
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
} else if matches!(
reference_kind,
ReferenceKind::GetField
| ReferenceKind::GetStatic
| ReferenceKind::PutField
| ReferenceKind::PutStatic
) {
self.field_ref(owner, name, descriptor)?
} else {
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
};
let index = self
.pool
.add_method_handle(reference_kind, reference)
.map_err(write_err)?;
self.handles.insert(key, index);
Ok(index)
}
pub(crate) fn invoke_dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.dynamic_like(bootstrap, name, descriptor, true)
}
pub(crate) fn dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.dynamic_like(bootstrap, name, descriptor, false)
}
fn dynamic_like(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
invoke: bool,
) -> Result<u16, ClassfileError> {
let key = (bootstrap, name.to_string(), descriptor.to_string(), invoke);
if let Some(&index) = self.dynamics.get(&key) {
return Ok(index);
}
let name_and_type_index = self.name_and_type(name, descriptor)?;
let constant = if invoke {
Constant::InvokeDynamic {
bootstrap_method_attr_index: bootstrap,
name_and_type_index,
}
} else {
Constant::Dynamic {
bootstrap_method_attr_index: bootstrap,
name_and_type_index,
}
};
let index = self.pool.add(constant).map_err(write_err)?;
self.dynamics.insert(key, index);
Ok(index)
}
}