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
14 changes: 14 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# REPORT
## [2026-09-17] 다른 클래스 수준 속성에도 개수 규칙이 필요한가 — ★**다섯에 «예». 그리고 제안의 값 전제가 거짓이었다** (rustjava-adopt-reject-duplicate-bootstrap-methods-p0)
- 무엇을: 채택 제안 `2026-09-16-reject-duplicate-bootstrap-methods#p0`(제목이 **「Decide whether …」** = 이 회차가 지는 것은 **판정**). ★**제품 동작 변경 있음** — 아래.
- ★**제안의 기준만으로는 «아니오»가 나온다**: 기존 검사 주석이 적은 기준은 「중복이 **하류에서 임의의 선택을 관측 가능하게 만드는가**」이고, 소비자 전수 실측 결과 그런 속성은 ★**`BootstrapMethods` 하나뿐**이다(`validation.rs` + `string_concat.rs:110 resolve_bootstrap_methods` 의 `find_map`). 나머지 6종은 `attribute.rs` 밖 소비자 **0**.
- ★★**판정을 바꾼 것은 «진짜 JVM»이다** — OpenJDK **26.0.1** 에 한 속성씩 물었다(★런처 메시지는 원인을 가리므로 `Class.forName` 으로 진단문을 받았다):
`SourceFile`·`InnerClasses`·`SourceDebugExtension`·`BootstrapMethods`(52) · `NestHost`·`NestMembers`(55) → ★**전건 `ClassFormatError: Multiple … attributes`**.
⇒ ★**제안의 `tradeoff` 「Rejecting more files that load today」는 그 다섯에 대해 «거짓»이다** — 그 파일들은 **오늘도 진짜 JVM 에서 로드되지 않는다.** 받아들이던 쪽이 **우리**였다.
- ★★**그래서 «표»이고 «버전 게이트»다 — 두 통제군이 그 이유다**:
⒜★`NestHost` **major 52** 에서는 ★**로드된다**(그 버전엔 정의되지 않아 무시 · JVMS 4.7.1) ⇒ 게이트 없이 세면 **모든 JVM 이 받는 파일을 거부**한다.
⒝★`Synthetic` 은 JVMS 4.7.8 이 at-most-one 이라는데 ★**HotSpot 은 둘을 받는다** ⇒ ★**빠뜨린 것이 아니라 «근거로 뺐다»**(넣으면 맞추려는 JVM 보다 엄격해진다).
- ★**반대 방향도 의도적이다** — 필드·메서드·Code 소속 속성은 클래스 속성표에 나타나도 **세지 않는다**(정의되지 않은 자리의 속성 = 무시).
- ★★**양방향 개악 4종 전건 red**: **M1** 버전 게이트 제거 → 통제군 `DuplicateNestHostOldMajor` red · **M2** `Synthetic` 추가 → 통제군 red · **M3** `SourceFile` 한 칸 제거 → 그 픽스처만 red · **M4** 호출부를 종전으로 되돌림 → 다섯 red · 복원 **17/0 green**. ★**M1·M2 가 요지다** — 통제군이 없으면 「전부 세면 된다」가 통과한다.
- ★**대가**: ⒜**제품 동작 변경** — 그 다섯을 둘씩 가진 클래스가 **로드되지 않는다**(전부 OpenJDK 도 거부하지만 변경은 변경이다) ⒝**표는 «적어 둔 목록»** 이라 속성이 늘어도 행을 더하기 전엔 안 덮이고 **울어 주는 것이 없다** ⒞`Synthetic` 배제는 **JVM 하나의 행동**에 걸려 있다 ⒟★**`attribute.rs` 를 만졌다**(제안 `target` 은 `validation.rs` 뿐) — `SourceDebugExtension` 파싱 팔 **한 줄**로 기존 **dead variant** 를 살린 것이고, 없으면 중복이 `Unknown` 에 섞여 **구별 불가**다. 최소였지만 **명시 파일 밖**이라 신고한다.
- 검증: `cargo test --all` **578 → 579 / 0 failed / 1 ignored** · `test_fixture_pins` **3/0** · 픽스처 재생성 **멱등** · 버전 표 **+7행**(AGENTS.md 의무) · DoD 7명령 rc=0.

## [2026-09-17] ldc 픽스처를 ASM 으로 재생성하자 — ★**기각**(rustjava-adopt-ldc-tags-real-world-generator-survey-p0)
- 무엇을: 운영자가 tower 에서 채택한 제안 `2026-09-16-ldc-tags-real-world-generator-survey#p0` 의 처분. ★**제품 코드 0줄** — 산출물은 «판정과 그 근거»다.
- ★**기각 사유 ⑴ 제안이 사려는 것이 이미 있다 — 그것도 더 센 오라클로**: ★**OpenJDK 26.0.1 이 양성 픽스처 4건을 «실행»한다**(`LdcMethodHandle`·`LdcMethodType`·`LdcDynamic`·`Ldc2WDynamic` **전건 rc=0**) ⇒ 진짜 JVM 의 **검증기**가 우리 손조립 바이트를 받아들인다. 대조군(위법 5건)은 **전건 rc=1**. 그리고 「ASM 이 이 태그를 내는가」는 **선행 조사 회차가 이미 동적으로 실증**했다.
Expand Down
12 changes: 12 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)

## 완료
- [rustjava-adopt-reject-duplicate-bootstrap-methods-p0] ★★**클래스 수준 속성 개수 규칙 — 다섯에 «예».** 채택 제안 `…-reject-duplicate-bootstrap-methods#p0`(worklog json 기록). ★**제품 동작 변경 있음.**
★**제안 기준만으로는 «아니오»였다**(하류 `find_map` 소비자가 있는 것은 `BootstrapMethods` 뿐 · 나머지 6종 소비자 0).
★★**판정을 바꾼 것은 진짜 JVM 이다** — OpenJDK 26.0.1 이 `SourceFile`·`InnerClasses`·`SourceDebugExtension`·`BootstrapMethods`(52)·`NestHost`·`NestMembers`(55) 중복을 ★**전건 `ClassFormatError`** 로 거부한다
⇒ ★**제안의 「오늘 로드되는 파일을 더 거부한다」는 «거짓»**(그 파일들은 진짜 JVM 에서도 안 열린다).
★★**통제군 둘이 표·버전게이트의 이유**: `NestHost`@**52** 는 ★**로드된다**(미정의 ⇒ 무시) · `Synthetic` 은 스펙이 하나라는데 ★**HotSpot 이 둘을 받는다**(근거로 뺐다).
★**개악 4종 전건 red**(M1 게이트 제거·M2 Synthetic 추가 → **통제군** red · M3 한 칸 제거 · M4 호출부 원복) · 복원 17/0.
★**대가**: 동작 변경 · 표는 수동 목록(늘어도 안 울린다) · Synthetic 배제는 JVM 하나에 의존 · ★**`attribute.rs` 한 줄**(제안 target 밖 — 신고).
★`--all` **579/0/1** · 버전 표 **+7행** · DoD 7명령 rc=0.
★★**게이트③ 착지 — PR #66 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` · 티켓 `merge_strategy: merge` 선언분 ⇒ ★**계보 보존**).
게이트② **approve** · 핀 **`1da1379e`** ↔ 착수 시 PR head **동일**(불이동) · ★**`CONFLICTING`** ⇒ 예외 사유 ⓖ 로 별 `-merge` 가 발권된 회차다.
★**충돌은 원장 2파일뿐**(`STATE.md`·`REPORT.md` 상단 삽입 · 제품 코드 **0**) — base 당김(뒤처짐 **3**) + 합집합 해소. 보존 증명 양방향 소실 **0** · 기여 불변 `--numstat` 정확 일치.
★핀에서 `ci-presence` **rc=0 CI_GREEN** · 자식 PR **0건** · 배포 **0**(배포 워크플로 없음) · 주기 자동 커밋 **0건**(최다 규칙성 author cv **0.57** > 0.1) · 라이브 실행 주체 **없음**.
- [rustjava-adopt-ldc-tags-real-world-generator-survey-p0] ★★**「ldc 픽스처를 ASM 으로 재생성」 제안 — 기각.** ★**제품 코드 0줄**(`declinedProposals` 기록).
★**사유 ⑴ 더 센 오라클이 이미 있다** — ★**OpenJDK 26.0.1 이 양성 4건을 실행한다(전건 rc=0)** · 위법 5건은 전건 rc=1 · 「ASM 이 낸다」는 선행 회차가 이미 동적 실증.
★**⑵ 손의 흔적은 «옮겨갈» 뿐이다**(ASM 도 드라이버 15줄이 모양을 고른다) ★**⑶ 생성기가 «두 기구»가 된다**(음성 픽스처는 ASM 불가) ·
Expand Down
4 changes: 4 additions & 0 deletions classfile/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ impl AttributeInfo {
AttributeInfo::LineNumberTable(length_count(be_u16, AttributeInfoLineNumberTableEntry::parse).parse(info)?.1)
}
"SourceFile" => AttributeInfo::SourceFile(Self::parse_source_file(info, constant_pool)?.1),
// The body is an unstructured UTF-8 blob nothing here reads; the variant exists so the
// at-most-one rule in `validation.rs` can see it. Without this arm it lands in `Unknown`,
// where a duplicate is indistinguishable from two attributes we do not recognise.
"SourceDebugExtension" => AttributeInfo::SourceDebugExtension,
"LocalVariableTable" => AttributeInfo::LocalVariableTable(Self::parse_local_variable_table(info, constant_pool)?.1),
"StackMap" => AttributeInfo::StackMap(info.to_vec()),
"StackMapTable" => AttributeInfo::StackMapTable(info.to_vec()),
Expand Down
80 changes: 63 additions & 17 deletions classfile/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
|| !constant_pool_tags_fit_the_class_file_version(class)
|| !bootstrap_method_static_arguments_are_in_the_pool(class)
|| !bootstrap_method_indices_resolve(class)
|| !at_most_one_bootstrap_methods_attribute(class)
|| !at_most_one_of_each_single_class_attribute(class)
{
return Err(ClassFileError::InvalidFormat);
}
Expand Down Expand Up @@ -191,25 +191,71 @@ fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
})
}

/// JVMS 4.7.23: at most one `BootstrapMethods` attribute may appear in a ClassFile's attributes
/// table. A file carrying two is broken, not a file using a feature this runtime lacks.
/// JVMS 4.7: several ClassFile attributes may appear at most once. A file carrying two is broken,
/// not a file using a feature this runtime lacks.
///
/// Kept separate from `bootstrap_method_indices_resolve` because it is a different sentence: that
/// one asks whether an index names a real entry, this one asks how many tables exist. Folding it in
/// would also mean renaming that function for a rule it did not previously make.
/// one asks whether an index names a real entry, this one asks how many tables exist. Counting is
/// the same shape `validate_class` already uses for the per-member "at most one" rules
/// (`ConstantValue` on a field, `Code` on a method).
///
/// It matters because `bootstrap_method_indices_resolve` resolves the table with `find_map`, which
/// stops at the first one. With two tables that choice is arbitrary — the index would be bounded
/// against whichever came first and the other silently ignored — so the honest answer is to reject
/// the file rather than pick. Counting is the same shape `validate_class` already uses for the
/// per-member "at most one" rules (`ConstantValue` on a field, `Code` on a method).
fn at_most_one_bootstrap_methods_attribute(class: &ClassInfo) -> bool {
class
.attributes
.iter()
.filter(|attribute| matches!(attribute, AttributeInfo::BootstrapMethods(_)))
.count()
<= 1
/// `BootstrapMethods` came first, because a duplicate there makes an arbitrary choice observable:
/// `bootstrap_method_indices_resolve` and `string_concat`'s `resolve_bootstrap_methods` both take
/// the table with `find_map`, which stops at the first one. That argument covers one attribute, and
/// the round that added it left the others as an open question.
///
/// ## Which others, and why not "all of them"
///
/// The list below is **what OpenJDK 26.0.1 actually rejects**, measured one attribute at a time
/// rather than read off the spec, because the two do not agree:
///
/// | duplicated attribute | class file version | OpenJDK 26.0.1 |
/// |---|---|---|
/// | `SourceFile`, `InnerClasses`, `SourceDebugExtension`, `BootstrapMethods` | 52 | `ClassFormatError: Multiple … attributes` |
/// | `NestHost`, `NestMembers` | 55 | `ClassFormatError: Multiple … attributes` |
/// | `NestHost` | **52** | **loads** — the attribute is not defined before 55, so it is ignored (JVMS 4.7.1) |
/// | `Synthetic` | 52 | **loads**, though JVMS 4.7.8 marks it at-most-one |
///
/// Two of those rows are the whole reason this is a table and not a `matches!` over every variant:
///
/// * **The version gate is load-bearing.** Counting `NestHost` at major 52 would reject a file every
/// real JVM accepts. An attribute that does not exist at that version is not duplicated, it is
/// unrecognised, and unrecognised attributes are ignored.
/// * **`Synthetic` is excluded on evidence, not oversight.** The spec says at most one; HotSpot
/// takes two. Rejecting it would make us stricter than the JVM we are trying to agree with, and
/// nothing here reads the attribute, so there is no arbitrary choice to make observable either.
///
/// The other direction is deliberate too: attributes that belong to a field, method or Code table
/// (`ConstantValue`, `Code`, `Exceptions`, `MethodParameters`, `StackMapTable`, `LineNumberTable`,
/// `LocalVariableTable`) are not listed even when they turn up in a class's attribute table, because
/// there they are attributes in a place they are not defined for — ignored, not counted.
fn at_most_one_of_each_single_class_attribute(class: &ClassInfo) -> bool {
// (discriminant, the class file version that introduced the attribute)
fn single_valued(attribute: &AttributeInfo) -> Option<(u8, u16)> {
Some(match attribute {
AttributeInfo::SourceFile(_) => (0, 45),
AttributeInfo::InnerClasses(_) => (1, 45),
AttributeInfo::SourceDebugExtension => (2, 49),
AttributeInfo::BootstrapMethods(_) => (3, 51),
AttributeInfo::NestHost(_) => (4, 55),
AttributeInfo::NestMembers(_) => (5, 55),
_ => return None,
})
}

class.attributes.iter().enumerate().all(|(position, attribute)| {
let Some((kind, introduced_in)) = single_valued(attribute) else {
return true;
};
if class.major_version < introduced_in {
return true;
}

// Only the first of each kind looks behind it, so one duplicate is reported once.
!class.attributes[..position]
.iter()
.any(|earlier| single_valued(earlier).is_some_and(|(earlier_kind, _)| earlier_kind == kind))
})
}

fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
Expand Down
38 changes: 38 additions & 0 deletions docs/worklog/2026-09-17-class-attribute-cardinality.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"schema": "worklog/v1",
"date": "2026-09-17",
"taskId": "rustjava-adopt-reject-duplicate-bootstrap-methods-p0",
"summary": "The proposal asked whether the other at-most-one ClassFile attributes deserve the rule BootstrapMethods got, and guessed the answer might be no for the ones nothing reads. Measured against OpenJDK 26.0.1 one attribute at a time, the answer is yes for five of them — and the proposal's stated cost, 'rejecting more files that load today', is false: a real JVM already refuses every one of those files. The interesting part is the two it does not refuse, which is why this is a version-gated table rather than a rule over every single-valued attribute.",
"changes": [
"classfile/src/validation.rs: at_most_one_bootstrap_methods_attribute -> at_most_one_of_each_single_class_attribute, a version-gated table over SourceFile, InnerClasses, SourceDebugExtension, BootstrapMethods, NestHost, NestMembers",
"classfile/src/attribute.rs: one parse arm for SourceDebugExtension, which makes an existing but unreachable enum variant live. Without it a duplicate lands in Unknown, where it is indistinguishable from two attributes we do not recognise.",
"test-data/src/attr/make_attr_fixtures.py: new generator for duplicate-attribute fixtures, carrying the measurement in its docstring",
"test-data/attr/: 7 fixtures — 5 that must be rejected, 2 controls that must keep loading",
"tests/test_class_format.rs: test_a_class_declaring_a_single_valued_attribute_twice_is_malformed, a table with a `loads` column",
"test-data/class-file-versions.txt: +7 rows for the new fixtures (AGENTS.md requires the table move with the fixture)"
],
"verification": [
"ORACLE, measured one attribute at a time with OpenJDK 26.0.1 via Class.forName rather than the launcher (whose 'no main method' message hides the real cause): duplicated SourceFile / InnerClasses / SourceDebugExtension / BootstrapMethods at major 52 -> ClassFormatError: Multiple <name> attributes; duplicated NestHost / NestMembers at major 55 -> same",
"CONTROL 1 — duplicated NestHost at major 52 LOADS. The attribute is not defined before 55, so it is ignored (JVMS 4.7.1) and there is nothing to duplicate. A rule without a version gate would reject a file every JVM accepts.",
"CONTROL 2 — duplicated Synthetic at major 52 LOADS, though JVMS 4.7.8 marks it at-most-one. The spec and HotSpot disagree; we follow the JVM. Nothing here reads the attribute either, so there is no arbitrary choice to make observable.",
"so the proposal's tradeoff premise is measurably false for the five: those files do not 'load today' on a real JVM — our accepting them was the deviation",
"criterion re-derived from the existing check's own comment (a duplicate makes an arbitrary choice observable downstream) and then measured: the only class-level attribute read with a first-wins selector is BootstrapMethods (validation.rs find_map and jvm-bytecode/src/string_concat.rs resolve_bootstrap_methods find_map). Consumers outside attribute.rs for SourceFile / InnerClasses / SourceDebugExtension / NestHost / NestMembers / Synthetic: zero. So the downstream-observability argument alone would have stopped at 'no' — the JVM oracle is what moved it.",
"MUTATION M1, delete the version gate: control DuplicateNestHostOldMajor turns red (we would reject what the JVM loads)",
"MUTATION M2, add Synthetic to the table: control DuplicateSynthetic turns red",
"MUTATION M3, remove the SourceFile row: that fixture alone stops being rejected, red",
"MUTATION M4, revert the call site to the BootstrapMethods-only form: the five turn red",
"restore: test_class_format 17 passed / 0 failed",
"generator is idempotent: a second run leaves the fixture bytes unchanged",
"cargo test --all and the DoD commands: see the round's REPORT.md entry"
],
"issues": [
"Product behaviour changes: a class carrying two of those five attributes now fails to load where it used to run. That is the point, and every such file is one OpenJDK also refuses — but it is a behaviour change, not a no-op.",
"The table is a written-down list, not derived. An attribute added to AttributeInfo tomorrow is not covered until someone adds a row, and nothing flags that.",
"Synthetic is excluded on one JVM's behaviour. If HotSpot tightened, or another JVM already rejects it, our control fixture would be pinning the wrong answer.",
"attribute.rs was touched even though the proposal's target is validation.rs. One arm, and it was the minimum to cover an attribute the JVM rejects — but it is outside the named file and is reported as such."
],
"adoptedProposals": [
"2026-09-16-reject-duplicate-bootstrap-methods#p0"
],
"proposals": []
}
Loading