Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@
- 검증: 검사기 `✓ 43 named … all 268 loadable` rc=0 · `check-worklog-json` rc=0 · `check-dod-ci-parity` rc=0(명령 7개) · `cargo fmt` rc=0 · `mbvar-guard` rc=0(위반 0).
- ★후속 추천: **loadable 집합을 «파싱으로 재유도»하지 말고 loader 쪽에서 «내보낼» 것인가**(M — 이번 결함 3건이 전부 그 재유도 자리였다). 상세 = `docs/worklog/2026-09-18-named-exception-classes-are-loadable.md`.

## [2026-09-18] 부트스트랩 인자 거부가 «어느 인자·무엇을 찾았는지» 말한다 (rustjava-bootstrap-argument-diagnostic-names-index-and-tag)
- 무엇을: 채택 제안 `2026-09-18-bootstrap-argument-diagnostic-sequencing#p0`(worklog json 기록). ★**새 기능이 아니라 «이미 계산된 값을 버리지 않는 것»** — 술어가 `false` 를 내는 그 자리에 index 와 찾은 항목이 **손에 있었다**.
- ★**ⓑ 대안을 만들지 않았다** — 같은 enum 의 `UnsupportedVersion(u16)` 이 **이미 3층을 관통해 경계에서 `format!` 되는 패턴**이라 그것을 **복제**했다(새 진단 체계 **0**).
- ★**전/후**(같은 픽스처 `test-data/ldc/LdcDynamicBSMArgPastEnd.class` · CLI 실제 출력):
`BEFORE java.lang.ClassFormatError: a bootstrap method argument names nothing or is not a loadable constant`
`AFTER java.lang.ClassFormatError: bootstrap method #0 argument #0 names no constant pool entry`
- ★**세 요구 3/3**: expected(문면) · index(`argument_index` ★+`method_index` — OpenJDK 는 그걸 안 말해 여럿일 때 모호하다) · actual(가리킨 상수의 **종류 이름**). ★태그를 **번호가 아니라 이름**으로 나른다 — 태그 바이트가 파싱 후 남지 않아 번호는 «아무도 분기 안 하는 두 번째 표»를 만들게 된다.
- ★★**양방향의 급소** — `StringConcat` 은 인자가 **1개**라 index 0 이 **계산이든 하드코딩이든 통과한다**. 그래서 정적 인자 **3개**짜리 `Lambda.class` 의 **#0 과 #2** 를 각각 망가뜨려 ★**보고된 index 가 0 ↔ 2 로 따라가는 것**을 잠갔다.
- ★**소비자 전수**: `InvalidFormat` 34사용처 중 **구조적 소비자는 1개**(`jvm-bytecode` 의 `From` match) — **arm 을 더했을 뿐 고치지 않았다**. 나머지 14규칙 무변.
- ★**대가**(재서 적는다): ★**타입이 «커지지 않았다»** — `Copy` 유지 · 크기 불변(시험으로 잠금) ⇒ 할당 0. ※제안이 경계한 「소유 데이터로 커진다」는 **이 설계에선 일어나지 않았다**. ★**두 경로 공존**의 혼동은 변형 docstring 한 줄로 못박았다 · ★이 규칙의 **메시지 문면이 바뀌어** 그것을 단언하던 시험 2곳과 grep 습관이 깨진다.
- 검증: `cargo test -p classfile` **16 passed**(전 13) · `--test test_class_format` **22 passed** · `cargo test --all` rc=0.
- ★후속 추천: ⑴**다른 14규칙 중 «수를 아는데 버리는» 것을 세라**(S — 아직 그 수를 모른다) ⑵`ClassDefinitionError` 의 `&'static str` 두 변형도 같은 한계(M · 이 회차는 나란히 더해 **우회**했다). 상세 = `docs/worklog/2026-09-18-bootstrap-argument-index-and-tag.md`.

## [2026-09-18] 이름으로 부르는 예외 클래스가 «실을 수 있는» 것인가 — 한 자리에서 대조한다 (rustjava-lock-every-named-exception-class-is-loadable)
- 무엇을: 채택 제안 `2026-09-17-string-concat-recipe-arity#p0`(worklog json `adoptedProposals` 기록). 산출물 = `scripts/check-named-exception-classes-are-loadable.py` **한 자리** + CI job + DoD 한 줄. ★**런타임 클래스 추가 0 · `.unwrap()` 무접촉.**
- ★**전제를 코드로 확인했다**(총괄 선실측 없음): `jvm/src/jvm.rs:943-950` 의 `new_class(...).await.`★**`unwrap()`** ⇒ 부트스트랩 로더가 이름을 못 풀면 **Java 예외가 아니라 프로세스가 죽는다**. ★**자기 참조다** — `:842` 가 클래스 부재를 `exception("java/lang/NoClassDefFoundError", …)` 로 보고하므로 **오류 경로 자신의 클래스**가 실려야 한다.
Expand Down
8 changes: 8 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@
★**수 전/후**: 이름 41→**43** · 호출부 812→**846** · loadable 263→**268**(왜인지 기재).
★**ⓒ 편집 «전»에 새 red 위험 측정** — 늘어나는 2이름 모두 등재 ⇒ **새 red 0**.
★**시간 유의차 없음**(전 1.42/1.67/1.46 ↔ 후 2.01/1.69/1.19 · 구간 겹침). ★런타임 클래스 추가 0 · `protos` 무접촉.
- [rustjava-bootstrap-argument-diagnostic-names-index-and-tag] ★★**부트스트랩 인자 거부가 «어느 인자·무엇을» 말한다.** 채택 제안 `2026-09-18-bootstrap-argument-diagnostic-sequencing#p0`.
★**전제 확인(코드)**: 술어가 `false` 를 내는 자리에 `index` 와 `constant_pool.get(index)` 가 **손에 있었다** — `bool` 이 둘을 버렸다.
★**새 체계 0** — 같은 enum 의 `UnsupportedVersion(u16)` 이 이미 3층을 관통하는 패턴이라 **복제**했다.
★**전/후**: `ClassFormatError: a bootstrap method argument names nothing or is not a loadable constant` → ★`ClassFormatError: bootstrap method #0 argument #0 names no constant pool entry`(CLI 실측).
★**세 요구 3/3**(expected·index+method_index·actual) · 태그는 **이름**으로(번호는 표가 하나 더 는다).
★**양방향 급소**: `StringConcat` 은 인자 1개라 index 0 이 하드코딩이어도 통과 ⇒ **인자 3개 `Lambda.class` 의 #0↔#2** 로 **index 가 따라가는 것**을 잠갔다.
★**소비자 1개**(`jvm-bytecode` `From`) — arm 추가뿐, 14규칙 무변. ★**타입 «안 커졌다»**(`Copy`·크기 불변 시험) · ★메시지 문면이 바뀌어 단언 2곳이 바뀐다.
★`-p classfile` **16 passed**(전 13) · `test_class_format` **22** · `--all` rc=0.
- [rustjava-lock-every-named-exception-class-is-loadable] ★★**이름으로 부르는 예외 클래스가 «실을 수 있는» 것인가 — 대조 한 자리.** 채택 제안 `2026-09-17-string-concat-recipe-arity#p0`(worklog json 기록).
★**전제 확인(코드)**: `jvm/src/jvm.rs:943-950` `new_class(...).await.`**`unwrap()`** ⇒ 못 싣는 이름은 **throw 가 아니라 패닉**. ★자기 참조 — `:842` 가 부재를 `exception("java/lang/NoClassDefFoundError")` 로 보고한다.
★**베이스라인 0**(★게이트² 정정 후): `exception(` 리터럴 **43 고유 / 846 호출부** ↔ 등재 **268**(`as_proto` 265 + `list_proto` 3). ★초판의 41/812/263 은 **전부 과소**였다(다중 줄 34 · `list_proto` 3 · 짧은 이름 충돌). ★제안의 「72」는 여전히 **재현 안 됨**.
Expand Down
21 changes: 21 additions & 0 deletions classfile/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,26 @@
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClassFileError {
InvalidFormat(&'static str),
/// A bootstrap method argument that is not a loadable constant, carrying the two things the
/// predicate already knows at the moment it refuses: *which* argument, and what it found there.
///
/// ★ Which variant to use: `InvalidFormat` is for a rule that has nothing to point at, and that
/// is still most of them. Use this one only when a number is already in hand — folding it into
/// prose is the thing this variant exists to stop. `UnsupportedVersion` is the same shape and
/// predates it, so this is the established way here rather than a second scheme.
///
/// It stays `Copy`: two `u16`s and a `&'static str`, no owned data.
InvalidBootstrapArgument {
/// Position of the `BootstrapMethods` entry, zero-based, as the attribute stores them.
method_index: u16,
/// Position within that entry's argument list, zero-based. OpenJDK calls this
/// `argument_index` and prints it without the method, which is ambiguous when a class has
/// more than one bootstrap method; both are carried here for that reason.
argument_index: u16,
/// The kind of constant the argument actually names, or `None` when the index names no
/// pool entry at all — "names nothing" and "names the wrong kind" are different failures
/// and the message says which.
actual: Option<&'static str>,
},
UnsupportedVersion(u16),
}
72 changes: 53 additions & 19 deletions classfile/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,10 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
"class file version does not support a constant tag it carries",
));
}
if !bootstrap_method_static_arguments_are_in_the_pool(class) {
return Err(ClassFileError::InvalidFormat(
// The rule is wider than the function name: the docstring above says the argument must also
// be a loadable constant, and OpenJDK says the same ("bad constant type"). The name stayed
// behind when the rule widened; renaming it is not this round's scope, so the cause is what
// gets the wording right.
"a bootstrap method argument names nothing or is not a loadable constant",
));
}
// The rule is wider than the function name: the docstring below says the argument must also be a
// loadable constant, and OpenJDK says the same ("bad constant type"). The name stayed behind when
// the rule widened; renaming it is not this round's scope.
bootstrap_method_static_arguments_are_in_the_pool(class)?;
if !bootstrap_method_indices_resolve(class) {
return Err(ClassFileError::InvalidFormat("a dynamic constant names no bootstrap method"));
}
Expand Down Expand Up @@ -165,11 +160,18 @@ fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool {
/// One consequence worth naming: a long or double occupies two pool slots and only the first is
/// usable (JVMS 4.4.5), so the second has no entry in this map and an argument naming it is
/// rejected. That is the intended reading of "valid index" rather than an accident of the map.
fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool {
class.attributes.iter().all(|attribute| match attribute {
AttributeInfo::BootstrapMethods(methods) => methods.iter().all(|method| {
method.arguments.iter().all(|index| {
class.constant_pool.get(index).is_some_and(|item| {
/// ★ It returns `Result` rather than `bool` for one reason: at the moment it refuses, it is holding
/// the position of the argument and the entry it found, and a `bool` throws both away. The caller
/// then had to describe the failure in prose it did not measure. Nothing else about the rule moved.
fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> Result<(), ClassFileError> {
for attribute in &class.attributes {
let AttributeInfo::BootstrapMethods(methods) = attribute else {
continue;
};
for (method_index, method) in methods.iter().enumerate() {
for (argument_index, index) in method.arguments.iter().enumerate() {
let entry = class.constant_pool.get(index);
let loadable = entry.is_some_and(|item| {
matches!(
item,
ConstantPoolItem::Integer(_)
Expand All @@ -182,11 +184,43 @@ fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool
| ConstantPoolItem::MethodType { .. }
| ConstantPoolItem::Dynamic { .. }
)
})
})
}),
_ => true,
})
});
if !loadable {
return Err(ClassFileError::InvalidBootstrapArgument {
method_index: method_index as u16,
argument_index: argument_index as u16,
actual: entry.map(constant_kind_name),
});
}
}
}
}
Ok(())
}

/// The JVMS 4.4 name of a constant's kind, for saying what an argument actually named.
///
/// ★ A name and not the numeric tag: the tag byte is not kept after parsing, so reporting it would
/// mean a second table to hold numbers nothing branches on. The name is what a reader of the
/// message needs, and it is derived from the variant rather than stored, so it cannot drift.
fn constant_kind_name(item: &ConstantPoolItem) -> &'static str {
match item {
ConstantPoolItem::Utf8(_) => "Utf8",
ConstantPoolItem::Integer(_) => "Integer",
ConstantPoolItem::Float(_) => "Float",
ConstantPoolItem::Long(_) => "Long",
ConstantPoolItem::Double(_) => "Double",
ConstantPoolItem::Class { .. } => "Class",
ConstantPoolItem::String { .. } => "String",
ConstantPoolItem::Fieldref { .. } => "Fieldref",
ConstantPoolItem::Methodref { .. } => "Methodref",
ConstantPoolItem::InterfaceMethodref { .. } => "InterfaceMethodref",
ConstantPoolItem::NameAndType { .. } => "NameAndType",
ConstantPoolItem::MethodHandle { .. } => "MethodHandle",
ConstantPoolItem::MethodType { .. } => "MethodType",
ConstantPoolItem::Dynamic { .. } => "Dynamic",
ConstantPoolItem::InvokeDynamic { .. } => "InvokeDynamic",
}
}

/// JVMS 4.4.10 and 4.7.23: `bootstrap_method_attr_index` is an index into the `bootstrap_methods`
Expand Down
95 changes: 91 additions & 4 deletions classfile/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,96 @@ fn test_a_bootstrap_argument_that_is_not_a_loadable_constant_is_rejected() {

assert_eq!(
ClassInfo::parse(&mutated).err(),
Some(ClassFileError::InvalidFormat(
"a bootstrap method argument names nothing or is not a loadable constant"
)),
"a bootstrap argument naming a Utf8 is not a loadable constant"
Some(ClassFileError::InvalidBootstrapArgument {
method_index: 0,
argument_index: 0,
actual: Some("Utf8"),
}),
"a bootstrap argument naming a Utf8 is not a loadable constant, and the refusal says which argument and what it found"
);
}

/// ★ The index has to *follow the input*, not be a constant that happens to read correctly.
///
/// `StringConcat` above has a single argument, so its `argument_index` is 0 whether the code
/// computes it or hardcodes it — that test cannot tell the two apart. A lambda's bootstrap method
/// takes three (samMethodType, implMethod, instantiatedMethodType), so breaking a different one has
/// to move the number. Both mutations are the same edit at a different offset, which is what makes
/// the pair a control for each other.
#[test]
fn test_the_reported_bootstrap_argument_index_follows_the_broken_argument() {
let lambda = include_bytes!("../../test-data/indy/Lambda.class");
let class = ClassInfo::parse(lambda).unwrap();
let arguments = &bootstrap_methods(&class)[0].arguments;
assert_eq!(arguments.len(), 3, "a LambdaMetafactory call site carries three static arguments");

// Derived from the parse rather than hardcoded, so a regenerated fixture moves the window with it.
let mut window = vec![0u8, arguments.len() as u8];
for argument in arguments {
window.extend_from_slice(&argument.to_be_bytes());
}
let at = lambda
.windows(window.len())
.position(|candidate| candidate == window)
.expect("test-data/indy/Lambda.class layout changed; the BootstrapMethods argument list moved");

// Entry #4 is a Utf8 ("java/lang/Object"): a real pool entry that is not a loadable constant.
// The kind is not assumed — the assertion below reads it back, so a regenerated fixture whose
// #4 is something else fails here rather than passing for the wrong reason.
let utf8: u16 = 4;
assert!(class.constant_pool.get(&utf8).is_some(), "the replacement index must be in the pool");

for broken in [0usize, 2usize] {
let mut mutated = lambda.to_vec();
let offset = at + 2 + broken * 2;
mutated[offset..offset + 2].copy_from_slice(&utf8.to_be_bytes());

assert_eq!(
ClassInfo::parse(&mutated).err(),
Some(ClassFileError::InvalidBootstrapArgument {
method_index: 0,
argument_index: broken as u16,
actual: Some("Utf8"),
}),
"breaking argument #{broken} must report argument #{broken}"
);
}
}

/// The other half of the rule: an index that names no entry at all reports `actual: None`, and the
/// message says "names no constant pool entry" rather than guessing a kind.
#[test]
fn test_a_bootstrap_argument_naming_nothing_reports_no_kind() {
let string_concat = include_bytes!("../../test-data/indy/StringConcat.class");
let class = ClassInfo::parse(string_concat).unwrap();
let window = [0u8, 1, 0, 34, 0, 1, 0, 32];
let at = string_concat
.windows(window.len())
.position(|candidate| candidate == window)
.expect("test-data/indy/StringConcat.class layout changed; the BootstrapMethods entry moved");

let past_end = u16::MAX;
assert!(class.constant_pool.get(&past_end).is_none(), "the index must name nothing");

let mut mutated = string_concat.to_vec();
mutated[at + 6..at + 8].copy_from_slice(&past_end.to_be_bytes());

assert_eq!(
ClassInfo::parse(&mutated).err(),
Some(ClassFileError::InvalidBootstrapArgument {
method_index: 0,
argument_index: 0,
actual: None,
})
);
}

/// The cost of the variant, measured rather than asserted in prose: it stays `Copy` and the enum
/// does not grow, because two `u16`s and a `&'static str` fit in the space `InvalidFormat` already
/// needed for its string.
#[test]
fn test_the_structured_variant_does_not_grow_the_error_type() {
assert_eq!(size_of::<ClassFileError>(), size_of::<&'static str>() + size_of::<usize>());
fn assert_copy<T: Copy>() {}
assert_copy::<ClassFileError>();
}
Loading
Loading