Skip to content

Commit 1ec83a1

Browse files
authored
[rustjava-bootstrap-argument-diagnostic-names-index-and-tag] fix(classfile): 부트스트랩 인자 거부가 어느 인자·무엇을 찾았는지 말한다 (#73)
[rustjava-bootstrap-argument-diagnostic-names-index-and-tag] fix(classfile): 부트스트랩 인자 거부가 어느 인자·무엇을 찾았는지 말한다
2 parents d63d319 + 41e7240 commit 1ec83a1

11 files changed

Lines changed: 377 additions & 24 deletions

File tree

‎REPORT.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@
204204
- 검증: 검사기 `✓ 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).
205205
- ★후속 추천: **loadable 집합을 «파싱으로 재유도»하지 말고 loader 쪽에서 «내보낼» 것인가**(M — 이번 결함 3건이 전부 그 재유도 자리였다). 상세 = `docs/worklog/2026-09-18-named-exception-classes-are-loadable.md`.
206206

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

‎STATE.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@
122122
★**수 전/후**: 이름 41→**43** · 호출부 812→**846** · loadable 263→**268**(왜인지 기재).
123123
★**ⓒ 편집 «전»에 새 red 위험 측정** — 늘어나는 2이름 모두 등재 ⇒ **새 red 0**.
124124
★**시간 유의차 없음**(전 1.42/1.67/1.46 ↔ 후 2.01/1.69/1.19 · 구간 겹침). ★런타임 클래스 추가 0 · `protos` 무접촉.
125+
- [rustjava-bootstrap-argument-diagnostic-names-index-and-tag] ★★**부트스트랩 인자 거부가 «어느 인자·무엇을» 말한다.** 채택 제안 `2026-09-18-bootstrap-argument-diagnostic-sequencing#p0`.
126+
★**전제 확인(코드)**: 술어가 `false` 를 내는 자리에 `index` 와 `constant_pool.get(index)` 가 **손에 있었다** — `bool` 이 둘을 버렸다.
127+
★**새 체계 0** — 같은 enum 의 `UnsupportedVersion(u16)` 이 이미 3층을 관통하는 패턴이라 **복제**했다.
128+
★**전/후**: `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 실측).
129+
★**세 요구 3/3**(expected·index+method_index·actual) · 태그는 **이름**으로(번호는 표가 하나 더 는다).
130+
★**양방향 급소**: `StringConcat` 은 인자 1개라 index 0 이 하드코딩이어도 통과 ⇒ **인자 3개 `Lambda.class` 의 #0↔#2** 로 **index 가 따라가는 것**을 잠갔다.
131+
★**소비자 1개**(`jvm-bytecode` `From`) — arm 추가뿐, 14규칙 무변. ★**타입 «안 커졌다»**(`Copy`·크기 불변 시험) · ★메시지 문면이 바뀌어 단언 2곳이 바뀐다.
132+
★`-p classfile` **16 passed**(전 13) · `test_class_format` **22** · `--all` rc=0.
125133
- [rustjava-lock-every-named-exception-class-is-loadable] ★★**이름으로 부르는 예외 클래스가 «실을 수 있는» 것인가 — 대조 한 자리.** 채택 제안 `2026-09-17-string-concat-recipe-arity#p0`(worklog json 기록).
126134
★**전제 확인(코드)**: `jvm/src/jvm.rs:943-950` `new_class(...).await.`**`unwrap()`** ⇒ 못 싣는 이름은 **throw 가 아니라 패닉**. ★자기 참조 — `:842` 가 부재를 `exception("java/lang/NoClassDefFoundError")` 로 보고한다.
127135
★**베이스라인 0**(★게이트² 정정 후): `exception(` 리터럴 **43 고유 / 846 호출부** ↔ 등재 **268**(`as_proto` 265 + `list_proto` 3). ★초판의 41/812/263 은 **전부 과소**였다(다중 줄 34 · `list_proto` 3 · 짧은 이름 충돌). ★제안의 「72」는 여전히 **재현 안 됨**.

‎classfile/src/error.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,26 @@
1010
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1111
pub enum ClassFileError {
1212
InvalidFormat(&'static str),
13+
/// A bootstrap method argument that is not a loadable constant, carrying the two things the
14+
/// predicate already knows at the moment it refuses: *which* argument, and what it found there.
15+
///
16+
/// ★ Which variant to use: `InvalidFormat` is for a rule that has nothing to point at, and that
17+
/// is still most of them. Use this one only when a number is already in hand — folding it into
18+
/// prose is the thing this variant exists to stop. `UnsupportedVersion` is the same shape and
19+
/// predates it, so this is the established way here rather than a second scheme.
20+
///
21+
/// It stays `Copy`: two `u16`s and a `&'static str`, no owned data.
22+
InvalidBootstrapArgument {
23+
/// Position of the `BootstrapMethods` entry, zero-based, as the attribute stores them.
24+
method_index: u16,
25+
/// Position within that entry's argument list, zero-based. OpenJDK calls this
26+
/// `argument_index` and prints it without the method, which is ambiguous when a class has
27+
/// more than one bootstrap method; both are carried here for that reason.
28+
argument_index: u16,
29+
/// The kind of constant the argument actually names, or `None` when the index names no
30+
/// pool entry at all — "names nothing" and "names the wrong kind" are different failures
31+
/// and the message says which.
32+
actual: Option<&'static str>,
33+
},
1334
UnsupportedVersion(u16),
1435
}

‎classfile/src/validation.rs‎

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,10 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
3838
"class file version does not support a constant tag it carries",
3939
));
4040
}
41-
if !bootstrap_method_static_arguments_are_in_the_pool(class) {
42-
return Err(ClassFileError::InvalidFormat(
43-
// The rule is wider than the function name: the docstring above says the argument must also
44-
// be a loadable constant, and OpenJDK says the same ("bad constant type"). The name stayed
45-
// behind when the rule widened; renaming it is not this round's scope, so the cause is what
46-
// gets the wording right.
47-
"a bootstrap method argument names nothing or is not a loadable constant",
48-
));
49-
}
41+
// The rule is wider than the function name: the docstring below says the argument must also be a
42+
// loadable constant, and OpenJDK says the same ("bad constant type"). The name stayed behind when
43+
// the rule widened; renaming it is not this round's scope.
44+
bootstrap_method_static_arguments_are_in_the_pool(class)?;
5045
if !bootstrap_method_indices_resolve(class) {
5146
return Err(ClassFileError::InvalidFormat("a dynamic constant names no bootstrap method"));
5247
}
@@ -165,11 +160,18 @@ fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool {
165160
/// One consequence worth naming: a long or double occupies two pool slots and only the first is
166161
/// usable (JVMS 4.4.5), so the second has no entry in this map and an argument naming it is
167162
/// rejected. That is the intended reading of "valid index" rather than an accident of the map.
168-
fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool {
169-
class.attributes.iter().all(|attribute| match attribute {
170-
AttributeInfo::BootstrapMethods(methods) => methods.iter().all(|method| {
171-
method.arguments.iter().all(|index| {
172-
class.constant_pool.get(index).is_some_and(|item| {
163+
/// ★ It returns `Result` rather than `bool` for one reason: at the moment it refuses, it is holding
164+
/// the position of the argument and the entry it found, and a `bool` throws both away. The caller
165+
/// then had to describe the failure in prose it did not measure. Nothing else about the rule moved.
166+
fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> Result<(), ClassFileError> {
167+
for attribute in &class.attributes {
168+
let AttributeInfo::BootstrapMethods(methods) = attribute else {
169+
continue;
170+
};
171+
for (method_index, method) in methods.iter().enumerate() {
172+
for (argument_index, index) in method.arguments.iter().enumerate() {
173+
let entry = class.constant_pool.get(index);
174+
let loadable = entry.is_some_and(|item| {
173175
matches!(
174176
item,
175177
ConstantPoolItem::Integer(_)
@@ -182,11 +184,43 @@ fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool
182184
| ConstantPoolItem::MethodType { .. }
183185
| ConstantPoolItem::Dynamic { .. }
184186
)
185-
})
186-
})
187-
}),
188-
_ => true,
189-
})
187+
});
188+
if !loadable {
189+
return Err(ClassFileError::InvalidBootstrapArgument {
190+
method_index: method_index as u16,
191+
argument_index: argument_index as u16,
192+
actual: entry.map(constant_kind_name),
193+
});
194+
}
195+
}
196+
}
197+
}
198+
Ok(())
199+
}
200+
201+
/// The JVMS 4.4 name of a constant's kind, for saying what an argument actually named.
202+
///
203+
/// ★ A name and not the numeric tag: the tag byte is not kept after parsing, so reporting it would
204+
/// mean a second table to hold numbers nothing branches on. The name is what a reader of the
205+
/// message needs, and it is derived from the variant rather than stored, so it cannot drift.
206+
fn constant_kind_name(item: &ConstantPoolItem) -> &'static str {
207+
match item {
208+
ConstantPoolItem::Utf8(_) => "Utf8",
209+
ConstantPoolItem::Integer(_) => "Integer",
210+
ConstantPoolItem::Float(_) => "Float",
211+
ConstantPoolItem::Long(_) => "Long",
212+
ConstantPoolItem::Double(_) => "Double",
213+
ConstantPoolItem::Class { .. } => "Class",
214+
ConstantPoolItem::String { .. } => "String",
215+
ConstantPoolItem::Fieldref { .. } => "Fieldref",
216+
ConstantPoolItem::Methodref { .. } => "Methodref",
217+
ConstantPoolItem::InterfaceMethodref { .. } => "InterfaceMethodref",
218+
ConstantPoolItem::NameAndType { .. } => "NameAndType",
219+
ConstantPoolItem::MethodHandle { .. } => "MethodHandle",
220+
ConstantPoolItem::MethodType { .. } => "MethodType",
221+
ConstantPoolItem::Dynamic { .. } => "Dynamic",
222+
ConstantPoolItem::InvokeDynamic { .. } => "InvokeDynamic",
223+
}
190224
}
191225

192226
/// JVMS 4.4.10 and 4.7.23: `bootstrap_method_attr_index` is an index into the `bootstrap_methods`

‎classfile/tests/test.rs‎

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,96 @@ fn test_a_bootstrap_argument_that_is_not_a_loadable_constant_is_rejected() {
367367

368368
assert_eq!(
369369
ClassInfo::parse(&mutated).err(),
370-
Some(ClassFileError::InvalidFormat(
371-
"a bootstrap method argument names nothing or is not a loadable constant"
372-
)),
373-
"a bootstrap argument naming a Utf8 is not a loadable constant"
370+
Some(ClassFileError::InvalidBootstrapArgument {
371+
method_index: 0,
372+
argument_index: 0,
373+
actual: Some("Utf8"),
374+
}),
375+
"a bootstrap argument naming a Utf8 is not a loadable constant, and the refusal says which argument and what it found"
374376
);
375377
}
378+
379+
/// ★ The index has to *follow the input*, not be a constant that happens to read correctly.
380+
///
381+
/// `StringConcat` above has a single argument, so its `argument_index` is 0 whether the code
382+
/// computes it or hardcodes it — that test cannot tell the two apart. A lambda's bootstrap method
383+
/// takes three (samMethodType, implMethod, instantiatedMethodType), so breaking a different one has
384+
/// to move the number. Both mutations are the same edit at a different offset, which is what makes
385+
/// the pair a control for each other.
386+
#[test]
387+
fn test_the_reported_bootstrap_argument_index_follows_the_broken_argument() {
388+
let lambda = include_bytes!("../../test-data/indy/Lambda.class");
389+
let class = ClassInfo::parse(lambda).unwrap();
390+
let arguments = &bootstrap_methods(&class)[0].arguments;
391+
assert_eq!(arguments.len(), 3, "a LambdaMetafactory call site carries three static arguments");
392+
393+
// Derived from the parse rather than hardcoded, so a regenerated fixture moves the window with it.
394+
let mut window = vec![0u8, arguments.len() as u8];
395+
for argument in arguments {
396+
window.extend_from_slice(&argument.to_be_bytes());
397+
}
398+
let at = lambda
399+
.windows(window.len())
400+
.position(|candidate| candidate == window)
401+
.expect("test-data/indy/Lambda.class layout changed; the BootstrapMethods argument list moved");
402+
403+
// Entry #4 is a Utf8 ("java/lang/Object"): a real pool entry that is not a loadable constant.
404+
// The kind is not assumed — the assertion below reads it back, so a regenerated fixture whose
405+
// #4 is something else fails here rather than passing for the wrong reason.
406+
let utf8: u16 = 4;
407+
assert!(class.constant_pool.get(&utf8).is_some(), "the replacement index must be in the pool");
408+
409+
for broken in [0usize, 2usize] {
410+
let mut mutated = lambda.to_vec();
411+
let offset = at + 2 + broken * 2;
412+
mutated[offset..offset + 2].copy_from_slice(&utf8.to_be_bytes());
413+
414+
assert_eq!(
415+
ClassInfo::parse(&mutated).err(),
416+
Some(ClassFileError::InvalidBootstrapArgument {
417+
method_index: 0,
418+
argument_index: broken as u16,
419+
actual: Some("Utf8"),
420+
}),
421+
"breaking argument #{broken} must report argument #{broken}"
422+
);
423+
}
424+
}
425+
426+
/// The other half of the rule: an index that names no entry at all reports `actual: None`, and the
427+
/// message says "names no constant pool entry" rather than guessing a kind.
428+
#[test]
429+
fn test_a_bootstrap_argument_naming_nothing_reports_no_kind() {
430+
let string_concat = include_bytes!("../../test-data/indy/StringConcat.class");
431+
let class = ClassInfo::parse(string_concat).unwrap();
432+
let window = [0u8, 1, 0, 34, 0, 1, 0, 32];
433+
let at = string_concat
434+
.windows(window.len())
435+
.position(|candidate| candidate == window)
436+
.expect("test-data/indy/StringConcat.class layout changed; the BootstrapMethods entry moved");
437+
438+
let past_end = u16::MAX;
439+
assert!(class.constant_pool.get(&past_end).is_none(), "the index must name nothing");
440+
441+
let mut mutated = string_concat.to_vec();
442+
mutated[at + 6..at + 8].copy_from_slice(&past_end.to_be_bytes());
443+
444+
assert_eq!(
445+
ClassInfo::parse(&mutated).err(),
446+
Some(ClassFileError::InvalidBootstrapArgument {
447+
method_index: 0,
448+
argument_index: 0,
449+
actual: None,
450+
})
451+
);
452+
}
453+
454+
/// The cost of the variant, measured rather than asserted in prose: it stays `Copy` and the enum
455+
/// does not grow, because two `u16`s and a `&'static str` fit in the space `InvalidFormat` already
456+
/// needed for its string.
457+
#[test]
458+
fn test_the_structured_variant_does_not_grow_the_error_type() {
459+
assert_eq!(size_of::<ClassFileError>(), size_of::<&'static str>() + size_of::<usize>());
460+
fn assert_copy<T: Copy>() {}
461+
assert_copy::<ClassFileError>();
462+
}

0 commit comments

Comments
 (0)