Skip to content

Commit 85cf0fb

Browse files
author
jun0
committed
[rustjava-adopt-link-stringconcatfactory-p2-fix] docs(state): 승계 회차 기록 · worklog 쌍
1 parent 8d96f3f commit 85cf0fb

4 files changed

Lines changed: 160 additions & 0 deletions

File tree

‎REPORT.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
11
# REPORT
2+
## [2026-09-17] 포획 «순서»를 값으로 잠그고, 클래스 파일이 «프로세스를 죽이는» 자리를 닫는다 (rustjava-adopt-link-stringconcatfactory-p2-fix)
3+
- 무엇을: 게이트② **request-changes** 승계(PR #61 · 핀 `87ef6a70`). ★검수자 지적 **F1·F2 둘 다 옳았고 둘 다 받았다.**
4+
- ★**F1 — 포획 «순서»가 전 스위트에 무관측이었다**: `LambdaBody::call` 의 읽기 순서를 뒤집어도(검수자 RM4) **576 green**.
5+
거부가 아니라 ★**조용히 틀린 답**이다. 근인은 단언 방식이 아니라 **픽스처**다 — 전건이 포획 **1개 이하**라 «순서»라는 축이 존재하지 않았다.
6+
⇒ 포획 2개 람다 **둘**을 넣었다: `(String,int)` → `a:7`(순서가 **글자**에 보인다) · `(int,int)` → `120`(★**값에만** 보인다 — 어떤 타입 검사로도 못 잡는 축).
7+
★**RM4 가 이제 죽는다**: `a:7→7:a` · `120→2001`. ★`(a, b) -> a + b` 는 javac 이 내는 «가장 평범한» 람다다.
8+
- ★★**F2 — 적법한 클래스 파일이 «호스트 프로세스»를 죽였다**(`jvm/src/type.rs:74` panic · 게스트 예외가 아니다).
9+
★**고친 자리를 «골랐고 왜인지 적는다»**: 검수자 제안(`lambda.rs` 2줄)만 쓰면 진단이 `UnsupportedOperationException` 이 되는데,
10+
★**OpenJDK 26 은 같은 파일을 `ClassFormatError` 로 거부한다**(`Method "run" … has illegal signature "I"`) — 그 파일은 «미지원»이 아니라 **«파손»**이다.
11+
JVMS 4.4.6 상 `NameAndType` 은 필드·메서드 서술자 **둘 다** 적법해야 하고(Fieldref·Methodref 가 같은 항목을 공유한다),
12+
★**어느 쪽인지는 «참조하는 태그»가 정한다(4.4.10)** ⇒ 일반 `NameAndType` 팔은 **그대로 두고**
13+
`InvokeDynamic`=메서드 서술자 · `Dynamic`=필드 서술자를 **그 팔에서** 요구하게 했다(`Methodref` 는 이미 그렇게 한다).
14+
- ★**조이기 «비용»을 먼저 쟀다**(티켓 요구): 커밋된 클래스 **175개** · indy/condy 참조 **44건** 중 새로 위법이 되는 것은 ★**이 회차가 만든 픽스처 1건**뿐.
15+
- ★**`lower()` 의 `try_parse` 는 «둘째 층»이고, 독립 관측이 «안 된다»는 것을 숨기지 않는다** — 검증을 통과하면서 `try_parse` 가 실패하는 입력을 만들지 못했다.
16+
남긴 이유는 측정이 아니라 **비용의 비대칭**이다: 바깥 층이 틀리면 게스트 예외, 안쪽이 없으면 **프로세스 사망**.
17+
- ★**덤**: 같은 panic 이 `origin/main` 의 string concat 경로(`extract_invoke_params`)에도 있었는데 **같은 규칙에 함께 막힌다**(검수자가 「별 티켓」이라 한 자리).
18+
넓힌 것이 아니라 **규칙을 옳은 자리에 둔 결과**다.
19+
- 검증: 개악 **2종 전건 red**(RM4 · F2 규칙 되돌리기) · `cargo test --all` **578 passed / 0 failed / 1 ignored** ·
20+
픽스처 재생성 **멱등**(★형제 #59 의 생성기와 **한 파일로 합친 뒤**에도 기존 픽스처 바이트 불변).
21+
- ★후속: `ClassFormatError` 에 **사유를 싣자**(M) — 이 회차가 세운 「미지원 ↔ 파손」 구분이 정작 파손 쪽에서 「Invalid class file」 한 줄로 뭉개진다.
22+
223
## [2026-09-17] `LambdaMetafactory.metafactory` 를 링크했다 — ★**람다와 메서드 참조가 «돈다»** (rustjava-adopt-link-stringconcatfactory-p2)
324
- 무엇을: 채택 제안 `2026-09-16-link-stringconcatfactory#p2`. ★**제품 동작이 바뀐다** — 람다·메서드 참조를 담은 클래스가
425
**적재 거부**에서 **실행**으로 바뀐다. `jvm/` 은 **무접촉**, `java.lang.invoke` 는 **한 줄도 추가하지 않았다**.

‎STATE.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
(없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다)
55

66
## 완료
7+
- [rustjava-adopt-link-stringconcatfactory-p2-fix] ★★**포획 «순서»를 값으로 잠그고 «호스트 abort»를 없앤다 — 게이트② 반려 승계(PR #61).**
8+
★**검수자 F1·F2 둘 다 옳았다.** F1: 픽스처 전건이 포획 1개 이하라 **순서 축이 무관측**이었고 RM4(읽기 순서 역전)가 **576 green** 이었다
9+
⇒ 포획 2개 람다 둘 추가(`(String,int)`=`a:7` 글자로 · `(int,int)`=`120` ★값으로만) ⇒ **RM4 red**(`7:a`·`2001`).
10+
F2: 서술자가 `I` 인 콜사이트가 **호스트 프로세스를 죽였다**. ★**고친 자리 = `validation.rs` 의 «사용 지점»**(JVMS 4.4.10:
11+
InvokeDynamic=메서드 · Dynamic=필드) — 일반 `NameAndType` 팔의 `||` 는 **옳으므로 두었다**(Fieldref·Methodref 공유 항목).
12+
★근거는 실측이다: **OpenJDK 26 도 같은 파일을 `ClassFormatError`** 로 거부한다 ⇒ 「미지원」이 아니라 「파손」이 옳은 진단.
13+
★**조이기 비용 선측정**: 클래스 175 · indy/condy 44건 중 새로 위법 **1건**(이 회차 픽스처)뿐.
14+
★`lower()` 의 `try_parse` 는 둘째 층이고 ★**독립 관측 불가임을 명시**했다(남긴 근거 = 비용 비대칭).
15+
★개악 2종 전건 red · `--all` **578/0/1** · 픽스처 재생성 멱등(형제 #59 생성기와 합친 뒤에도 바이트 불변).
716
- [rustjava-adopt-link-stringconcatfactory-p2] ★★**`LambdaMetafactory.metafactory` 링크 — 람다·메서드 참조가 «돈다».**
817
채택 제안 `2026-09-16-link-stringconcatfactory#p2`(worklog json `adoptedProposals` 기록). ★**제품 동작 변경 있음**
918
(람다 포함 클래스: 적재 거부 → 실행). ★`jvm/` 무접촉 · `java.lang.invoke` **0줄**.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-17",
4+
"taskId": "rustjava-adopt-link-stringconcatfactory-p2-fix",
5+
"summary": "Gate-2 request-changes follow-up: make capture *order* observable (it was not, so reversing it stayed green), and stop a class file from aborting the host process at load time — fixed at the usage site JVMS 4.4.10 names, which also makes the diagnosis match OpenJDK's.",
6+
"changes": [
7+
"test-data/src/indy/LambdaKinds.java: two two-capture lambdas — (String,int) shows a swap in the text, (int,int) shows it only in the value",
8+
"tests/test_class_format.rs: those two lines added to the asserted output; new test for the field-descriptor call site with the reviewer's control",
9+
"classfile/src/validation.rs: InvokeDynamic requires a method descriptor, Dynamic a field descriptor (JVMS 4.4.10) — the shared NameAndType arm is left as it is",
10+
"jvm-bytecode/src/lambda.rs: lower() parses the call site descriptor with try_parse instead of the panicking parse/as_method",
11+
"test-data/src/indy/make_indy_fixtures.py: call_site_descriptor parameter + MetafactoryFieldDescriptorCallSite fixture; merged with the sibling generator that landed as #59"
12+
],
13+
"verification": [
14+
"RM4 (reviewer's mutation: capture read order reversed) — was green at 576, now red: a:7 -> 7:a and 120 -> 2001",
15+
"F2 mutation (revert the InvokeDynamic descriptor rule) — the new test fails; with the fix the file is ClassFormatError, matching OpenJDK 26",
16+
"cost of tightening measured before doing it: 175 committed class files, 44 invokedynamic/dynamic references, exactly 1 newly rejected — the fixture written for it",
17+
"cargo test --all: 578 passed / 0 failed / 1 ignored",
18+
"regenerating the indy fixtures leaves every pre-existing one byte-identical, after merging two siblings' generators into one file"
19+
],
20+
"issues": [
21+
"The try_parse in lower() is a second layer and I could not construct an input that passes the tightened validation and still fails it — so it is not independently observable. It stays because the cost of being wrong there is a host abort rather than a guest exception, and that asymmetry is the justification, not a measurement.",
22+
"The same host abort exists on origin/main through the string-concat path (extract_invoke_params). The validation rule closes that entrance too, which was not this round's goal but follows from putting the rule where JVMS puts it."
23+
],
24+
"adoptedProposals": [],
25+
"proposals": [
26+
{
27+
"title": "Give ClassFormatError the reason it was rejected for",
28+
"plainSummary": "Every malformed class file reports the same flat 'Invalid class file'.",
29+
"userBenefit": "A file rejected for an illegal call-site descriptor reads the same as one with a truncated constant pool; the message cannot tell you which.",
30+
"why": "This round added a rule whose whole value is the distinction between 'unsupported' and 'malformed', and the malformed side still cannot say why. OpenJDK names the member and the signature. The parser already knows which predicate failed at the moment it fails.",
31+
"tradeoff": "ClassFileError is a flat enum by an upstream cut (822504b); adding variants means touching the error type every validator shares, and the tests that assert on the kind rather than the text would need to keep working.",
32+
"effort": "M",
33+
"target": "classfile/src/error.rs, classfile/src/validation.rs"
34+
}
35+
]
36+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 2026-09-17 — Capture order, and a class file that killed the process
2+
3+
`taskId: rustjava-adopt-link-stringconcatfactory-p2-fix` ·
4+
gate-2 request-changes follow-up for PR #61 (pin `87ef6a70`)
5+
6+
Two findings, both the reviewer's, both correct.
7+
8+
## F1 — the capture *order* was unobservable
9+
10+
The reviewer reversed one line in `LambdaBody::call` (`&self.captures` → `.iter().rev()`) and the
11+
whole suite stayed green at **576 / 0**. Not a refusal — a **wrong answer**, silently.
12+
13+
The cause is in the fixtures, not the assertion style: every one of them captures **at most one
14+
value**. `LambdaKinds` captures `base` in one lambda and `bound` in another; `Lambda` and
15+
`LambdaCapturingThis` capture one apiece. With one capture there is no order to get wrong, so the
16+
line that orders them is not covered by anything. The round's own mutation M10 ("captures not
17+
stored") tested *presence*, which is a different axis and reads deceptively like the same one.
18+
19+
Two lambdas now capture two values each, and they were chosen to fail differently:
20+
21+
| lambda | captures | prints | why this one |
22+
|---|---|---|---|
23+
| `pair` | `(String, int)` | `a:7` | a swap shows up in the **text** |
24+
| `weighted` | `(int, int)` | `120` | a swap shows up **only in the value** — no type check could catch it |
25+
26+
RM4 now dies on both: `a:7 → 7:a` and `120 → 2001`.
27+
28+
This is not an exotic shape. `(a, b) -> a + b` is what javac emits for the most ordinary lambda
29+
there is, and until this round it would have silently returned the arguments backwards.
30+
31+
## F2 — a valid class file aborted the host process
32+
33+
```
34+
thread 'main' panicked at jvm/src/type.rs:74:13: Invalid type
35+
```
36+
37+
A call site whose descriptor is `I` — a *field* descriptor — reaches `lower()`, which read it with
38+
the panicking `JavaType::parse` / `as_method`. A panic is not a guest exception: the process dies.
39+
`verifier.rs` already writes that exact sentence about `ldc` ("reaching it would abort the host,
40+
not the guest"), so the repo's own doctrine names this a defect.
41+
42+
### Where to fix it, and why not where it was suggested
43+
44+
The review proposed two lines in `lambda.rs` (`try_parse` + `let … else`). That removes the abort,
45+
but it answers **`UnsupportedOperationException`** — and that answer is wrong:
46+
47+
```
48+
$ java -cp . MetafactoryFieldDescriptorCallSite # OpenJDK 26.0.1
49+
java.lang.ClassFormatError: Method "run" in class MetafactoryFieldDescriptorCallSite
50+
has illegal signature "I"
51+
```
52+
53+
The file is not *unsupported*. It is **malformed**, and this repository has spent several rounds on
54+
keeping those two words apart. So the fix belongs where JVMS puts the rule.
55+
56+
JVMS 4.4.6 says a `NameAndType` descriptor is "a valid field descriptor or method descriptor" —
57+
which it must be, because `Fieldref` and `Methodref` share that entry kind. *Which* one is decided
58+
by the entry that refers to it, and 4.4.10 states it: `CONSTANT_InvokeDynamic` names a method,
59+
`CONSTANT_Dynamic` names a field type. The existing `NameAndType` arm is therefore **right as it
60+
stands** and was left alone; the missing check was on the referring arm, which the codebase already
61+
does for `Methodref` via `validate_member_reference(..., MemberKind::Method)`.
62+
63+
**Cost measured before tightening**, as the ticket required: 175 committed class files, 44
64+
invokedynamic/dynamic references, **exactly one** newly rejected — the fixture written for this
65+
finding. Nothing else in the tree moves.
66+
67+
`lower()` still uses `try_parse`. That is a second layer, and I could not construct an input that
68+
passes the tightened validation and still fails it — so **it is not independently observable**, and
69+
I am not claiming a mutation kills it. It stays because the two layers fail differently: the outer
70+
one produces a guest exception, and the missing inner one produces a dead process.
71+
72+
### A side effect worth naming
73+
74+
The same panic exists on `origin/main` through the string-concat path
75+
(`Interpreter::extract_invoke_params`), which the review scoped out as a separate ticket. Putting
76+
the rule at the usage site closes that entrance too — not by widening this round, but because a
77+
rule in the right place covers everything that reads through it.
78+
79+
## Mutations
80+
81+
| | mutation | result |
82+
|---|---|---|
83+
| **RM4** | capture read order reversed (the reviewer's) | **red** — `7:a` / `2001` |
84+
| **F2-M** | the `InvokeDynamic` descriptor rule reverted | **red** — the file is no longer `ClassFormatError` |
85+
86+
`cargo test --all`: **578 passed / 0 failed / 1 ignored**. Regenerating the indy fixtures leaves
87+
every pre-existing one byte-identical — including after merging this branch's generator with the
88+
sibling one that landed as #59, which is the check that the merge kept both.
89+
90+
## One process note
91+
92+
`git checkout --` to undo a mutation also reverted an **uncommitted** fix sitting in the same file,
93+
and the next command reported the fix as missing. Commit before mutating; the mutation matrix in
94+
this round was re-run on a committed tree.

0 commit comments

Comments
 (0)