Skip to content

Commit 2dc986f

Browse files
jun0claude
andcommitted
[rustjava-error-path-name-the-missing-bootstrap-class-p0] fix(jvm): 어느 부트스트랩 클래스가 없는지 «말하게» 한다
채택 제안 2026-09-20-error-path-class-closure#p0. `Jvm::new` 의 `bootstrap_classes` 루프가 `.unwrap()` 으로 죽어, 클래스 집합에 구멍이 있는 호스트는 `called Option::unwrap() on a None value` 를 읽고 여섯 중 어느 것인지 목록을 이분해야 했다. 그중 둘(Object·Serializable)은 오류 경로 폐포 에도 있어 같은 결손이 «어느 루프에 먼저 걸리느냐»로 문면 품질이 갈렸다. 이제 `let … else` 가 ⑴이름 ⑵여섯 중 하나라는 것 ⑶어디에 물었는지 — `Jvm::new` 에 넘긴 호스트 자신의 부트스트랩 로더이지 클래스패스가 아니다(`java.class.path` 는 시스템 클래스로더 몫이고 그것은 이 함수 뒤쪽에서 만들어져 여기서 실패하면 아예 돌지 않는다) — 를 말한다. 스윕도 함께 고친다. `Outcome::Panicked` 의 주석은 「naming the class」였는데 문면을 읽지 않아, 이 결함을 자기 눈으로 보고도 좋은 쪽에 세고 있었다. 이제 패닉 payload 를 숨긴 이름과 대조하고, 이름을 못 댄 거절은 `PanickedAnonymously` 로 따로 세며 그 자체가 실패다. 양방향(제품 호출부 · 두 사실을 따로): 개악(`.unwrap()` 복원) → 이름 적중 0/5 · `7 by name · 5 anonymous` FAILED ↔ 고침 → 5/5 · `12 · 0` ok. rc 만 보면 둘이 같다(양쪽 다 패닉). 가족: 이 축의 unwrap 은 1곳(고쳤다). 같은 모양의 레지스트리 조회 4곳 (jvm.rs:995·1002·1335 · garbage_collector.rs:109)은 내부 불변식 축이고 스윕이 닿지 않음을 실측(익명 거절 0/37) ⇒ blind spot 으로 적고 두었다. 패닉을 오류 반환으로 바꾸지 않았다 — `JavaError` 는 변종 1개이고 내용이 `Box<dyn ClassInstance>` 인데 여기서 없는 것이 바로 그 인스턴스를 만들 클래스다. 제안 카드로 적었다. DoD 9명령 rc 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 97707f2 commit 2dc986f

6 files changed

Lines changed: 174 additions & 6 deletions

File tree

‎REPORT.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# REPORT
2+
## [2026-09-20] 없는 부트스트랩 클래스의 «이름»을 말하게 했다 (rustjava-error-path-name-the-missing-bootstrap-class-p0)
3+
- 무엇을: 채택 제안 `2026-09-20-error-path-class-closure#p0`. `Jvm::new` 의 `bootstrap_classes` 루프가 `.unwrap()` 으로 죽어 **어느 클래스가 없는지 말하지 않던** 것을 `let … else` 로 바꿔 ⑴이름 ⑵여섯 중 하나라는 것 ⑶★**어디에 물었는지**(= `Jvm::new` 에 넘긴 **호스트 자신의 부트스트랩 로더**이지 클래스패스가 **아니다** — `java.class.path` 는 시스템 클래스로더 몫이고 그건 이 함수 뒤쪽에서 만들어져 여기서 실패하면 안 돈다)를 말하게 했다.
4+
- 왜 지금: 지난 회차 스윕이 「이름을 대고 거절 12건」으로 셌는데 ★**그중 5건의 실제 문면이 `called Option::unwrap() on a None value`** 였다. 구멍 난 호스트는 **여섯을 이분**해야 했고, 그중 둘(`Object`·`Serializable`)은 오류 경로 폐포에도 있어 ★**어느 루프에 먼저 걸리느냐로 문면의 품질이 갈렸다**.
5+
- ★**가족을 세고 골랐다**: 이 축의 `unwrap` 은 **1곳**(고쳤다). 같은 «모양»의 레지스트리 조회 **4곳**(`jvm.rs:995·1002·1335`·`garbage_collector.rs:109`)은 ★**다른 축**이다 — 호스트 입력이 아니라 내부 불변식이고, 고친 뒤 스윕 37 후보 전건에서 **익명 거절 0** = 클래스 집합 축에서 **닿지 않는다**. ⇒ 양방향으로 못 잠그므로 **손대지 않고 blind spot 으로 적었다**.
6+
- ★★**스윕이 자기 눈으로 보고도 좋은 쪽에 세고 있었다**: `Outcome::Panicked` 의 주석은 「naming the class」인데 **문면을 읽지 않았다**. 이제 패닉 payload 를 downcast 해 **숨긴 이름과 대조**하고, 이름을 못 댄 거절은 `PanickedAnonymously` 로 **따로 세며 그 자체가 실패**다.
7+
- ★**양방향 — 「죽었다」와 「내 문면이 읽혔다」를 «따로»**(제품 호출부 · 사본 아님): 개악(`.unwrap()` 복원) → 문면 5/5 가 `Option::unwrap()`, ★**이름 적중 0/5**, 스윕 `0 recursed · 7 by name · **5 anonymous** · 25 clean` **FAILED** ↔ 고침 → 이름 적중 ★**5/5**, 스윕 `0 · **12** · **0** · 25` **ok**. ★**rc 만 보면 둘이 같다**(양쪽 다 패닉) — 그래서 두 사실을 분리해 셌다. 정상 구성은 이 분기에 **닿지 않는다**(조용).
8+
- ★**대가**: 런타임 비용 **0**(같은 호출 하나). ★**패닉을 오류 반환으로 바꾸지 «않았다»** — `JavaError` 는 변종 1개이고 내용이 `Box<dyn ClassInstance>` 인데 ★**여기서 없는 것이 바로 그 인스턴스를 만들 클래스들**이라 새 변종은 「자바 예외가 아닌 오류」여야 한다(사내 파급 3곳 · 사외는 공개 enum 파괴 변경) ⇒ **멈추고 제안 카드로 적었다**. `[B` 는 여전히 측정 밖(로더가 **합성**한다).
9+
- 검증: DoD 9명령 rc 0.
10+
- ★후속 추천 **1건**: `Jvm::new` 가 **패닉해도 되는가**(이제 두 곳이다)를 정하라(M). 상세 = `docs/worklog/2026-09-20-name-the-missing-bootstrap-class.{md,json}`.
11+
212
## [2026-09-20] 오류 경로의 클래스 집합을 «한 번에» 쟀다 — ★**답은 «둘»이 아니었다**(rustjava-string-on-the-error-path-p0)
313
- 무엇을: 채택 제안 `2026-09-19-string-on-the-error-path#p0`. 후보를 **유도**해(기록 로더로 정상 구성 1회 — 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ **37**) 하나씩 숨겨 재구성하는 **스윕**을 만들고 돌렸다.
414
- ★★**결과 — 제안이 「still just these two」면 «기록된 음성»이라 했던 그 가정이 «반증»됐다**: ★**5개가 더 재귀한다**(바닥 없음) — `java/lang/Throwable` · `Error` · `LinkageError` · `CharSequence` · `Comparable`. ★**우연이 아니다** — 기존 두 이름의 **상위형·인터페이스 폐포**다(클래스를 resolve 하면 상위형도 resolve 되고, 거기 결손은 «아직 resolve 중인 그 두 클래스»로 보고된다).

‎STATE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)
88

99
## 완료
10+
- [rustjava-error-path-name-the-missing-bootstrap-class-p0] ★★**없는 부트스트랩 클래스의 «이름»을 말하게 했다.** 채택 제안 `2026-09-20-error-path-class-closure#p0`.
11+
★`Jvm::new` 의 `bootstrap_classes` 루프 `.unwrap()` → `let … else` · 문면 = 이름 + 여섯 중 하나 + ★**어디에 물었는지**(호스트 로더이지 `java.class.path` 가 **아니다**).
12+
★**가족을 세고 골랐다**: 이 축 **1곳 고침** ↔ 같은 «모양»의 레지스트리 조회 **4곳**은 다른 축(내부 불변식)이고 스윕이 **닿지 않음을 실측**(익명 거절 0/37) ⇒ **blind spot 으로 적고 두었다**.
13+
★★**스윕이 자기 눈으로 보고도 좋은 쪽에 세고 있었다** — `Panicked` 가 문면을 안 읽었다. 이제 payload 를 숨긴 이름과 대조하고 `PanickedAnonymously` 는 **그 자체가 실패**.
14+
★**양방향(두 사실을 «따로»)**: 개악 → 이름 적중 **0/5** · `7 by name · 5 anonymous` **FAILED** ↔ 고침 → **5/5** · `12 · 0` **ok**. ★**rc 만 보면 둘이 같다**(양쪽 패닉).
15+
★**대가**: 런타임 **0**. 패닉→오류 반환은 **하지 않았다**(`JavaError` 변종 1개 · 없는 것이 바로 예외를 만들 클래스 · 사내 3곳 + 공개 enum 파괴) ⇒ 제안 카드.
1016
- [rustjava-string-on-the-error-path-p0] ★★**오류 경로의 클래스 집합을 한 번에 쟀다 — 답은 «둘»이 아니었다.** 채택 제안 `2026-09-19-string-on-the-error-path#p0`.
1117
★**후보를 «유도»했다**(손 목록 아님): 기록 로더로 정상 구성 1회 → 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ 후보 **37**.
1218
★★**5개가 더 재귀한다**: `Throwable`·`Error`·`LinkageError`·`CharSequence`·`Comparable` = 기존 두 이름의 **상위형·인터페이스 폐포**.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"date": "2026-09-20",
3+
"taskId": "rustjava-error-path-name-the-missing-bootstrap-class-p0",
4+
"summary": "Adopted 2026-09-20-error-path-class-closure#p0. The `bootstrap_classes` loop in Jvm::new took the process down on `called Option::unwrap() on a None value`, so a host whose class set was missing one of the six had to bisect the list to learn which. It now names the class and says where the name was asked for. The sweep that found the defect could not have caught it -- it counted a panic as 'refused by name' without reading the message -- so the message is now read and matched against the hidden name, which is a second axis rather than a stricter version of the first.",
5+
"decision": "Keep the panic, name the class. Returning an error instead was measured and rejected for this round: `JavaError` has one variant carrying a `Box<dyn ClassInstance>`, and at this point in construction the classes an exception instance is made of are exactly the ones missing, so a new variant would hand the caller an error nothing can render. Recorded as proposal p0 rather than done here.",
6+
"measurements": {
7+
"cited_tree": "origin/main 97707f26",
8+
"sites_in_the_family": {
9+
"load_class(...).unwrap() on the construction path": 1,
10+
"class-registry get(name).unwrap() elsewhere": 4,
11+
"reachable from a host's class set (measured by the sweep)": 1
12+
},
13+
"sibling_sites_left_alone": ["jvm/src/jvm.rs:995", "jvm/src/jvm.rs:1002", "jvm/src/jvm.rs:1335", "jvm/src/garbage_collector.rs:109"],
14+
"bootstrap_classes": 6,
15+
"reachable_bootstrap_names": 5,
16+
"unreachable_bootstrap_name": "[B -- an array, synthesised by define_array_class, so no class set can lack it",
17+
"sweep_before_fix": { "recursed": 0, "refused_by_name": 7, "refused_anonymously": 5, "failed_cleanly": 25, "built": 0 },
18+
"sweep_after_fix": { "recursed": 0, "refused_by_name": 12, "refused_anonymously": 0, "failed_cleanly": 25, "built": 0 },
19+
"error_variant_blast_radius": { "irrefutable_let_destructures_in_repo": 3, "JavaError_mentions_in_repo": 528, "downstream": "public enum, breaking for consumers outside this repo (wie)" },
20+
"runtime_cost": "none -- the same single load_class call, an `unwrap` replaced by a `let ... else`"
21+
},
22+
"verification": {
23+
"bidirectional": "product call site jvm/src/jvm.rs, not a copy. With `.unwrap()` restored: panic text is `called Option::unwrap() on a None value` for all 5 reachable names and the sweep reports `7 refused by name · 5 refused anonymously` and FAILS. Restored: the text names the class for all 5 and the sweep reports `12 · 0` and passes.",
24+
"death_and_message_measured_separately": "rc alone would have passed the pre-fix code -- it panicked either way. The two facts are counted apart: process died (5/5 before, 5/5 after) and the hidden name appears in the panic text (0/5 before, 5/5 after).",
25+
"quiet_when_nothing_is_hidden": "a normal construction never reaches the branch: `cargo test --all` green, the sweep's `0 not needed / 0 recursed` line unchanged",
26+
"existing_locks_unchanged": "jvm/tests/test_exception_fallback_recursion.rs and the closure walk's own message are untouched",
27+
"dod": "all 9 DoD commands rc 0"
28+
},
29+
"changes": [
30+
"jvm/src/jvm.rs: the `bootstrap_classes` loop's `.unwrap()` becomes a `let ... else` naming the class, the count it is one of, and the loader it was asked of (the host's, not `java.class.path` -- the system class loader is built later in the same function); the stale comment in the closure walk below that called this defect 'left recorded rather than fixed' is corrected",
31+
"jvm/tests/test_error_path_class_sweep.rs: the panic payload is downcast and matched against the hidden name, so `Outcome::Panicked` means what its doc comment already claimed; a refusal that does not name its class is the new `PanickedAnonymously` and fails the sweep"
32+
],
33+
"issues": [
34+
"Four sibling `unwrap`s die anonymously on a missing class the same way (jvm.rs:995, :1002, :1335, garbage_collector.rs:109), but they read the class *registry* mid-run, not the host's class set, and the sweep measures them as unreachable from that axis: 0 anonymous refusals across 37 candidates after this fix. Left alone rather than fixed blind -- there is no test that can reach them, so a change there would be unlockable in both directions.",
35+
"The sweep's array exclusion is unchanged, so `[B` -- the sixth bootstrap class -- is still outside what this round could measure. It is synthesised by the loader, so no class set can lack it; that is the same argument last round made, not a new one."
36+
],
37+
"adoptedProposals": [
38+
"2026-09-20-error-path-class-closure#p0"
39+
],
40+
"proposals": [
41+
{
42+
"title": "Decide whether Jvm::new may panic at all, now that it does so twice",
43+
"plainSummary": "Building a JVM kills the process when the class set is incomplete, instead of returning an error the host can handle.",
44+
"userBenefit": "A host embedding the JVM could report a bad class set and carry on -- pick another runtime, show a message -- instead of having its process aborted by a library.",
45+
"why": "AGENTS.md says library code never panics and returns `Result<T>`, and `Jvm::new` already returns one. There are now two panics in it: the closure walk and, as of this round, the `bootstrap_classes` loop. Both are deliberate and both are explained in place, which is why this is a decision rather than a bug. Measured cost of the alternative: `JavaError` has a single variant holding a `Box<dyn ClassInstance>`, so a non-Java variant is needed; that breaks 3 irrefutable `let JavaError::JavaException(..)` destructures in this repo and is a breaking change for consumers outside it. Measured obstacle: at both sites the classes an exception is made of are the missing ones, so the caller would receive an error that cannot be turned into a Java exception -- the variant would have to be honestly non-Java, which is the decision.",
46+
"tradeoff": "The work is small and the benefit is real only for an embedder that can act on the failure; for a host that would abort anyway, a panic with a good message is as useful and nothing changes. Doing it also widens a public enum, which is the kind of change that is cheap now and expensive to reverse.",
47+
"effort": "M",
48+
"target": "jvm/src/error.rs, jvm/src/jvm.rs"
49+
}
50+
]
51+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# 2026-09-20 — 없는 부트스트랩 클래스의 «이름»을 말하게 했다
2+
3+
채택 제안 `2026-09-20-error-path-class-closure#p0` (티켓 `rustjava-error-path-name-the-missing-bootstrap-class-p0`).
4+
5+
## 왜 지금인가
6+
지난 회차의 스윕이 12건을 「이름을 대고 거절」로 셌는데, 그중 **5건의 실제 문면은
7+
`called Option::unwrap() on a None value`** 였다. 클래스 집합에 구멍이 있는 호스트는
8+
그 문면을 읽고도 **여섯 중 어느 것인지 몰라 목록을 이분해야** 했다. 그리고 그 5건 중 둘
9+
(`java/lang/Object`·`java/io/Serializable`)은 오류 경로 폐포에도 들어 있어, **같은 결손이
10+
어느 루프에 먼저 걸리느냐에 따라** 좋은 문면이 되기도 하고 쓸모없는 문면이 되기도 했다.
11+
12+
## 가족은 몇 곳인가 — 세고 나서 골랐다
13+
| 자리 | 형태 | 처분 |
14+
|---|---|---|
15+
| `jvm/src/jvm.rs:92` (`bootstrap_classes` 루프) | 호스트의 **클래스 집합** 결손 → 익명 사망 | ★**고쳤다** |
16+
| `jvm.rs:995`·`:1002`·`:1335`, `garbage_collector.rs:109` | 실행 중 **클래스 레지스트리** 조회 실패 → 익명 사망 | 두었다(아래) |
17+
18+
두 번째 줄 4곳은 **같은 모양이지만 다른 축**이다. 호스트가 만들 수 있는 입력이 아니라 「상위형은
19+
하위형보다 먼저 등록돼 있다」는 **내부 불변식**이고, 스윕이 그것을 실측한다 — 고친 뒤 37 후보 전건에서
20+
**익명 거절 0**. ⇒ 닿는 테스트가 없어 **양방향으로 잠글 수 없는 변경**이라 손대지 않고 여기 적는다.
21+
22+
## 무엇을 했나
23+
- `.unwrap()` → `let ... else { panic!(…) }`. 문면은 ⑴**클래스 이름** ⑵그것이 **여섯 중 하나**라는 것
24+
⑶★**어디에 물었는지** — `Jvm::new` 에 넘긴 **호스트 자신의 부트스트랩 로더**이지 클래스패스가 아니다
25+
(`java.class.path` 는 시스템 클래스로더 몫이고, 그것은 **이 함수 뒤쪽에서 만들어지며 여기서 실패하면 아예 안 돈다**).
26+
- 폐포 walk 위 주석의 「이름을 말하지 않는 unwrap … 고치지 않고 기록만 한다」는 **낡아서 고쳤다**.
27+
- 스윕의 `Outcome::Panicked` 가 **패닉 문면을 읽는다**. 종전에는 「죽었다」만 보고 「이름을 댔다」로 셌고,
28+
그래서 **이 결함을 자기 눈으로 보고도 좋은 쪽에 세었다**. 이름을 못 댄 거절은 `PanickedAnonymously` 로
29+
**따로 세고 그 자체가 실패**다.
30+
31+
## 양방향 — 「죽었다」와 「내 문면이 읽혔다」를 따로 쟀다
32+
제품 호출부(`jvm/src/jvm.rs`)를 되돌려 쟀다(사본 아님).
33+
34+
| | 패닉 문면(5개 이름 각각) | 이름 적중 | 스윕 |
35+
|---|---|---|---|
36+
| `.unwrap()` 복원(개악) | `called Option::unwrap() on a None value` | ★**0 / 5** | `0 recursed · 7 by name · **5 anonymous** · 25 clean` → **FAILED** |
37+
| 고침 | `the class set has no <name>, which is one of the 6 …` | ★**5 / 5** | `0 · **12** · **0** · 25` → **ok** |
38+
39+
★**rc 만 보면 두 줄이 같다** — 개악 쪽도 패닉이다. 그래서 두 사실을 **분리해** 셌다.
40+
정상 구성은 이 분기에 **닿지 않는다**(조용하다).
41+
42+
## 대가
43+
- 런타임 비용 **0** — 같은 `load_class` 호출 하나, `unwrap` 이 `let … else` 가 됐을 뿐이다.
44+
- 패닉을 **오류 반환으로 바꾸지 않았다.** `JavaError` 는 변종이 하나이고 그 안은
45+
`Box<dyn ClassInstance>` 다 — ★**여기서 없는 것이 바로 예외 인스턴스를 만드는 그 클래스들**이라
46+
새 변종은 「자바 예외가 아닌 오류」여야 한다. 사내 파급 **3곳**(반증 불가 `let` 해체) · 사외는
47+
공개 enum 파괴 변경. ⇒ 규모가 커져 **멈추고 제안 카드로 적었다**(계약 3).

‎jvm/src/jvm.rs‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,25 @@ impl Jvm {
8989
"java/lang/Class",
9090
];
9191
for class_name in bootstrap_classes.iter() {
92-
let class_definition = jvm.inner.bootstrap_class_loader.load_class(&jvm, class_name).await?.unwrap();
92+
// Panics like the closure walk below, and for the same reason -- nothing can be *raised*
93+
// yet, this is what loads the classes an exception is made of -- but it has to say which
94+
// name it was, which `unwrap` did not: a host with a gap in its class set read
95+
// `called Option::unwrap() on a None value` and had to bisect this list to find out
96+
// which of six. Measured by last round's sweep: 5 of the 12 named refusals were this
97+
// line, and two of those names (`java/lang/Object`, `java/io/Serializable`) are also in
98+
// the error path's closure, so the same gap got a good message or a useless one
99+
// depending only on which loop reached it first.
100+
let Some(class_definition) = jvm.inner.bootstrap_class_loader.load_class(&jvm, class_name).await? else {
101+
panic!(
102+
"the class set has no {class_name}, which is one of the {} classes loaded before \
103+
anything else and before any error can be raised. Asked of the bootstrap class \
104+
loader passed to `Jvm::new`, which is the host's own -- not the class path: \
105+
`java.class.path` belongs to the system class loader, which is built later in \
106+
this same function and never runs if this fails. Add {class_name} to that \
107+
loader's class set.",
108+
bootstrap_classes.len()
109+
)
110+
};
93111
let class = Class::new(class_definition, None, None);
94112

95113
jvm.register_class_internal(class, None).await?;
@@ -120,8 +138,7 @@ impl Jvm {
120138
// is reported with the very classes still being resolved. The closure is 9 names and the
121139
// account of it is complete: 2 were already checked, 5 recursed, and the remaining 2
122140
// (`java/lang/Object`, `java/io/Serializable`) are in `bootstrap_classes` above, so they fail
123-
// before this runs -- on an `unwrap` that does not say which class, which is a smaller defect
124-
// than this one and is left recorded rather than fixed here.
141+
// before this runs -- naming themselves there, as of the round that closed that gap.
125142
//
126143
// So the closure is what is checked, not a list someone has to remember to extend. Asked of
127144
// the loader directly and never through `resolve_class`, because the reporting path cannot

0 commit comments

Comments
 (0)