Skip to content

Commit 5887039

Browse files
jun0claude
andcommitted
[rustjava-checker-output-determinism-has-no-guard] merge origin/main — 승인된 원장 2파일 합집합
REPORT.md·STATE.md 두 원장 파일만 충돌했다(코드 파일 충돌 0). 양쪽 항목 전건 보존 · 시간순 (origin/main `2b2a5b1c` 15:48 먼저, 이 회차 `3dbaaaf4` 13:54 뒤) 합집합으로 해소했다. 기여 불변 확인: diff <(git diff --numstat 5d732b1 HEAD) <(git diff --numstat origin/main <tree>) rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 parents 3dbaaaf + 2b2a5b1 commit 5887039

7 files changed

Lines changed: 370 additions & 52 deletions

File tree

‎REPORT.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
# REPORT
2+
## [2026-09-20] 오류 경로의 클래스 집합을 «한 번에» 쟀다 — ★**답은 «둘»이 아니었다**(rustjava-string-on-the-error-path-p0)
3+
- 무엇을: 채택 제안 `2026-09-19-string-on-the-error-path#p0`. 후보를 **유도**해(기록 로더로 정상 구성 1회 — 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ **37**) 하나씩 숨겨 재구성하는 **스윕**을 만들고 돌렸다.
4+
- ★★**결과 — 제안이 「still just these two」면 «기록된 음성»이라 했던 그 가정이 «반증»됐다**: ★**5개가 더 재귀한다**(바닥 없음) — `java/lang/Throwable` · `Error` · `LinkageError` · `CharSequence` · `Comparable`. ★**우연이 아니다** — 기존 두 이름의 **상위형·인터페이스 폐포**다(클래스를 resolve 하면 상위형도 resolve 되고, 거기 결손은 «아직 resolve 중인 그 두 클래스»로 보고된다).
5+
- ★**처방은 목록이 아니라 «폐포»다**: 손으로 적은 assert 2개 → **작업목록 루프 1개**(씨앗 2 · `interface_names()`·`super_class_name()` 을 밀어 넣는다 · ★로더에 **직접** 묻는다 — `resolve_class` 는 질문을 `Jvm::exception` 에 넘기고 그것이 곧 순환이다). ★**폐포 9개로 계산이 닫힌다**: 이미 막힌 **2** + 재귀 **5** + `bootstrap_classes` 가 먼저 잡는 **2**(`Object`·`Serializable`) ⇒ 설명 안 되는 이름 **0**.
6+
- ★**양방향**(제품 호출부 · 사본 아님): 정상 `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` 를 그대로 담는다).
7+
- ★**대가**: ⒜시작 비용 **로더 질문 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건).
8+
- 검증: DoD 9명령 rc 0.
9+
- ★후속 추천 **2건**: ⒜**부트스트랩 클래스의 이름 없는 unwrap 패닉**(S) ⒝**`[Ljava/lang/String;` 의 «상한이 못 끝내는» 두 번째 순환**(M). 상세 = `docs/worklog/2026-09-20-error-path-class-closure.{md,json}`.
10+
211
## [2026-09-20] 검사기 출력 순서를 «잠갔다» — 습관을 규칙으로 (rustjava-checker-output-determinism-has-no-guard)
312
- 무엇을: 채택 제안 `2026-09-19-merge-drops-deterministic-order#p0`. `scripts/check-script-output-order.py` 신설 — ★**`scripts/*.py` 의 어떤 `for`·컴프리헨션도 `sorted(...)` 밖에서 set 을 순회하지 않는다**를 **AST 로** 단언한다. CI 잡 `script_output_order` 1개 + DoD 10번째 줄.
413
- 왜: 전 회차가 한 단어로 고친 비결정성을 ★**아무도 잠그지 않았다**. ★**「그물 0」을 이 트리에서 재현했다** — 제품 호출부에 비결정성을 되돌려 놓고 재니 파이썬 검사기 **4종 전건 rc 0** · `cargo fmt` **rc 0**. ★해소 여부도 먼저 쟀다: `scripts/`·`rust.yml` 최종 커밋은 **`35f34797`**(그 수정 자신) · 추적 파일의 `PYTHONHASHSEED` 는 **산문과 주석뿐**이다.

‎STATE.md‎

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

99
## 완료
10+
- [rustjava-string-on-the-error-path-p0] ★★**오류 경로의 클래스 집합을 한 번에 쟀다 — 답은 «둘»이 아니었다.** 채택 제안 `2026-09-19-string-on-the-error-path#p0`.
11+
★**후보를 «유도»했다**(손 목록 아님): 기록 로더로 정상 구성 1회 → 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ 후보 **37**.
12+
★★**5개가 더 재귀한다**: `Throwable`·`Error`·`LinkageError`·`CharSequence`·`Comparable` = 기존 두 이름의 **상위형·인터페이스 폐포**.
13+
★**처방 = 폐포 walk**(assert 2 → 작업목록 1 · 로더에 **직접** 질의). ★**폐포 9로 계산이 닫힌다**(막힌 2 + 재귀 5 + bootstrap 2) ⇒ 미설명 **0**.
14+
★**양방향**: `0 recursed` ok ↔ `extend` 2줄 제거 → **5 recursed FAILED** ↔ 복원 ok. 기존 잠금 2건 **무수정 통과**.
15+
★**대가**: 로더 질문 **44→51** · 스윕 **~15초** · ★배열 5개 제외(로더가 **합성**하므로 클래스 집합이 못 빠뜨린다 — 단 `[Ljava/lang/String;` 는 상한 20에서도 오버플로 = in-process 불가).
16+
★**후속 2건**: 부트스트랩 unwrap 이 **이름을 말하지 않는다**(S) · `[Ljava/lang/String;` 의 두 번째 순환(M).
1017
- [rustjava-checker-output-determinism-has-no-guard] ★★**검사기 출력 순서를 잠갔다 — 습관을 규칙으로.** 채택 제안 `2026-09-19-merge-drops-deterministic-order#p0` · 신설 `scripts/check-script-output-order.py`(AST) + CI 잡 `script_output_order` + DoD 10번째 줄.
1118
★**불변식 한 줄**: `scripts/*.py` 의 어떤 `for`·컴프리헨션도 **`sorted(...)` 밖에서 set 을 순회하지 않는다**.
1219
★**「그물 0」 재현**: 비결정성을 되돌린 채 파이썬 검사기 **4종 rc 0** · `cargo fmt` **rc 0**. ★해소 여부 선행 확인 — `scripts/`·`rust.yml` 최종 커밋은 **`35f34797`**(그 수정 자신).
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"date": "2026-09-20",
3+
"taskId": "rustjava-string-on-the-error-path-p0",
4+
"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.",
5+
"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.",
6+
"measurements": {
7+
"cited_tree": "origin/main 5d732b13",
8+
"names_recorded_during_a_normal_construction": 51,
9+
"distinct_names": 42,
10+
"array_names_skipped": 5,
11+
"candidates_swept": 37,
12+
"before_fix": { "recursed": 5, "refused_by_name": 7, "failed_cleanly": 25, "built": 0 },
13+
"after_fix": { "recursed": 0, "refused_by_name": 12, "failed_cleanly": 25, "built": 0 },
14+
"recursing_names": ["java/lang/Throwable", "java/lang/Error", "java/lang/LinkageError", "java/lang/CharSequence", "java/lang/Comparable"],
15+
"closure_size": 9,
16+
"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"],
17+
"closure_account": "2 already checked + 5 recursing + 2 (Object, Serializable) already in bootstrap_classes, so the 9 are fully accounted for",
18+
"loader_questions_per_startup": "44 -> 51: the walk asks 9 where the two asserts it replaces asked 2",
19+
"sweep_runtime_s": "13.98 / 15.92 / 30.79 across runs on a loaded host",
20+
"give_up_after": 20
21+
},
22+
"verification": {
23+
"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",
24+
"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",
25+
"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.",
26+
"dod": "all 9 DoD commands rc 0"
27+
},
28+
"changes": [
29+
"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",
30+
"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",
31+
"jvm/tests/test_error_path_class_sweep.rs: the sweep, kept as the lock"
32+
],
33+
"issues": [
34+
"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.",
35+
"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.",
36+
"The sweep costs ~15s of `cargo test --all`. That is the price of the generalisation and it was not hidden."
37+
],
38+
"adoptedProposals": [
39+
"2026-09-19-string-on-the-error-path#p0"
40+
],
41+
"proposals": [
42+
{
43+
"title": "Say which bootstrap class is missing instead of unwrapping on None",
44+
"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.",
45+
"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.",
46+
"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.",
47+
"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.",
48+
"effort": "S",
49+
"target": "jvm/src/jvm.rs"
50+
},
51+
{
52+
"title": "Find out why hiding [Ljava/lang/String; overflows the stack at a cap of 20",
53+
"plainSummary": "One array class, when the loader refuses it, loops in a way that the test harness's escape hatch cannot stop.",
54+
"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.",
55+
"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'.",
56+
"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.",
57+
"effort": "M",
58+
"target": "jvm/src/jvm.rs, jvm/tests/test_error_path_class_sweep.rs"
59+
}
60+
]
61+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# 2026-09-20 — 오류 경로의 클래스 집합을 «한 번에» 쟀다 · 답은 «둘이 아니었다»
2+
3+
채택 제안 `2026-09-19-string-on-the-error-path#p0`.
4+
티켓 `rustjava-string-on-the-error-path-p0` · 인용 트리 `origin/main 5d732b13`.
5+
6+
## 제안이 물은 것
7+
8+
> 「This round and the one before it each closed exactly one class, and each found the next by
9+
> **reading the code and guessing** … A sweep that **hides each bootstrap-reachable name in turn**
10+
> and records which ones recurse would answer the whole question once.」
11+
> tradeoff — 「If it turns out the answer is **still just these two**, the result is a **recorded
12+
> negative** rather than a new check.」
13+
14+
## 답 — ★**「still just these two」가 아니다**
15+
16+
기록 로더로 정상 구성을 한 번 돌려 후보를 «유도»했다(손으로 적지 않았다): 요청 **51건** · 서로 다른 이름 **42** ·
17+
배열 이름 **5** 제외 ⇒ 후보 **37**. 그 37개를 하나씩 숨겨 재구성했다.
18+
19+
| 분류 | 고침 전 | 고침 후 |
20+
|---|---|---|
21+
| ★**재귀(바닥 없음)** | **5** | ★**0** |
22+
| 이름을 들어 거절 | 7 | **12** |
23+
| 한 번 묻고 깨끗이 실패 | 25 | 25 |
24+
| 없어도 구성됨 | 0 | 0 |
25+
26+
★**재귀한 5개** = `java/lang/Throwable` · `java/lang/Error` · `java/lang/LinkageError` ·
27+
`java/lang/CharSequence` · `java/lang/Comparable`.
28+
★**그 5개는 우연이 아니다** — 기존에 막아 둔 두 이름의 **상위형·인터페이스 폐포(closure)** 다.
29+
클래스를 resolve 하면 상위형도 resolve 되고, ★**거기서 난 결손은 «아직 resolve 중인 바로 그 두 클래스»로 보고된다.**
30+
31+
## 처방 — 목록이 아니라 «폐포»를 본다
32+
33+
`Jvm::new` 의 손으로 적은 assert 두 개를 **작업목록 루프 하나**로 바꿨다. 씨앗은 그 두 이름이고,
34+
얻은 정의의 `interface_names()`·`super_class_name()` 을 계속 밀어 넣는다. 로더에 **직접** 묻는다 —
35+
`resolve_class` 로 물으면 그 질문이 `Jvm::exception` 으로 가고, 그것이 곧 순환이다.
36+
37+
★**폐포는 9개이고 계산이 «닫힌다»**: `NoClassDefFoundError · LinkageError · Error · Throwable ·
38+
Object · Serializable · String · CharSequence · Comparable`
39+
= **이미 막혀 있던 2** + **재귀한 5** + **`bootstrap_classes` 가 먼저 잡는 2**(`Object`·`Serializable`).
40+
⇒ 설명되지 않는 이름이 **0** 이다.
41+
42+
## 양방향 — 제품 호출부에서(사본 아님)
43+
44+
| | 개악 | 결과 |
45+
|---|---|---|
46+
| **정상** | 없음 | `37 candidate(s): 0 recursed · 12 refused by name · 25 failed cleanly · 0 not needed` · **ok** |
47+
| **개악** | `jvm/src/jvm.rs` 의 `pending.extend(...)` 두 줄 제거(= 두 이름만 보던 고침 전 동작) | ★**`5 recursed` · FAILED** |
48+
| **복원** | 되돌림 | **0 recursed · ok** |
49+
50+
★기존 잠금 `jvm/tests/test_exception_fallback_recursion.rs` **2건은 손대지 않고 그대로 통과**한다 —
51+
새 패닉 문안이 `has no java/lang/String` · `has no java/lang/NoClassDefFoundError` 를 그대로 담기 때문이다.
52+
53+
## 대가 — 숨기지 않는다
54+
55+
- ★**시작 비용**: 로더 질문 **44 → 51**(폐포 9회 ↔ 종전 2회). 벽시계는 재지 «않았다» —
56+
전 회차가 같은 자리에서 재고 「이 호스트의 노이즈 아래」로 버렸다. **결정론적 수로만 말한다.**
57+
- ★**`cargo test --all` 에 스윕 ~15초**가 붙는다. 일반화의 값이고, 깎지 않았다.
58+
- ★**배열 이름 5개는 스윕에서 제외**했고 그 이유는 «의견이 아니라 측정»이다: 부트스트랩 로더가
59+
배열을 **합성**한다(`define_array_class`) ⇒ 어떤 클래스 집합도 배열을 «빠뜨릴» 수 없다.
60+
그래도 재 봤다 — `[C` 는 **재귀**(상한 20에 21질문) · ★**`[Ljava/lang/String;` 는 «그 상한에서도»
61+
스택 오버플로**다(= 그 순환은 로더로 돌아오지 않아 상한이 끝내지 못한다). ⇒ ★**이 스윕이 in-process 로
62+
돌릴 수 없는 유일한 후보**이고, 제안 `#p1` 로 남겼다.
63+
- ★**부트스트랩 6개를 숨기면 `called Option::unwrap() on a None value`** 로 죽는다 — 구성 시점에
64+
멈추니 옳지만 **이름을 말하지 않는다**. 12건 중 **5건**이 그것이다. 이 회차의 결함이 아니라 별 계급이라
65+
제안 `#p0` 으로 남겼다.
66+
67+
## 왜 지금인가
68+
69+
전 두 회차가 **각각 한 클래스씩** 닫았고 다음 이름을 **읽고 추측해** 찾았다. 그 속도로는 폐포 9개를 닫는 데
70+
회차 7번이 더 든다 — 그리고 ★**그 방식은 「이게 마지막인가」에 영원히 답하지 못한다.** 이번에 기계가 답했다.

0 commit comments

Comments
 (0)