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
9 changes: 9 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# REPORT
## [2026-09-20] 오류 경로의 클래스 집합을 «한 번에» 쟀다 — ★**답은 «둘»이 아니었다**(rustjava-string-on-the-error-path-p0)
- 무엇을: 채택 제안 `2026-09-19-string-on-the-error-path#p0`. 후보를 **유도**해(기록 로더로 정상 구성 1회 — 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ **37**) 하나씩 숨겨 재구성하는 **스윕**을 만들고 돌렸다.
- ★★**결과 — 제안이 「still just these two」면 «기록된 음성»이라 했던 그 가정이 «반증»됐다**: ★**5개가 더 재귀한다**(바닥 없음) — `java/lang/Throwable` · `Error` · `LinkageError` · `CharSequence` · `Comparable`. ★**우연이 아니다** — 기존 두 이름의 **상위형·인터페이스 폐포**다(클래스를 resolve 하면 상위형도 resolve 되고, 거기 결손은 «아직 resolve 중인 그 두 클래스»로 보고된다).
- ★**처방은 목록이 아니라 «폐포»다**: 손으로 적은 assert 2개 → **작업목록 루프 1개**(씨앗 2 · `interface_names()`·`super_class_name()` 을 밀어 넣는다 · ★로더에 **직접** 묻는다 — `resolve_class` 는 질문을 `Jvm::exception` 에 넘기고 그것이 곧 순환이다). ★**폐포 9개로 계산이 닫힌다**: 이미 막힌 **2** + 재귀 **5** + `bootstrap_classes` 가 먼저 잡는 **2**(`Object`·`Serializable`) ⇒ 설명 안 되는 이름 **0**.
- ★**양방향**(제품 호출부 · 사본 아님): 정상 `37 candidate(s): 0 recursed · 12 refused by name · 25 failed cleanly` **ok** ↔ `pending.extend(...)` 두 줄 제거(= 고침 전 동작) → ★**`5 recursed` FAILED** ↔ 복원 **ok**. ★기존 잠금 2건은 **손대지 않고 통과**한다(새 문안이 `has no java/lang/String`·`has no java/lang/NoClassDefFoundError` 를 그대로 담는다).
- ★**대가**: ⒜시작 비용 **로더 질문 44 → 51**(폐포 9 ↔ 종전 2). ★**벽시계는 재지 않았다** — 전 회차가 같은 자리에서 재고 「노이즈 아래」로 버렸다. ⒝`cargo test --all` 에 스윕 **~15초**. ⒞★**배열 5개는 제외**했고 근거는 측정이다 — 로더가 배열을 **합성**하므로(`define_array_class`) 클래스 집합이 배열을 빠뜨릴 수 없다. 그래도 쟀다: `[C` **재귀**(상한 20에 21질문) · ★`[Ljava/lang/String;` 는 **그 상한에서도 스택 오버플로**(순환이 로더로 안 돌아와 상한이 못 끝낸다) ⇒ in-process 로 못 도는 유일한 후보. ⒟부트스트랩 6개를 숨기면 **`Option::unwrap()` 패닉**이라 **이름을 말하지 않는다**(12건 중 5건).
- 검증: DoD 9명령 rc 0.
- ★후속 추천 **2건**: ⒜**부트스트랩 클래스의 이름 없는 unwrap 패닉**(S) ⒝**`[Ljava/lang/String;` 의 «상한이 못 끝내는» 두 번째 순환**(M). 상세 = `docs/worklog/2026-09-20-error-path-class-closure.{md,json}`.

## [2026-09-19] 오류 경로의 «또 하나»는 `java/lang/String` 이고 — ★**재귀한다**(rustjava-error-path-needs-java-lang-string-measure-first)
- 무엇을: 채택 제안 `2026-09-19-fallback-class-absence-fails-at-construction#p0`(worklog json 기록). ★**제안의 조건이 「측정이 먼저」였고 그대로 했다** — 선행 회차가 String 에 대해 **아무것도 주장하지 않았고**(자기 worklog 에 「실측 아님」이라 적었다) 셋 중 무엇인지가 열려 있었다.
- ★★**답 = ⒜ 재귀한다**(⒝ 깨끗한 실패도, ⒞ 이미 상주도 아니다). 하니스로 String 을 숨겨 이분법으로 경계를 찾았다: 상한 **116 생존 ↔ 117 `stack overflow, aborting`(SIGABRT · rc 134)** · ★**양쪽 2회씩 재현** · ★**상한 100000 도 abort** ⇒ 상한은 «하니스가 양보하는 지점»이지 **바닥이 아니다**.
Expand Down
7 changes: 7 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)

## 완료
- [rustjava-string-on-the-error-path-p0] ★★**오류 경로의 클래스 집합을 한 번에 쟀다 — 답은 «둘»이 아니었다.** 채택 제안 `2026-09-19-string-on-the-error-path#p0`.
★**후보를 «유도»했다**(손 목록 아님): 기록 로더로 정상 구성 1회 → 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ 후보 **37**.
★★**5개가 더 재귀한다**: `Throwable`·`Error`·`LinkageError`·`CharSequence`·`Comparable` = 기존 두 이름의 **상위형·인터페이스 폐포**.
★**처방 = 폐포 walk**(assert 2 → 작업목록 1 · 로더에 **직접** 질의). ★**폐포 9로 계산이 닫힌다**(막힌 2 + 재귀 5 + bootstrap 2) ⇒ 미설명 **0**.
★**양방향**: `0 recursed` ok ↔ `extend` 2줄 제거 → **5 recursed FAILED** ↔ 복원 ok. 기존 잠금 2건 **무수정 통과**.
★**대가**: 로더 질문 **44→51** · 스윕 **~15초** · ★배열 5개 제외(로더가 **합성**하므로 클래스 집합이 못 빠뜨린다 — 단 `[Ljava/lang/String;` 는 상한 20에서도 오버플로 = in-process 불가).
★**후속 2건**: 부트스트랩 unwrap 이 **이름을 말하지 않는다**(S) · `[Ljava/lang/String;` 의 두 번째 순환(M).
- [rustjava-error-path-needs-java-lang-string-measure-first] ★★**오류 경로의 또 하나 `java/lang/String` — ⒜ «재귀한다»로 확정하고 선재 확인을 넣었다.** 채택 제안 `2026-09-19-fallback-class-absence-fails-at-construction#p0`. ★제안의 조건이 「측정이 먼저」였고 그대로 했다.
★**실측**: 상한 **116 생존 ↔ 117 SIGABRT «stack overflow, aborting»**(양쪽 2회 재현) · ★**상한 100000 도 abort** ⇒ 바닥이 없다.
★**⒞ 아님을 구조로**: `bootstrap_classes` **6개에 String 없음** · `from_rust_class` 는 이름을 **`[B`(nameBytes)** 로 넣는다 ⇒ 처음 필요한 곳은 프로퍼티 루프.
Expand Down
61 changes: 61 additions & 0 deletions docs/worklog/2026-09-20-error-path-class-closure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"date": "2026-09-20",
"taskId": "rustjava-string-on-the-error-path-p0",
"summary": "Adopted 2026-09-19-string-on-the-error-path#p0. Swept every class name construction asks the bootstrap loader for, one hidden at a time. The answer to 'is it still just those two?' is no: five more recurse with no floor, and they are exactly the supertype/interface closure of the two known names. Jvm::new now walks that closure instead of naming classes one at a time, and the sweep stays as the lock.",
"decision": "Check the closure, not a list. The two hand-written asserts are replaced by a worklist that asks the bootstrap loader directly for java/lang/String, java/lang/NoClassDefFoundError and every supertype and interface they carry, so a class added to the error path later brings its own prerequisites with it.",
"measurements": {
"cited_tree": "origin/main 5d732b13",
"names_recorded_during_a_normal_construction": 51,
"distinct_names": 42,
"array_names_skipped": 5,
"candidates_swept": 37,
"before_fix": { "recursed": 5, "refused_by_name": 7, "failed_cleanly": 25, "built": 0 },
"after_fix": { "recursed": 0, "refused_by_name": 12, "failed_cleanly": 25, "built": 0 },
"recursing_names": ["java/lang/Throwable", "java/lang/Error", "java/lang/LinkageError", "java/lang/CharSequence", "java/lang/Comparable"],
"closure_size": 9,
"closure": ["java/lang/NoClassDefFoundError", "java/lang/LinkageError", "java/lang/Error", "java/lang/Throwable", "java/lang/Object", "java/io/Serializable", "java/lang/String", "java/lang/CharSequence", "java/lang/Comparable"],
"closure_account": "2 already checked + 5 recursing + 2 (Object, Serializable) already in bootstrap_classes, so the 9 are fully accounted for",
"loader_questions_per_startup": "44 -> 51: the walk asks 9 where the two asserts it replaces asked 2",
"sweep_runtime_s": "13.98 / 15.92 / 30.79 across runs on a loaded host",
"give_up_after": 20
},
"verification": {
"bidirectional": "product call site jvm/src/jvm.rs, not a copy: with the two `pending.extend(...)` lines removed (i.e. the pre-fix behaviour of checking only the two names) the sweep reports `5 recursed` and FAILS; restored, it reports `0 recursed` and passes",
"existing_locks_unchanged": "jvm/tests/test_exception_fallback_recursion.rs passes untouched -- the new panic message still contains `has no java/lang/String` and `has no java/lang/NoClassDefFoundError`, which is what those two should_panic tests match",
"arrays_excluded_with_a_measurement_not_an_opinion": "the bootstrap loader synthesises array classes (define_array_class), so no class set can lack one. Measured anyway: hiding `[C` recurses (21 questions at a cap of 20) and hiding `[Ljava/lang/String;` overflows the stack at that same cap -- its recursion does not come back through the loader, so the cap cannot end it. That last one is the only candidate this sweep cannot run in-process.",
"dod": "all 9 DoD commands rc 0"
},
"changes": [
"jvm/src/jvm.rs: the two hand-written asserts become one worklist over the supertype/interface closure, asked of the loader directly; the NoClassDefFoundError resolve_class stays where it was",
"test-utils/src/lib.rs: RecordsRequests + test_jvm_recording (the candidate list has to come from what the loader is actually asked); test_jvm_hiding returns (Result<Jvm>, count) so the count survives a failing run -- the failing runs are the interesting ones",
"jvm/tests/test_error_path_class_sweep.rs: the sweep, kept as the lock"
],
"issues": [
"Hiding one of the six bootstrap_classes fails on `called Option::unwrap() on a None value`, which does not say which class. Bounded, so not this round's defect, but it is 5 of the 12 named refusals and the message is useless. Recorded as proposal p0.",
"Why `[Ljava/lang/String;` overflows at a cap of 20 was not chased: it is out of reach of the class-set axis. Recorded as proposal p1.",
"The sweep costs ~15s of `cargo test --all`. That is the price of the generalisation and it was not hidden."
],
"adoptedProposals": [
"2026-09-19-string-on-the-error-path#p0"
],
"proposals": [
{
"title": "Say which bootstrap class is missing instead of unwrapping on None",
"plainSummary": "If a host's class set is missing one of the six classes loaded first, the runtime dies with a message that does not name it.",
"userBenefit": "A host with a gap in its class set reads the name of the missing class instead of a bare unwrap panic, which is the same thing the error path's own check already gives for the other names.",
"why": "Measured by the sweep this round: of 37 candidates, 12 refuse by name and 5 of those 12 are `called Option::unwrap() on a None value` -- java/lang/Object, java/lang/Runnable, java/lang/Thread, java/io/Serializable and java/lang/Class, from the `bootstrap_classes` loop in Jvm::new. They fail at construction, which is correct, but the panic says nothing. Two of them (Object, Serializable) are also in the error path's closure, so the same gap gets a good message or a useless one depending only on which loop reaches it first.",
"tradeoff": "It is a one-line change to an `unwrap` and cannot go wrong, which is also the argument against doing it at all -- nothing is broken, only unhelpful. Against that: the round that has to debug it pays the whole cost, and this round has just demonstrated that reading a class-set failure without a name is exactly the expensive kind.",
"effort": "S",
"target": "jvm/src/jvm.rs"
},
{
"title": "Find out why hiding [Ljava/lang/String; overflows the stack at a cap of 20",
"plainSummary": "One array class, when the loader refuses it, loops in a way that the test harness's escape hatch cannot stop.",
"userBenefit": "Nothing directly -- it is a bound on how much the sweep can see, and knowing it is what stops the next round from trusting a green sweep more than it should.",
"why": "Measured: hiding `[C` recurses and stops at the cap (21 questions at a cap of 20), but hiding `[Ljava/lang/String;` aborts with `stack overflow, aborting` at that same cap. The cap only ends a loop that comes back through the loader, so this one does not -- there is a second cycle shape here that the sweep's instrument cannot observe. It is out of reach of the class-set axis (array classes are synthesised, not supplied), so the sweep skips arrays and says so, but 'cannot be reached today' is a weaker claim than 'cannot exist'.",
"tradeoff": "This is curiosity about a path no host can take, and the honest expected result is a recorded explanation rather than a fix. The cost of not doing it is that the sweep's array exclusion rests on one sentence that nobody has checked against the second cycle.",
"effort": "M",
"target": "jvm/src/jvm.rs, jvm/tests/test_error_path_class_sweep.rs"
}
]
}
70 changes: 70 additions & 0 deletions docs/worklog/2026-09-20-error-path-class-closure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 2026-09-20 — 오류 경로의 클래스 집합을 «한 번에» 쟀다 · 답은 «둘이 아니었다»

채택 제안 `2026-09-19-string-on-the-error-path#p0`.
티켓 `rustjava-string-on-the-error-path-p0` · 인용 트리 `origin/main 5d732b13`.

## 제안이 물은 것

> 「This round and the one before it each closed exactly one class, and each found the next by
> **reading the code and guessing** … A sweep that **hides each bootstrap-reachable name in turn**
> and records which ones recurse would answer the whole question once.」
> tradeoff — 「If it turns out the answer is **still just these two**, the result is a **recorded
> negative** rather than a new check.」

## 답 — ★**「still just these two」가 아니다**

기록 로더로 정상 구성을 한 번 돌려 후보를 «유도»했다(손으로 적지 않았다): 요청 **51건** · 서로 다른 이름 **42** ·
배열 이름 **5** 제외 ⇒ 후보 **37**. 그 37개를 하나씩 숨겨 재구성했다.

| 분류 | 고침 전 | 고침 후 |
|---|---|---|
| ★**재귀(바닥 없음)** | **5** | ★**0** |
| 이름을 들어 거절 | 7 | **12** |
| 한 번 묻고 깨끗이 실패 | 25 | 25 |
| 없어도 구성됨 | 0 | 0 |

★**재귀한 5개** = `java/lang/Throwable` · `java/lang/Error` · `java/lang/LinkageError` ·
`java/lang/CharSequence` · `java/lang/Comparable`.
★**그 5개는 우연이 아니다** — 기존에 막아 둔 두 이름의 **상위형·인터페이스 폐포(closure)** 다.
클래스를 resolve 하면 상위형도 resolve 되고, ★**거기서 난 결손은 «아직 resolve 중인 바로 그 두 클래스»로 보고된다.**

## 처방 — 목록이 아니라 «폐포»를 본다

`Jvm::new` 의 손으로 적은 assert 두 개를 **작업목록 루프 하나**로 바꿨다. 씨앗은 그 두 이름이고,
얻은 정의의 `interface_names()`·`super_class_name()` 을 계속 밀어 넣는다. 로더에 **직접** 묻는다 —
`resolve_class` 로 물으면 그 질문이 `Jvm::exception` 으로 가고, 그것이 곧 순환이다.

★**폐포는 9개이고 계산이 «닫힌다»**: `NoClassDefFoundError · LinkageError · Error · Throwable ·
Object · Serializable · String · CharSequence · Comparable`
= **이미 막혀 있던 2** + **재귀한 5** + **`bootstrap_classes` 가 먼저 잡는 2**(`Object`·`Serializable`).
⇒ 설명되지 않는 이름이 **0** 이다.

## 양방향 — 제품 호출부에서(사본 아님)

| | 개악 | 결과 |
|---|---|---|
| **정상** | 없음 | `37 candidate(s): 0 recursed · 12 refused by name · 25 failed cleanly · 0 not needed` · **ok** |
| **개악** | `jvm/src/jvm.rs` 의 `pending.extend(...)` 두 줄 제거(= 두 이름만 보던 고침 전 동작) | ★**`5 recursed` · FAILED** |
| **복원** | 되돌림 | **0 recursed · ok** |

★기존 잠금 `jvm/tests/test_exception_fallback_recursion.rs` **2건은 손대지 않고 그대로 통과**한다 —
새 패닉 문안이 `has no java/lang/String` · `has no java/lang/NoClassDefFoundError` 를 그대로 담기 때문이다.

## 대가 — 숨기지 않는다

- ★**시작 비용**: 로더 질문 **44 → 51**(폐포 9회 ↔ 종전 2회). 벽시계는 재지 «않았다» —
전 회차가 같은 자리에서 재고 「이 호스트의 노이즈 아래」로 버렸다. **결정론적 수로만 말한다.**
- ★**`cargo test --all` 에 스윕 ~15초**가 붙는다. 일반화의 값이고, 깎지 않았다.
- ★**배열 이름 5개는 스윕에서 제외**했고 그 이유는 «의견이 아니라 측정»이다: 부트스트랩 로더가
배열을 **합성**한다(`define_array_class`) ⇒ 어떤 클래스 집합도 배열을 «빠뜨릴» 수 없다.
그래도 재 봤다 — `[C` 는 **재귀**(상한 20에 21질문) · ★**`[Ljava/lang/String;` 는 «그 상한에서도»
스택 오버플로**다(= 그 순환은 로더로 돌아오지 않아 상한이 끝내지 못한다). ⇒ ★**이 스윕이 in-process 로
돌릴 수 없는 유일한 후보**이고, 제안 `#p1` 로 남겼다.
- ★**부트스트랩 6개를 숨기면 `called Option::unwrap() on a None value`** 로 죽는다 — 구성 시점에
멈추니 옳지만 **이름을 말하지 않는다**. 12건 중 **5건**이 그것이다. 이 회차의 결함이 아니라 별 계급이라
제안 `#p0` 으로 남겼다.

## 왜 지금인가

전 두 회차가 **각각 한 클래스씩** 닫았고 다음 이름을 **읽고 추측해** 찾았다. 그 속도로는 폐포 9개를 닫는 데
회차 7번이 더 든다 — 그리고 ★**그 방식은 「이게 마지막인가」에 영원히 답하지 못한다.** 이번에 기계가 답했다.
Loading
Loading