Skip to content

Commit 1929749

Browse files
jun0claude
andcommitted
[2026-09-18-nonliteral-exception-call-sites-p0-fix] merge origin/main — 승인된 원장 2파일 합집합
게이트③ 2-c 재충돌(형제 착지 19커밋). 코드 충돌 0 — 충돌은 REPORT.md·STATE.md 둘뿐이고 검수 회신 머리 `해소승인:` 줄이 그 둘을 이름으로 미리 승인했다. 해소 = 양측 엔트리 전건 보존 합집합 · 날짜 내림차순 배치 · 소실 ours=0 theirs=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 parents 76e47ff + 8c74bba commit 1929749

24 files changed

Lines changed: 1577 additions & 2 deletions

‎.github/workflows/rust.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ jobs:
8181
- uses: actions/checkout@v7
8282
- run: python3 scripts/check-dod-ci-parity.py
8383

84+
# a checker that iterates a set prints its findings in a different order on different runs, so two
85+
# rounds cannot diff their output — and every other axis stays green while it does. This reads the
86+
# checkers' AST rather than re-running them. One runner, not the matrix.
87+
script_output_order:
88+
runs-on: ubuntu-latest
89+
steps:
90+
- uses: actions/checkout@v7
91+
- run: python3 scripts/check-script-output-order.py
92+
8493
# Jvm::exception unwraps new_class(), so naming a class the loader cannot resolve panics instead
8594
# of throwing. This compares the names against the registered protos (see the script's docstring
8695
# for what it cannot see). One runner, not the matrix.

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
python3 scripts/check-dod-ci-parity.py
3434
python3 scripts/check-named-exception-classes-are-loadable.py
3535
python3 scripts/check-merge-dropped-symbols.py
36+
python3 scripts/check-script-output-order.py
3637
```
3738
★★**이 블록은 이제 «기계가 지킨다» — `scripts/check-dod-ci-parity.py`(CI job `dod_parity`)가
3839
이 코드블록과 `rust.yml` 을 «각각 파싱해» 대칭차를 낸다.** 어긋나면 그 자리에서 red 다.

‎REPORT.md‎

Lines changed: 77 additions & 0 deletions
Large diffs are not rendered by default.

‎STATE.md‎

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

99
## 완료
10+
- [rustjava-error-path-string-array-hiding-overflows-stack-p1] ★★**`[Ljava/lang/String;` 오버플로의 원인 — 지난 회차 기재가 «틀렸다».** 채택 제안 `2026-09-20-error-path-class-closure#p1` · ★**조사 회차 · 제품 코드 0줄**.
11+
★**재현**: cap 20 · 기본 스택 → `stack overflow, aborting` rc **134**.
12+
★★**반증**: 「로더로 안 돌아와 cap 이 못 끝낸다」는 **거짓** — 살아남은 전 실행에서 `asked = cap+1`(= `[C` 와 같은 모양). 원인은 ★**턴당 비용**.
13+
★**두 손잡이가 따로 움직인다**(가설 분리): cap **18↔19** · 스택 **2 MiB↔4 MiB**(cap 20 고정).
14+
★**순환을 백트레이스로**: `fillInStackTrace`→`instantiate_array`→`resolve_class`→`load_class`(None)→`exception`→`new_class`→`invoke_special`×4→`Throwable.<init>`→`fillInStackTrace` · ★**한 턴 ≈ 57 프레임**.
15+
★**대조**: `[C` 만 같이 돈다 · `[I`·`[Ljava/lang/Object;`·`java/lang/Integer` 등은 **asked=0** ⇒ **「배열이면 난다」가 아니다**.
16+
★**고침 = 스윕 헤더의 거짓 문장 하나** · 배열 제외는 유지(이유가 다르고 여전히 유효) · **개악 양방향 해당 없음**(동작 무변경).
17+
★**남긴 것**: 순환은 제품에 **바닥이 없다**(오늘 닿을 수 없을 뿐) ⇒ 후속 카드 「`Jvm::exception` 에 바닥을」(M).
18+
- [rustjava-error-path-name-the-missing-bootstrap-class-p0] ★★**없는 부트스트랩 클래스의 «이름»을 말하게 했다.** 채택 제안 `2026-09-20-error-path-class-closure#p0`.
19+
★`Jvm::new` 의 `bootstrap_classes` 루프 `.unwrap()` → `let … else` · 문면 = 이름 + 여섯 중 하나 + ★**어디에 물었는지**(호스트 로더이지 `java.class.path` 가 **아니다**).
20+
★**가족을 세고 골랐다**: 이 축 **1곳 고침** ↔ 같은 «모양»의 레지스트리 조회 **4곳**은 다른 축(내부 불변식)이고 스윕이 **닿지 않음을 실측**(익명 거절 0/37) ⇒ **blind spot 으로 적고 두었다**.
21+
★★**스윕이 자기 눈으로 보고도 좋은 쪽에 세고 있었다** — `Panicked` 가 문면을 안 읽었다. 이제 payload 를 숨긴 이름과 대조하고 `PanickedAnonymously` 는 **그 자체가 실패**.
22+
★**양방향(두 사실을 «따로»)**: 개악 → 이름 적중 **0/5** · `7 by name · 5 anonymous` **FAILED** ↔ 고침 → **5/5** · `12 · 0` **ok**. ★**rc 만 보면 둘이 같다**(양쪽 패닉).
23+
★**대가**: 런타임 **0**. 패닉→오류 반환은 **하지 않았다**(`JavaError` 변종 1개 · 없는 것이 바로 예외를 만들 클래스 · 사내 3곳 + 공개 enum 파괴) ⇒ 제안 카드.
24+
- [rustjava-string-on-the-error-path-p0] ★★**오류 경로의 클래스 집합을 한 번에 쟀다 — 답은 «둘»이 아니었다.** 채택 제안 `2026-09-19-string-on-the-error-path#p0`.
25+
★**후보를 «유도»했다**(손 목록 아님): 기록 로더로 정상 구성 1회 → 요청 **51** · 서로 다른 이름 **42** · 배열 **5** 제외 ⇒ 후보 **37**.
26+
★★**5개가 더 재귀한다**: `Throwable`·`Error`·`LinkageError`·`CharSequence`·`Comparable` = 기존 두 이름의 **상위형·인터페이스 폐포**.
27+
★**처방 = 폐포 walk**(assert 2 → 작업목록 1 · 로더에 **직접** 질의). ★**폐포 9로 계산이 닫힌다**(막힌 2 + 재귀 5 + bootstrap 2) ⇒ 미설명 **0**.
28+
★**양방향**: `0 recursed` ok ↔ `extend` 2줄 제거 → **5 recursed FAILED** ↔ 복원 ok. 기존 잠금 2건 **무수정 통과**.
29+
★**대가**: 로더 질문 **44→51** · 스윕 **~15초** · ★배열 5개 제외(로더가 **합성**하므로 클래스 집합이 못 빠뜨린다 — 단 `[Ljava/lang/String;` 는 상한 20에서도 오버플로 = in-process 불가).
30+
★**후속 2건**: 부트스트랩 unwrap 이 **이름을 말하지 않는다**(S) · `[Ljava/lang/String;` 의 두 번째 순환(M).
31+
- [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번째 줄.
32+
★**불변식 한 줄**: `scripts/*.py` 의 어떤 `for`·컴프리헨션도 **`sorted(...)` 밖에서 set 을 순회하지 않는다**.
33+
★**「그물 0」 재현**: 비결정성을 되돌린 채 파이썬 검사기 **4종 rc 0** · `cargo fmt` **rc 0**. ★해소 여부 선행 확인 — `scripts/`·`rust.yml` 최종 커밋은 **`35f34797`**(그 수정 자신).
34+
★**정적을 고른 이유**: 두 `PYTHONHASHSEED` 재실행은 ★**회차당 약 절반 눈을 감고**(해시 순서 = 정렬 순서면 무증상), `assert sorted` 는 **다른 곳의 새 출처를 못 잡는다**(제안 자신의 약점 기술).
35+
★**양방향 2×2**(제품 호출부): M1 `check-merge-dropped-symbols.py:254` 제거 → rc 1 ↔ 복원 rc 0 · M2 ★**다른 파일** `check-dod-ci-parity.py:215` → rc 1 ↔ 복원 rc 0. 정상 `7 script(s): 0` · 0.06초.
36+
★**측정된 사각**: 튜플 언패킹 반환 set 은 못 본다 — `ci_runs, ci_tcs = parse_ci(...)` 에 정렬 없는 순회를 넣으니 ★**rc 0 통과**. 후속 제안 `#p0`.
37+
★★**게이트² 반려 승계(`-fix`)**: **R1** `", ".join(myset)`·`print(*myset)` 이 통과했다(사고와 **같은 계급** · 미기재) ⇒ `str.join` 첫 인자 + `Starred`(Load) 를 검사에 더하고 약속 문구를 ★**«for/컴프리헨션 · str.join · \*-언팩» 세 위치로 명시** ·
38+
**R2** 「set→dict 는 set 에서 잡힌다」가 **거짓**(`dict.fromkeys`)이라 **삭제**. ★`fromkeys` 만 반쪽으로 잡지 «않았다» — 진짜 계급은 «컨테이너 순서 오염»이고 하나만 잡으면 **없는 프로그램을 있는 것처럼 보이게 한다**(오늘 0건 ⇒ 문안 결함) ·
39+
**R3** ★**python 린터·포매터·테스트 0**(추적 파일 0건 · `rust.yml` 은 검사기 5회 실행뿐) ⇒ **이 파일을 보는 기계는 CI 잡 «하나»**임을 빚으로 적었다(하네스는 만들지 «않았다»).
40+
★승계 양방향: `join`·`*` 반례 **rc 1** ↔ `sorted()` 씌우면 **무검출**(오탐 0) ↔ 반례 제거 **rc 0** · 원 M1·M2 **회귀 재확인**.
41+
- [rustjava-error-path-needs-java-lang-string-measure-first] ★★**오류 경로의 또 하나 `java/lang/String` — ⒜ «재귀한다»로 확정하고 선재 확인을 넣었다.** 채택 제안 `2026-09-19-fallback-class-absence-fails-at-construction#p0`. ★제안의 조건이 「측정이 먼저」였고 그대로 했다.
42+
★**실측**: 상한 **116 생존 ↔ 117 SIGABRT «stack overflow, aborting»**(양쪽 2회 재현) · ★**상한 100000 도 abort** ⇒ 바닥이 없다.
43+
★**⒞ 아님을 구조로**: `bootstrap_classes` **6개에 String 없음** · `from_rust_class` 는 이름을 **`[B`(nameBytes)** 로 넣는다 ⇒ 처음 필요한 곳은 프로퍼티 루프.
44+
★**자리**: 프로퍼티 루프 **앞**(기존 `NoClassDefFoundError` 확인은 그 **뒤**라 String 형상엔 **늦다**) · 관용은 동일(로더에 **직접** 질의 후 resolve — bare `resolve_class` 는 순환 자신에게 넘긴다).
45+
★**양방향**(제품 호출부): green ↔ 제거 시 ★**바이너리째 SIGABRT** ↔ 복원 green.
46+
★**시작 비용**: 로더 질의 **1 → 2회**(결정론 계수). ★**벽시계는 버렸다** — 형제 레인 부하로 같은 형상 p50 이 69ms~1690ms 고 교대 8회 중 3회는 확인 있는 쪽이 더 빨랐다 ⇒ 수를 주장하지 않는다.
47+
★**미측정**: 두 클래스의 생성자·정적 초기화가 닿는 나머지(후속 카드).
48+
- [2026-09-19-partial-clone-blob-vs-absence-p0] ★★**`check-merge-dropped-symbols.py` 의 출력 순서를 고정했다 — 두 회차를 diff 할 수 있다.** 채택 제안 `2026-09-19-partial-clone-blob-vs-absence#p0`. ★**코드 1줄**(+주석 8줄) · `.rs` 0줄.
49+
★**재현**: `origin/main` 판본 · `PYTHONHASHSEED=random` **10회** → 순서 **2종**(같은 6건) ⇒ 없는 차이가 diff 에 보였다.
50+
★★**제안 진단은 부정확**: `findings` 는 set 이 아니라 **list** 이고 이름은 이미 정렬돼 있었다 — set 4개 중 출력에 닿는 것은 ★**`changed`(경로) 하나**다. ⇒ 「print site」가 아니라 ★**출처에서** 정렬했다(`check()` 의 **반환값**도 결정적이어야 하므로).
51+
★**양방향**: 2종 ↔ **1종** ↔ 되돌리면 2종. ★찾은 것 불변(15줄 · 집합 일치 · rc 1).
52+
★**대가**: 아무도 잠그지 않는다 — `scripts/` 테스트 하네스 **0** · 비결정성을 넣어도 파이썬 검사기 **4종 rc 0** · fmt **rc 0** ⇒ ★**그물 «0»**. 후속 제안으로 남겼다.
1053
- [2026-09-18-nonliteral-exception-call-sites-p0] ★★**비리터럴 사각은 «관문»이 아니라 «보고»다 — 제안의 전제 둘 다 소멸.** 채택 제안 `2026-09-18-nonliteral-exception-call-sites#p0`.
1154
★**전제 재측**: 「baseline 0」 → ★**1**(그 1자리는 **정상**이고 «비리터럴이어야만» 한다 — 리터럴이면 이 검사기가 red) · 「죽는다」 → ★**더는 안 죽는다**(#76 착지) ⇒ 해악이 «죽음»에서 «틀린 catch»로 내려갔다.
1255
★**관문을 0으로 걸었으면 제안된 날 main 이 red** 였다 — ★제안이 자기 `why` 에 그 비용을 예고했고 **4시간 뒤** 현실이 됐다.
@@ -21,6 +64,12 @@
2164
★★**결정적 실측**: 초판 검사기를 지금 트리에서 돌리면 **265/263 ↔ 등재 줄 268** ⇒ ★불변식 하나로 **결함 2건이 1회차에 잡혔을 값**(현행 268/268/268). ★단 «모든 오귀속»은 못 잡는다(과소계수 계급만).
2265
★**브리프 전제 1건이 거짓**: 이 repo 엔 `machine-independence-guard` 가 **없다**(다른 repo 축).
2366
★**재개 조건 사전 등록**: loadable 재유도 경로에서 **세 번째** 결함이 나오면 다시 연다(「고침 이후 0」은 하루짜리라 논거로 쓰지 않았다).
67+
- [2026-09-19-exception-reports-instead-of-aborting-p0] ★★**보고자의 부재는 보고될 수 없다 — 로더에 직접 묻고 «이름을 들어» 실패한다.** 채택 제안 `2026-09-19-exception-reports-instead-of-aborting#p0`.
68+
★**이 회차가 그 순환을 «처음 실측»했다**(제안 회차는 「실측 아님」이라 적었다): 숨김 로더로 **6/21/61/121 왕복** 후 완료 · 상한 160·200 → ★**SIGABRT 스택 오버플로** ⇒ 바닥 없음 · **121~160 사이**에서 죽는다.
69+
★★**측정이 설계를 두 번 기각했다**: ⑴부트스트랩 목록 추가 → **6 테스트 즉사**(해석이 «초기화»를 돌려 스레드가 필요) ⑵시스템 로더 뒤 `resolve_class` → 정상 6/6 인데 ★**숨김 로더에선 여전히 스택 오버플로**(시점만 이동).
70+
★**처방**: 예외 기구를 **우회**해 로더에 직접 묻는다 — 없으면 생성 시점에 이름을 들어 실패 · 그 뒤 등재해 재질의 0.
71+
★**축**: 전 **바이너리째 SIGABRT** ↔ 후 **`should_panic` 통과** · 정상 기동 **6/6 불변**.
72+
★**대가**: ★패닉이고 `AGENTS.md` 와 충돌한다(대안 둘은 각각 «불가능»·«460자리 무언 변경»이라 기각 · `Jvm::new` 는 이미 unwrap 한다) · 필요보다 일찍 실패 · **이 순환만** 막는다(`String` 은 미측정 · 후속 카드).
2473
- [rustjava-jvm-exception-throws-instead-of-unwrap] ★★**일으키려던 예외를 못 만들면 죽던 것을 «보고»로 바꿨다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`. ★시그니처 불변 · variant 0 · 호출부 편집 0.
2574
★**급소**: `from_rust_string`·`new_class` 의 실패는 **이미 `JavaError`**(= 자바 예외)다 — unwrap 이 그것을 버렸다. ⇒ 그대로 돌려준다.
2675
★**실측**: `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+
}

0 commit comments

Comments
 (0)