Skip to content

Commit e9910a7

Browse files
authored
[rustjava-jvm-exception-throws-instead-of-unwrap] fix(jvm): 일으키려던 예외를 못 만들면 «보고»한다 — 죽지 않는다 (#76)
[rustjava-jvm-exception-throws-instead-of-unwrap] fix(jvm): 일으키려던 예외를 못 만들면 «보고»한다 — 죽지 않는다
2 parents 97d2258 + 0fe03dc commit e9910a7

7 files changed

Lines changed: 271 additions & 9 deletions

‎REPORT.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
# REPORT
2+
## [2026-09-19] 일으키려던 예외를 못 만들면 «죽었다» — 그 보고를 손에 쥔 채로(rustjava-jvm-exception-throws-instead-of-unwrap)
3+
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`(worklog json `adoptedProposals` 기록). `Jvm::exception` 의 `.unwrap()` 두 개를 **반환**으로 바꿨다. ★**시그니처 불변 · 새 enum variant 0 · 호출부 편집 0.**
4+
- ★★**급소 — 실패가 «이미» JavaError 다.** `from_rust_string`·`new_class` 는 `jvm::Result<T>` = `Result<T, JavaError>` 를 돌려주므로 그 실패는 **그 자체가 자바 예외**다. unwrap 은 그것을 버리고 프로세스를 죽였다. ⇒ **그대로 돌려준다.**
5+
- ★**실측이 그 한 줄을 말한다**(가설 아님): 못 싣는 이름을 부르면
6+
`panicked at jvm/src/jvm.rs:948:94: called Result::unwrap() on an Err value: JavaException(ClassInstance(java/lang/NoClassDefFoundError))`
7+
⇒ ★`load_class` 가 **이미 올바른 `NoClassDefFoundError` 를 만들어 건넸는데** 그 보고가 «패닉 메시지 안에» 실려 사라졌다.
8+
- ★**재현 가능성을 두 축으로 갈라 적는다**(「이론상」과 「실측」을 섞지 않는다): ⒜**이 트리 자기 호출부에서는 0** — named 43 전건 loadable(PR #72 검사기가 잠근다) **그리고** 43 전건이 `<init>(Ljava/lang/String;)V` 를 **자기 proto 에** 갖는다(이 회차 실측 · 누락 0 · 조상 의존 0) ⒝★**공개 API 로는 도달한다** — `pub async fn exception` 이고 `wie` 가 이 크레이트를 싣는다. 로더가 못 주는 이름을 부르면 **호스트 프로세스가 죽는다**.
9+
- ★**양방향 축**(★제품 함수에 · 픽스처 사본 아님) `jvm/tests/test_exception_construction.rs`: 전 **FAILED**(위 패닉) ↔ 후 **ok**. ★`jvm.rs` 를 되돌리면 **red** 가 된다.
10+
- ★★**호출부 파급 — 「조용히 통과하는 경로」를 «구조로» 없앴다**: `.exception(` **846 → 846**(편집 0) · `JavaError::` **527자리 편집 0**. ⇒ ★**대안이던 「JavaError 에 variant 추가」였다면 `let …else`/`if let` **460자리**가 조용히 `else` 로 새고**, `jvm.rs:1073` 의 **반증불가 let**(variant 가 하나라서 컴파일되는 자리)이 깨졌다. 이 형상은 그 함정을 **애초에 만들지 않는다**.
11+
- ★**진짜 행동 변화 하나는 숨기지 않는다**: 실패 시 호출자는 **요청한 것과 «다른» 예외 클래스**를 받는다. ⇒ 잡은 예외를 클래스로 분기하는 **제품 12자리**(`print_stream`·`print_writer`·`filter_output_stream`·`formatter` 의 `IOException` · `integer`·`long` 의 `NumberFormatException`)는 그 경우 **매치하지 않고 전파**된다. 죽는 것보다 낫지만 **무해한 변경은 아니다**.
12+
- ★**안 고친 것**: ⒤★**퇴화 경우는 불변이고 그것은 패닉이 아니라 «무한 재귀»다** — `NoClassDefFoundError` 자신이 안 실리면 `load_class → exception → new_class → load_class` 가 돈다. ★**옛 unwrap 도 그것을 막지 못했다**(안쪽 `new_class` 가 애초에 돌아오지 않아 unwrap 에 닿지 않는다). ★**이것은 호출그래프에서 읽은 것이고 «실측이 아니다»** — 커스텀 로더 하네스가 필요해 범위 밖으로 뒀다(후속 카드) ⒥`from_rust_string` 쪽 unwrap 도 같은 방식으로 고쳤지만 ★**재현 경로를 못 찾았다**(그 실패는 `java/lang/String` 자체를 못 만들 때뿐) — **주장하지 않는다**.
13+
- ★★**이 회차가 «자기 diff 밖»에서 바꾼 것 둘 — 둘 다 선택이 아니었다**:
14+
⑴★**검사기 문면 3자리가 «거짓»이 됐다** — `check-named-exception-classes-are-loadable.py` 가 「unwraps … aborts the process」·「DELIBERATELY NOT DONE HERE: turning the `.unwrap()` into a thrown exception」·실패 메시지 「panics instead of throwing」을 **단언**하고 있었다. ⇒ **문면만 고쳤다(술어 무접촉)**. ★잠금의 «이유»가 바뀐다: 못 싣는 이름은 이제 **죽이지 않고 «틀린 예외»를 던진다**(IOException 을 물었는데 NoClassDefFoundError 가 와서 `catch` 가 안 걸린다) ⇒ **더 조용해졌으니 잠금 가치는 오히려 커졌다**. ★그 검사기 축을 양방향 재검증했다: 등재 1줄 제거 → **rc=1** · 원복 → **rc=0**.
15+
⑵★★**형제 회차의 «0» 이 «1» 이 된다** — `2026-09-18-nonliteral-exception-call-sites` 가 비리터럴 **0** 을 재고 ★**관문으로 올리지 않기로** 했는데, 하루 만에 **정당한 비리터럴 호출부**가 생겼다: ★**이 테스트는 «못 싣는 이름»을 불러야 하고**, 리터럴로 쓰면 loadable 검사기가 **실제로 red 였다**(실측 · `test_exception_construction.rs:13` 지목). ⇒ ★**그때 관문을 걸었다면 이 테스트가 막혔다.** 재측 = **비리터럴 1**(그 1이 이 테스트다).
16+
※그 술어가 ★**«주석 안의 토큰»도 센다**는 것도 이때 드러났다(주석 한 줄 때문에 2로 읽혔다 → 문구 교체). 형제 제안을 이어받는 회차 몫이다.
17+
- 검증: DoD 9명령 · 아래 절.
18+
- ★후속 추천: **퇴화 재귀에 바닥을 깔 것인가**(M · 재진입 가드 ↔ 비-예외 실패 표현 — 후자는 460자리를 조용히 바꾼다). 상세 = `docs/worklog/2026-09-19-exception-reports-instead-of-aborting.md`.
219
## [2026-09-19] 부분 클론도 «거절»할까 — ★**아니다. 모호했던 것은 «환경»이 아니라 «호출 하나»였다**(rustjava-partial-clone-refusal-decision)
320
- 무엇을: 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`(worklog json 기록). ★**거절하지 않는다** — 대신 `symbols()` 가 실패한 `git show` 를 «부재»로 읽기 «전»에 `git ls-tree` 로 그 경로가 트리에 있는지 묻는다. ★결정을 `preflight()` docstring 에 못박았다(다음 사람이 다시 묻지 않도록).
421
- ★★**추측하지 않고 «진짜 부분 클론»을 만들어 쟀다**(`--filter=blob:none` · 범위는 알려진 사고 머지 `e53b2142^..e53b2142`):

‎STATE.md‎

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

99
## 완료
10+
- [rustjava-jvm-exception-throws-instead-of-unwrap] ★★**일으키려던 예외를 못 만들면 죽던 것을 «보고»로 바꿨다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`. ★시그니처 불변 · variant 0 · 호출부 편집 0.
11+
★**급소**: `from_rust_string`·`new_class` 의 실패는 **이미 `JavaError`**(= 자바 예외)다 — unwrap 이 그것을 버렸다. ⇒ 그대로 돌려준다.
12+
★**실측**: `panicked … unwrap() on an Err value: JavaException(java/lang/NoClassDefFoundError)` — ★올바른 보고가 **패닉 메시지 안에** 실려 사라졌다.
13+
★**도달성 두 축**: 자기 호출부 **0**(named 43 전건 loadable + 43 전건 String 생성자 보유 · 이 회차 실측) ↔ ★**공개 API 로는 도달**(`wie` 가 싣는다 · 호스트가 죽는다).
14+
★**양방향**(제품 함수): 전 **FAILED**(패닉) ↔ 후 **ok** · 되돌리면 red.
15+
★**파급 0 의 근거**: `.exception(` **846→846** · `JavaError::` **527 편집 0** ⇒ variant 추가안이었다면 `let …else` **460자리**가 조용히 샜다(그래서 그 안을 버렸다).
16+
★**대가**: 실패 시 **다른 클래스의 예외**가 온다 ⇒ 클래스로 분기하는 **제품 12자리**는 못 잡고 전파한다(죽는 것보다 낫지만 무해하지 않다).
17+
★**불변**: 퇴화 경우(폴백 클래스 자체 부재)는 **무한 재귀**이고 옛 unwrap 도 못 막았다 — ★호출그래프에서 읽었고 **실측 아님**(후속 카드).
18+
★**자기 diff 밖 파급 둘**: ⑴검사기 문면 3자리가 거짓이 돼 **문면만** 고쳤다(술어 무접촉 · 축 양방향 재검증 rc=1/rc=0) — 잠금의 이유가 「죽는다」에서 ★**「틀린 예외가 온다」**로 바뀐다 ⑵★형제 회차의 비리터럴 **0 → 1**(이 테스트가 그 1이다) — ★**그 회차가 관문을 «안» 건 판단이 하루 만에 값을 했다**(걸었으면 이 테스트가 막혔다).
1019
- [rustjava-partial-clone-refusal-decision] ★★**부분 클론을 «거절하지 않는다» — 모호했던 것은 환경이 아니라 «호출 하나»였다.** 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`.
1120
★**진짜 blobless 클론으로 쟀다**: promisor **도달 가능**이면 답이 **완전 클론과 동일**(rc 1 · 6 dropped · 15.7s vs 2.5s) ⇒ ★거절은 «돌아가는 설정»을 막는 것.
1221
★**그러나 조용한 green 은 실재**: 신선한 blobless + promisor **도달 불가** → `0 dropped` · ★**rc 0**(완전 클론은 6건).
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "rustjava-jvm-exception-throws-instead-of-unwrap",
4+
"summary": "Jvm::exception unwrapped the two Results it builds the exception from, so a failure there aborted the process. Both failures are already JavaError - a Java exception describing what went wrong - so they are now returned. No signature change, no new enum variant, no call site edited: 846 exception( sites and 527 JavaError:: sites are untouched.",
5+
"measurements": {
6+
"exception_call_sites_before": 846,
7+
"exception_call_sites_after": 846,
8+
"javaerror_sites_untouched": 527,
9+
"javaerror_let_else_sites_that_a_new_variant_would_have_silently_diverted": 460,
10+
"product_sites_dispatching_on_exception_class": 12,
11+
"named_classes_all_loadable": 43,
12+
"named_classes_missing_string_ctor": 0,
13+
"named_classes_relying_on_ancestor_string_ctor": 0,
14+
"reachable_from_in_tree_call_sites": 0,
15+
"reachable_through_public_api": true,
16+
"nonliteral_call_sites_before_this_round": 0,
17+
"nonliteral_call_sites_after_this_round": 1
18+
},
19+
"verification": [
20+
"axis, product function (not a fixture copy): jvm/tests/test_exception_construction.rs asks for an unloadable class; BEFORE = FAILED with 'panicked at jvm/src/jvm.rs:948:94: called Result::unwrap() on an Err value: JavaException(java/lang/NoClassDefFoundError)'; AFTER = ok, 1 passed. Reverting jvm.rs turns it red.",
21+
"fan-out: .exception( count 846 before and after; no call site edited; no JavaError variant added, so none of the 460 let-else sites change behaviour",
22+
"reachability measured two ways: all 43 named classes loadable (checker, PR #72) and all 43 declare <init>(Ljava/lang/String;)V directly (0 missing, 0 inherited)",
23+
"the edited checker still bites both ways: removing one loader registration -> rc=1 naming java/lang/BootstrapMethodError; restored -> rc=0; normal form 846 call sites / 43 names / 268 loadable"
24+
],
25+
"changes": [
26+
"jvm/src/jvm.rs — Jvm::exception returns the JavaError it was given instead of unwrapping it (+ doc comment recording the measured panic)",
27+
"jvm/tests/test_exception_construction.rs — new axis test",
28+
"docs/worklog/2026-09-19-exception-reports-instead-of-aborting.{md,json}, REPORT.md, STATE.md",
29+
"scripts/check-named-exception-classes-are-loadable.py - rewrote the three passages this change falsified (docstring x2 + failure message); predicate untouched"
30+
],
31+
"issues": [
32+
"Behavioural, not a no-op: in the failure case the caller receives a different exception class than it asked for, so the 12 product sites that dispatch on the caught class will not match and the error propagates instead of being caught. Better than aborting, but it is a real difference.",
33+
"The degenerate case is unchanged and is unbounded recursion, not a panic: if java/lang/NoClassDefFoundError itself were unloadable, load_class -> exception -> new_class -> load_class cycles. Read off the call graph, NOT measured - the old unwrap never bounded it either, because the inner new_class never returns.",
34+
"No reproduction was found for the from_rust_string unwrap (it fails only if java/lang/String cannot be built). Fixed the same way but not claimed as measured.",
35+
"This round creates the tree's first non-literal exception( call site, so the sibling round's measured nonliteral count moves 0 -> 1. That round deliberately did not gate on the zero; had it done so, this test would have been blocked by it. The test cannot use a literal: a literal unloadable name turns check-named-exception-classes-are-loadable.py red (measured).",
36+
"While re-measuring, found that the sibling round's predicate also counts the token inside comments (a comment mentioning it read as a second non-literal site until reworded). Belongs to whoever picks up that proposal."
37+
],
38+
"adoptedProposals": [
39+
"2026-09-18-named-exception-classes-are-loadable#p1"
40+
],
41+
"proposals": [
42+
{
43+
"title": "Bound the exception-construction recursion when the fallback class itself is unloadable",
44+
"plainSummary": "If the runtime cannot even build the error it uses to report a missing class, it keeps trying in a loop until it runs out of stack. This was already true before this round's change; nothing here made it worse or better.",
45+
"userBenefit": "A host embedding the runtime with an incomplete class set would get a clear failure instead of a stack overflow, which is the one remaining way this path can still take the process down.",
46+
"why": "load_class raises NoClassDefFoundError by calling Jvm::exception, which calls new_class, which calls load_class. If that class is missing the cycle has no floor. This round deliberately did not touch it: bounding it needs either a re-entrancy guard on Jvm or a non-exception failure representation, both of which are larger than the adopted proposal's point, and proving it needs a custom-loader test harness this tree does not have.",
47+
"tradeoff": "A re-entrancy guard adds state to Jvm and a branch to the hottest error path; a non-exception JavaError variant would change 460 let-else sites' behaviour silently, which is exactly what this round avoided. Doing nothing leaves a stack overflow reachable only by an embedder whose loader lacks java/lang/NoClassDefFoundError.",
48+
"effort": "M",
49+
"target": "jvm/src/jvm.rs, jvm/src/error.rs, test-utils/src/lib.rs"
50+
}
51+
]
52+
}

0 commit comments

Comments
 (0)