Skip to content

Commit 64cc4f6

Browse files
authored
[2026-09-19-exception-reports-instead-of-aborting-p0] fix(jvm): 보고자의 부재는 «생성 시점»에 이름을 들어 실패한다 (#80)
[2026-09-19-exception-reports-instead-of-aborting-p0] fix(jvm): 보고자의 부재는 «생성 시점»에 이름을 들어 실패한다
2 parents ad9eb1a + 3671b2d commit 64cc4f6

7 files changed

Lines changed: 255 additions & 0 deletions

‎REPORT.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010
- ★**되돌릴 조건(사전 등록)**: 제안이 스스로 댄 계수 — 「재유도 결함이 몇 개 더 나오는가」. ★**오늘 값은 «고침 이후 0»이고 그것은 «하루»짜리 증거라 논거로 쓰지 않았다.** ⇒ ★**loadable 재유도 경로에서 «세 번째» 결함이 나오면 이 결정을 다시 연다**(`named` 스캔 결함은 세지 마라 — 로더 읽기가 고치는 자리가 아니다).
1111
- 검증: DoD 9명령 · 아래 절.
1212
- ★후속 추천: **재유도가 «짧게 나왔는지»를 단언할 것인가**(S · 위 불변식 — 이 회차가 값을 쟀고 짓지는 않았다). 상세 = `docs/worklog/2026-09-19-loadable-set-source-of-truth.md`.
13+
## [2026-09-19] 보고자는 «자기 부재»를 보고할 수 없다 — 로더에 직접 묻는다(2026-09-19-exception-reports-instead-of-aborting-p0)
14+
- 무엇을: 채택 제안 `2026-09-19-exception-reports-instead-of-aborting#p0`(worklog json 기록). ★**제안이 옳았고, 이 회차가 그것을 «처음 실측»했다** — 제안한 회차는 자기 worklog 에 「호출그래프에서 읽은 것이고 **실측이 아니다** · 커스텀 로더 하네스가 필요해 범위 밖」이라 적었다. 그 하네스(`test_jvm_hiding`)를 여기서 만들었다.
15+
- ★★**실측**: `java/lang/NoClassDefFoundError` 를 숨기는 로더로 — 상한 5/20/60/120 → **6/21/61/121 왕복 후 완료** · 상한 **160·200 → ★`stack overflow, aborting`(SIGABRT)**. ⇒ ★**바닥이 없고, 121~160 사이에서 프로세스가 죽는다.**
16+
- ★★**측정이 설계를 «두 번» 기각했다**(종이 위에서는 둘 다 옳아 보였다):
17+
⑴**`Jvm::new` 의 `bootstrap_classes` 에 추가** → **6 테스트 즉사**. 패닉 줄이 `threads.get_mut(&thread_id).unwrap()` 로 매핑된다 ⇒ ★**클래스 해석은 «초기화»를 돌리고, 그 목록 «아래»에서 붙는 스레드가 필요하다.**
18+
⑵**시스템 클래스로더 뒤에서 `resolve_class`** → 정상 기동 **6/6 통과**이고 «제공 가능한 로더»에겐 순환이 실제로 도달 불가가 된다. ★**그러나 숨김 로더에선 여전히 스택 오버플로**였다 — 시점만 «생성 중»으로 옮겼을 뿐. ⇒ ★**근인은 배치가 아니라 «형태»다: 보고 경로는 «보고자의 부재»를 보고할 수 없다.**
19+
- ★**지은 것**: 예외 기구를 **우회해** 로더에 **직접** 묻고, 없으면 그 자리에서 **이름을 들어 실패**한다. 그 뒤 `resolve_class` 로 등재해 이후 로더를 다시 묻지 않게 한다.
20+
- ★**양방향 축(제품 경로)**: 전 **SIGABRT 스택 오버플로 — 테스트 바이너리째 죽는다** ↔ 후 **생성 시점에 그 메시지로 패닉**(`should_panic` 통과). ★되돌리면 «실패»가 아니라 **바이너리가 내려간다** — 그것이 호스트가 겪던 바로 그 실패다. 정상 기동은 전후 **6/6 불변**.
21+
- ★**대가(숨기지 않는다)**: ⒜★**패닉이고 `AGENTS.md` 는 라이브러리 패닉을 금한다** — 실제 충돌이라 그대로 적는다. `Err` 반환은 **불가능**(`JavaError` 가 `ClassInstance` 를 품는데 그 인스턴스를 만들 클래스가 바로 없는 것이다) · 비-예외 variant 는 **460 `let…else` 자리를 조용히 바꾼다**며 `Jvm::exception` 회차가 이미 기각했다 · ★`Jvm::new` 는 기존 6클래스에 대해 **이미 `.unwrap()`** 한다(같은 계급의 실패) ⇒ 이 파일의 기존 답에 **메시지를 붙인 것**이다. ⒝기동마다 클래스 1개를 더 해석한다 ⒞★**필요보다 일찍 실패한다** — 에러 경로를 안 밟던 호스트는 이제 **JVM 을 못 만든다**(의도한 교환: 대안은 «에러 경로에서 죽을 때까지 돈다») ⒟**이 순환만** 막는다 — `java/lang/String` 이 다음 후보다(`exception()` 이 메시지 문자열을 먼저 만든다). ★**재지 않았고 주장하지 않는다**(후속 카드).
22+
- ★**노력도**: 제안 추정 **M** 이 맞았다 — 단 이유가 다르다. 하네스는 쉬웠고(~40줄), 비용은 ★**앞의 두 설계를 «재서» 기각해야 했다는 것**이다.
23+
- 검증: DoD 9명령 · 아래 절.
24+
- ★후속 추천: **에러 경로가 필요로 하는 «다른» 클래스(`java/lang/String`)도 미리 확인할 것인가**(S). 상세 = `docs/worklog/2026-09-19-fallback-class-absence-fails-at-construction.md`.
25+
1326
## [2026-09-19] 일으키려던 예외를 못 만들면 «죽었다» — 그 보고를 손에 쥔 채로(rustjava-jvm-exception-throws-instead-of-unwrap)
1427
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`(worklog json `adoptedProposals` 기록). `Jvm::exception` 의 `.unwrap()` 두 개를 **반환**으로 바꿨다. ★**시그니처 불변 · 새 enum variant 0 · 호출부 편집 0.**
1528
- ★★**급소 — 실패가 «이미» JavaError 다.** `from_rust_string`·`new_class` 는 `jvm::Result<T>` = `Result<T, JavaError>` 를 돌려주므로 그 실패는 **그 자체가 자바 예외**다. unwrap 은 그것을 버리고 프로세스를 죽였다. ⇒ **그대로 돌려준다.**

‎STATE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
★★**결정적 실측**: 초판 검사기를 지금 트리에서 돌리면 **265/263 ↔ 등재 줄 268** ⇒ ★불변식 하나로 **결함 2건이 1회차에 잡혔을 값**(현행 268/268/268). ★단 «모든 오귀속»은 못 잡는다(과소계수 계급만).
1515
★**브리프 전제 1건이 거짓**: 이 repo 엔 `machine-independence-guard` 가 **없다**(다른 repo 축).
1616
★**재개 조건 사전 등록**: loadable 재유도 경로에서 **세 번째** 결함이 나오면 다시 연다(「고침 이후 0」은 하루짜리라 논거로 쓰지 않았다).
17+
- [2026-09-19-exception-reports-instead-of-aborting-p0] ★★**보고자의 부재는 보고될 수 없다 — 로더에 직접 묻고 «이름을 들어» 실패한다.** 채택 제안 `2026-09-19-exception-reports-instead-of-aborting#p0`.
18+
★**이 회차가 그 순환을 «처음 실측»했다**(제안 회차는 「실측 아님」이라 적었다): 숨김 로더로 **6/21/61/121 왕복** 후 완료 · 상한 160·200 → ★**SIGABRT 스택 오버플로** ⇒ 바닥 없음 · **121~160 사이**에서 죽는다.
19+
★★**측정이 설계를 두 번 기각했다**: ⑴부트스트랩 목록 추가 → **6 테스트 즉사**(해석이 «초기화»를 돌려 스레드가 필요) ⑵시스템 로더 뒤 `resolve_class` → 정상 6/6 인데 ★**숨김 로더에선 여전히 스택 오버플로**(시점만 이동).
20+
★**처방**: 예외 기구를 **우회**해 로더에 직접 묻는다 — 없으면 생성 시점에 이름을 들어 실패 · 그 뒤 등재해 재질의 0.
21+
★**축**: 전 **바이너리째 SIGABRT** ↔ 후 **`should_panic` 통과** · 정상 기동 **6/6 불변**.
22+
★**대가**: ★패닉이고 `AGENTS.md` 와 충돌한다(대안 둘은 각각 «불가능»·«460자리 무언 변경»이라 기각 · `Jvm::new` 는 이미 unwrap 한다) · 필요보다 일찍 실패 · **이 순환만** 막는다(`String` 은 미측정 · 후속 카드).
1723
- [rustjava-jvm-exception-throws-instead-of-unwrap] ★★**일으키려던 예외를 못 만들면 죽던 것을 «보고»로 바꿨다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`. ★시그니처 불변 · variant 0 · 호출부 편집 0.
1824
★**급소**: `from_rust_string`·`new_class` 의 실패는 **이미 `JavaError`**(= 자바 예외)다 — unwrap 이 그것을 버렸다. ⇒ 그대로 돌려준다.
1925
★**실측**: `panicked … unwrap() on an Err value: JavaException(java/lang/NoClassDefFoundError)` — ★올바른 보고가 **패닉 메시지 안에** 실려 사라졌다.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "2026-09-19-exception-reports-instead-of-aborting-p0",
4+
"summary": "Measured the exception-construction cycle for the first time (the round that proposed it could not) and bounded it. With a loader that hides java/lang/NoClassDefFoundError the cycle survives 121 round trips and aborts on a stack overflow before 160. Jvm::new now asks the loader for that class directly - bypassing the exception machinery, which cannot report its own absence - and fails immediately with a named message instead.",
5+
"measurements": {
6+
"round_trips_survived_cap_5": 6,
7+
"round_trips_survived_cap_20": 21,
8+
"round_trips_survived_cap_60": 61,
9+
"round_trips_survived_cap_120": 121,
10+
"stack_overflow_between": "121 and 160",
11+
"files_changed": 3,
12+
"tests_added": 1,
13+
"normal_startup_tests_before": 6,
14+
"normal_startup_tests_after": 6,
15+
"designs_rejected_by_measurement": 2
16+
},
17+
"verification": [
18+
"cycle measured with a new harness (test_jvm_hiding in test-utils): caps 5/20/60/120 complete with 6/21/61/121 requests for the hidden class; caps 160 and 200 die with 'fatal runtime error: stack overflow, aborting' (SIGABRT)",
19+
"rejected design 1 - adding the class to Jvm::new bootstrap_classes - failed 6 tests; the panic line mapped to threads.get_mut(&thread_id).unwrap(), i.e. resolution runs class initialisation which needs the thread attached after that list",
20+
"rejected design 2 - resolving it after the system class loader - passed normal startup 6/6 but still overflowed the stack against the hiding loader, only during construction; the reporting path cannot report the reporter's absence",
21+
"axis: before = SIGABRT stack overflow that takes the test binary down; after = should_panic test passes on the named message; test_string 6/6 unaffected in both"
22+
],
23+
"changes": [
24+
"jvm/src/jvm.rs - Jvm::new asks the bootstrap loader directly for java/lang/NoClassDefFoundError and fails with a named message, then resolves it so the registry answers later",
25+
"test-utils/src/lib.rs - HidesOneClass loader wrapper and test_jvm_hiding(hidden, give_up_after)",
26+
"jvm/tests/test_exception_fallback_recursion.rs - new regression test",
27+
"docs/worklog/2026-09-19-fallback-class-absence-fails-at-construction.{md,json}, REPORT.md, STATE.md"
28+
],
29+
"issues": [
30+
"It is a panic, and AGENTS.md says library code should not panic. Returning Err is impossible - JavaError carries a ClassInstance and the missing class is what would have to be instantiated - and a non-exception variant was rejected by the round that landed Jvm::exception because it silently changes 460 let-else sites. Jvm::new already unwraps for the same class of failure on its six bootstrap classes.",
31+
"A host whose class set lacks the reporter but which never takes an error path used to run; now it cannot construct a JVM at all. Deliberate: the alternative is that it runs until an error path aborts it.",
32+
"One more class is resolved at every JVM startup, for a condition no complete class set will hit.",
33+
"The bound covers this cycle only. java/lang/String is the obvious next candidate, since exception() builds the message string first. Not measured, not claimed."
34+
],
35+
"adoptedProposals": [
36+
"2026-09-19-exception-reports-instead-of-aborting#p0"
37+
],
38+
"proposals": [
39+
{
40+
"title": "Check the other class the error path needs before it is needed: java/lang/String",
41+
"plainSummary": "Raising an error also builds its message text, which needs the String class. If a host's class set lacked that one, the same kind of failure would happen and nothing checks for it yet.",
42+
"userBenefit": "A host with an incomplete class set would learn at start-up, by name, rather than when the first error is raised.",
43+
"why": "Jvm::exception calls JavaLangString::from_rust_string before it builds the exception instance, so String is on the error path exactly as NoClassDefFoundError is. This round measured and closed the NoClassDefFoundError cycle and deliberately did not claim anything about String: no harness run was done for it, and whether it recurses, fails cleanly, or is already resident by construction time is unknown. The harness added here (test_jvm_hiding) makes that a short measurement rather than a design question.",
44+
"tradeoff": "If String turns out to be resident well before any error path can run, the check is dead weight on every start-up and one more line asserting something that cannot happen; the measurement has to come first, which is why this is a proposal and not a second assert.",
45+
"effort": "S",
46+
"target": "jvm/src/jvm.rs, jvm/tests/test_exception_fallback_recursion.rs"
47+
}
48+
]
49+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 2026-09-19 — The reporter cannot report its own absence. Ask the loader directly instead.
2+
3+
Round: `2026-09-19-exception-reports-instead-of-aborting-p0`
4+
Adopted proposal: `2026-09-19-exception-reports-instead-of-aborting#p0` —
5+
*"Bound the exception-construction recursion when the fallback class itself is unloadable."*
6+
7+
## The proposal was right, and this round is the first time it was measured
8+
9+
The round that filed it wrote, in its own worklog: *"This is read off the call graph, **not measured**
10+
— constructing a JVM whose loader lacks that class needs a custom loader harness, which was out of
11+
this round's scope."* That harness is `test_jvm_hiding` in `test-utils`, added here. With it:
12+
13+
| loader | result |
14+
|---|---|
15+
| hides `java/lang/NoClassDefFoundError`, cap 5 / 20 / 60 / 120 | completes — **6 / 21 / 61 / 121** round trips through the cycle |
16+
| same, cap 160 or 200 | ★ `thread has overflowed its stack` · `fatal runtime error: stack overflow, aborting` · **SIGABRT** |
17+
18+
So the cycle is real, it has no floor, and it kills the process somewhere between **121 and 160**
19+
levels. `load_class` reports a class it cannot provide by calling
20+
`Jvm::exception("java/lang/NoClassDefFoundError", …)`; building that exception goes back through the
21+
loader; if the loader cannot provide *that* class either, the two call each other forever.
22+
23+
## Two attempts that measurement rejected
24+
25+
Neither was wrong on paper. Both were wrong in the tree.
26+
27+
**1. Add it to `Jvm::new`'s `bootstrap_classes`.** Six tests failed instantly. The panic line mapped
28+
to `threads.get_mut(&thread_id).unwrap()` — resolving a class runs its initialisation, which needs
29+
the thread attached *below* that list. The list is for definitions that need nothing.
30+
31+
**2. Resolve it after the system class loader, so the registry answers later.** Normal startup passed
32+
(6/6), and for every loader that *can* provide the class the cycle does become unreachable. But
33+
against the hiding loader it **still overflowed the stack** — only now during construction. The
34+
proposal's stated benefit is *"a clear failure instead of a stack overflow"*, and this was the same
35+
stack overflow at a different time.
36+
37+
The reason is the shape of the problem, not the placement: **the reporting path cannot report the
38+
absence of the reporter.** `resolve_class` hands failure to `Jvm::exception`, which is the cycle.
39+
40+
## What was built
41+
42+
Ask the loader **directly**, bypassing the exception machinery, before resolving:
43+
44+
```rust
45+
assert!(
46+
jvm.inner.bootstrap_class_loader.load_class(&jvm, "java/lang/NoClassDefFoundError").await?.is_some(),
47+
"the class set has no java/lang/NoClassDefFoundError, which is the class this runtime reports \
48+
every other missing class with. …that recursion has no floor (measured: 121 round trips, then \
49+
the process aborts on a stack overflow). Add it to the class set."
50+
);
51+
jvm.resolve_class("java/lang/NoClassDefFoundError").await?;
52+
```
53+
54+
One direct question turns the condition into an immediate, named failure; the `resolve_class` that
55+
follows registers the class so the loader is never asked again on an error path.
56+
57+
**Changed: 3 files.** `jvm/src/jvm.rs` (the check + comment), `test-utils/src/lib.rs` (the harness),
58+
`jvm/tests/test_exception_fallback_recursion.rs` (new, 1 test).
59+
60+
## Axis — bidirectional, on the product path
61+
62+
| form of `Jvm::new` | a class set without the reporter |
63+
|---|---|
64+
| before | `stack overflow, aborting` — **SIGABRT, the test binary dies** |
65+
| after | panics at construction with the message above — `should_panic` test passes |
66+
67+
Reverting the check does not merely fail the test, it **takes the test binary down**, which is
68+
exactly the failure a host used to get. Normal startup is unaffected: `test_string` 6/6 before and
69+
after.
70+
71+
## What this costs
72+
73+
- **It is a panic, and `AGENTS.md` says library code should not panic.** Stated plainly because it is
74+
a real conflict. The alternatives were measured and rejected: returning `Err` is impossible here —
75+
`JavaError` carries a `ClassInstance`, and the missing class is precisely what would have to be
76+
instantiated — and adding a non-exception variant was rejected by the round that landed
77+
`Jvm::exception`, because it silently changes 460 `let…else` sites. `Jvm::new` already `.unwrap()`s
78+
for the same class of failure on its six bootstrap classes, so this is the file's existing answer
79+
to "the class set is unusable", now with a message instead of `called Option::unwrap() on a None value`.
80+
- **One more class is resolved at every JVM startup**, for a condition that no complete class set
81+
will ever hit.
82+
- **It fails earlier than strictly necessary.** A host whose class set lacks the reporter but which
83+
never takes an error path used to run fine; now it cannot construct a JVM at all. That is a
84+
deliberate trade — the alternative is that it runs until an error path aborts it.
85+
- **The bound is on this cycle only.** Nothing here prevents a different pair of classes from
86+
recursing the same way; `java/lang/String` is the obvious candidate, since `exception()` builds the
87+
message string first. Not measured, not claimed, filed below.
88+
89+
## Effort
90+
91+
The proposal estimated **M**. That was right, and not for the reason it gave: the harness was the
92+
easy part (~40 lines). The cost was that **the first two designs had to be measured to be rejected**.

0 commit comments

Comments
 (0)