Skip to content

Commit 76e47ff

Browse files
author
jun0
committed
[2026-09-18-nonliteral-exception-call-sites-p0-fix] merge origin/main — 원장 2파일 합집합(충돌 PR 은 pull_request CI 가 돌지 않는다)
형제 착지(#76 e9910a7 · #77 ad9eb1a)로 생긴 원장 재충돌. 코드 충돌 0. ★당긴 이유: 충돌 상태에서는 pull_request 워크플로가 «돌지 못해» 이 head 의 검사가 12건 → 1건으로 줄었다(coverage 만). ★그중 named_exception_classes 는 «내가 고친 바로 그 스크립트»를 돌리는 잡이라, 그대로 두면 CI 가 이 회차의 변경을 한 번도 검증하지 못한 채 검수로 간다. 보존: 소실 0/0 · 마커 0 · 해소면 밖 변경 0(검사기 바이트 동일).
2 parents 8dd8318 + ad9eb1a commit 76e47ff

5 files changed

Lines changed: 265 additions & 0 deletions

File tree

‎REPORT.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
- 검증: DoD 9명령 · 아래 절.
1717
- ★후속 추천: **그 사각이 «제품인지 테스트인지»를 말할 것인가**(S). 상세 = `docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md`.
1818

19+
## [2026-09-19] 적재 가능 집합을 «로더에서 읽을까» — ★**아니다, 재유도를 유지한다**(rustjava-loadable-set-from-loader-vs-rederive-decision)
20+
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p2`(worklog json 기록). ★**순수 결정 회차 — `.rs` 0줄 · `scripts/` 0줄.** 산출 = `docs/loadable-set-source-of-truth.md`(선례 = `docs/test-data-target-policy.md`).
21+
- ★★**전제부터 확인했다 — 「4결함 중 3이 재유도」는 «참»이다.** 산문이 아니라 **고침 커밋 `89c2e83c` 에서** 갈랐다: ⑴`as_proto` 전용 → `list_proto` 3건 누락 ⑵한 `impl` 의 첫 `name:` 오귀속 ⑶짧은 이름 키 충돌 = **재유도(loadable) 3건** · ⑷줄 단위 스캔이 rustfmt 가 쪼갠 34건 누락 = ★**호출부 스캔(named) 1건**.
22+
- ★★**그런데 그 4번째가 결정한다 — 로더에서 읽어도 «그 절반»은 못 없앤다.** `named`(코드가 `Jvm::exception` 에 «무엇을 넘기는가»)는 **로더가 답할 수 없다** — 소스를 읽는 것 말고는 알 길이 없다. ⇒ ★로더 읽기는 **파서 둘 중 하나를 없앨 뿐 파싱을 없애지 못한다**. 실제로 그 절반에서 **다음 결함이 «4시간 31분» 뒤에 또 났다**(`89c2e83c` 19:49 → `128e0fe5` 00:21 — ★「하루 만에」는 **캘린더 기준으로만** 참이다): ★**같은 앵커가 «다른 함수 8종»을 쓸어담는다** — `exception(` 이 `assert_exception(`·`suppress_io_exception(` 등의 부분문자열이라 **41자리**(첫 인자가 `jvm`)가 함께 걸리고, ★**세면 «33», 맞는 답은 «0»** 이다.
23+
- ★**대가 실측 둘**: ⒜★**검사기 전용 «생산 API» 가 필요하다** — `get_runtime_class_proto` 는 268 등재를 **함수 «안» 지역 배열**로 만들어 `.find(|p| p.name == name)` 로 쓴다. ★**열거 API 는 0개**(실측) ⇒ 런타임이 검사기를 위해 export 를 갖거나, 테스트가 목록을 다시 적어 **진실원이 다시 둘**이 된다. ⒝★**빌드 없는 검사에 빌드가 붙는다** — 현행 **0.75/0.96/0.77초** · CI 잡은 checkout + `python3` 두 줄이고 ★`rust.yml` **5잡 중 4잡이 그 형상**(툴체인 필요한 것은 `rust_ci` 하나뿐)이다.
24+
- ★★**결정적 실측 — 그 실패는 «공짜로» 잡힌다**: 재유도 버그는 «파스가 짧게 나온다»는 지문을 갖고, 불변식 하나(`등재 줄 수 == 파싱된 등재 == 해석된 이름 수`)가 그것을 본다. **초판 검사기(`38cbab7e`)를 지금 트리에서 돌리니 265 / 263 ↔ 등재 줄 268** ⇒ ★**결함 ⑴⑶ 이 1회차에 그 자리에서 잡혔을 값이다**(현행은 268/268/268 통과). ⇒ 독립성·0.8초·무빌드를 **하나도 내주지 않고** 얻는다.
25+
★**과장하지 않는다**: 이 불변식이 «모든 오귀속»을 잡지는 «않는다» — 틀리되 서로 다른 이름으로 매핑되면 268 을 유지한다. 잡는 것은 **과소계수 계급**이되 ★**loadable 쪽에서만**이다 — 세 항(등재 줄·파싱된 등재·해석된 이름)이 **전부 `loader.rs`+`classes/`** 에서 나와 ★**`named` 수를 아예 보지 않는다**. ⇒ 실측된 거짓 초록 «둘» 중 ★**하나만 잡는다**(결함 3 = 짧은 이름 키 충돌) · ★**결함 4는 못 잡는다**(과소계수가 `named` 쪽이다 — 812 ↔ 846). ★잡는 것은 **거짓 초록 1 + 거짓 빨강 1** 이다.
26+
- ★**브리프의 전제 1건이 이 repo 에선 «거짓»이다**(적어 둔다): 「이 repo 의 `machine-independence-guard` 가 그 축」 — ★**RustJava 엔 그런 가드가 없다**(`git ls-files | grep -i machine-independence` 빈 출력 · `rust.yml` 5잡 전수 확인). 그것은 **다른 repo 의 축**이다. ⇒ 여기서 잰 ⒜ 의 대가는 그것이 아니라 **툴체인·빌드 의존**이다.
27+
- ★**되돌릴 조건(사전 등록)**: 제안이 스스로 댄 계수 — 「재유도 결함이 몇 개 더 나오는가」. ★**오늘 값은 «고침 이후 0»이고 그것은 «하루»짜리 증거라 논거로 쓰지 않았다.** ⇒ ★**loadable 재유도 경로에서 «세 번째» 결함이 나오면 이 결정을 다시 연다**(`named` 스캔 결함은 세지 마라 — 로더 읽기가 고치는 자리가 아니다).
28+
- 검증: DoD 9명령 · 아래 절.
29+
- ★후속 추천: **재유도가 «짧게 나왔는지»를 단언할 것인가**(S · 위 불변식 — 이 회차가 값을 쟀고 짓지는 않았다). 상세 = `docs/worklog/2026-09-19-loadable-set-source-of-truth.md`.
1930
## [2026-09-19] 일으키려던 예외를 못 만들면 «죽었다» — 그 보고를 손에 쥔 채로(rustjava-jvm-exception-throws-instead-of-unwrap)
2031
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`(worklog json `adoptedProposals` 기록). `Jvm::exception` 의 `.unwrap()` 두 개를 **반환**으로 바꿨다. ★**시그니처 불변 · 새 enum variant 0 · 호출부 편집 0.**
2132
- ★★**급소 — 실패가 «이미» JavaError 다.** `from_rust_string`·`new_class` 는 `jvm::Result<T>` = `Result<T, JavaError>` 를 돌려주므로 그 실패는 **그 자체가 자바 예외**다. unwrap 은 그것을 버리고 프로세스를 죽였다. ⇒ **그대로 돌려준다.**

‎STATE.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
★**축**: 제품 코드 주입 → **1→2**(파일:줄 지목) · 원복 → 1 · rc 양쪽 0 · 술어 민감도 **42**.
1515
★**대가**: 찍힌 수는 무시할 수 있다 · 수는 술어만큼만 정확 · 제품/테스트 미구별(후속 카드).
1616
★**회귀 둘을 스스로 만들고 재서 걷어냈다**(두 번 걷기 · 개행 인덱스) ⇒ 최종 비용 **유의차 없음**.
17+
- [rustjava-loadable-set-from-loader-vs-rederive-decision] ★★**적재 가능 집합은 «재유도»를 유지한다 — 로더에서 읽지 않는다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p2`. ★**순수 결정 · 코드 0줄** · 산출 = `docs/loadable-set-source-of-truth.md`.
18+
★**전제 확인**: 「4중 3이 재유도」는 **참**(고침 커밋 `89c2e83c` 에서 갈랐다 — loadable 3 · named 1).
19+
★★**그 1건이 결정한다**: `named`(코드가 무엇을 넘기는가)는 **로더가 답할 수 없다** ⇒ 로더 읽기는 **파서 하나를 없앨 뿐 파싱을 못 없앤다**(그 절반에서 형제 회차가 ★**4시간 31분** 뒤에 또 잡았다 — ★**「다른 함수 8종 41자리」**를 쓸어담는 앵커 함정이고, 세면 **33** · 맞는 답은 **0** 이다).
20+
★**대가 실측**: 열거 API **0개**(268 등재가 함수 «안» 지역 배열) ⇒ **검사기 전용 생산 API** 가 필요 · 현행 **0.8초·무빌드**인데 `rust.yml` **5잡 중 4잡이 그 형상**이다.
21+
★★**결정적 실측**: 초판 검사기를 지금 트리에서 돌리면 **265/263 ↔ 등재 줄 268** ⇒ ★불변식 하나로 **결함 2건이 1회차에 잡혔을 값**(현행 268/268/268). ★단 «모든 오귀속»은 못 잡는다(과소계수 계급만).
22+
★**브리프 전제 1건이 거짓**: 이 repo 엔 `machine-independence-guard` 가 **없다**(다른 repo 축).
23+
★**재개 조건 사전 등록**: loadable 재유도 경로에서 **세 번째** 결함이 나오면 다시 연다(「고침 이후 0」은 하루짜리라 논거로 쓰지 않았다).
1724
- [rustjava-jvm-exception-throws-instead-of-unwrap] ★★**일으키려던 예외를 못 만들면 죽던 것을 «보고»로 바꿨다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`. ★시그니처 불변 · variant 0 · 호출부 편집 0.
1825
★**급소**: `from_rust_string`·`new_class` 의 실패는 **이미 `JavaError`**(= 자바 예외)다 — unwrap 이 그것을 버렸다. ⇒ 그대로 돌려준다.
1926
★**실측**: `panicked … unwrap() on an Err value: JavaException(java/lang/NoClassDefFoundError)` — ★올바른 보고가 **패닉 메시지 안에** 실려 사라졌다.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Should the loadable set be read from the loader instead of re-derived?
2+
3+
**Decision: no. `check-named-exception-classes-are-loadable.py` keeps parsing the Rust source.**
4+
The proposal's premise is correct — three of the four defects really were in the re-derivation — but
5+
reading from the loader buys less than it looks and costs a production API plus a build dependency,
6+
and the failure mode it aims at is detectable for nothing without giving either up.
7+
8+
Adopted proposal: `2026-09-18-named-exception-classes-are-loadable#p2`. Measured 2026-09-19 against
9+
`origin/main` @ `ddc6c4ce`.
10+
11+
## First: is "three of four" true?
12+
13+
It is. Read from the fix commit `89c2e83c`, not from the prose about it — four distinct defects, and
14+
which half of the checker each lived in:
15+
16+
| # | defect | fixed by | half |
17+
|---|---|---|---|
18+
| 1 | only `as_proto()` matched, so three `list_proto()` registrations read as absent | `REGISTERED` regex gains `(?:as\|list)_proto` | **loadable (re-derivation)** |
19+
| 2 | first `name:` in an `impl` block attributed the wrong class when a type holds two proto constructors | `IMPL_BLOCK` → `IMPL_START` + `PROTO_FN`, per function | **loadable (re-derivation)** |
20+
| 3 | bare type names as keys, so one of a colliding pair answered for its twin | key becomes `(module, type, function)` | **loadable (re-derivation)** |
21+
| 4 | line-by-line scan missed 34 calls rustfmt had broken across a newline | `NAMED.finditer(line)` → `finditer(text)` | **named (call-site scan)** |
22+
23+
So 3/4, as claimed. What the claim does *not* say, and what decides this: **the fourth is in the half
24+
that reading the loader cannot remove.**
25+
26+
## Why reading from the loader buys less than it looks
27+
28+
The check compares two sets. Reading the loader replaces one of them:
29+
30+
- `loadable` — which classes the runtime can resolve. This *could* come from the runtime.
31+
- `named` — which class names the Rust code passes to `Jvm::exception`. This **cannot**. There is no
32+
way to learn it except by reading the source, short of executing all 846 call sites.
33+
34+
Defect 4 was in `named`, and that half keeps every hazard that made it: 846 call sites, rustfmt
35+
splitting calls across lines, and — found **4 h 31 min later** by
36+
`2026-09-18-nonliteral-exception-call-sites` (`89c2e83c` 19:49 → `128e0fe5` 00:21, which is "the next
37+
day" only by the calendar) — the same anchor matching **eight other function names**: `exception(` is
38+
a substring of `assert_exception(`, `suppress_io_exception(` and six more, **41 sites** whose first
39+
argument is `jvm` rather than a class name. Counting them would have answered **33** instead of the
40+
correct **0**. Reading the loader removes three defects' worth of parsing and leaves the parser.
41+
42+
## What it would cost
43+
44+
**A production API that exists only for the check.** `get_runtime_class_proto` builds its 268
45+
registrations as a **local array inside the function** and consumes it with
46+
`protos.into_iter().find(|proto| proto.name == name)`. There is no enumeration — measured: zero
47+
public functions returning the proto list. So emitting the names means either exporting that array
48+
from `rustjava-runtime`, or writing a test that restates the list, which re-creates the two-sources
49+
problem the proposal is trying to remove.
50+
51+
**A build dependency on a check that has none.** Measured: the checker runs in **0.75–0.96 s** on
52+
nothing but source text. Its own job, `named_exception_classes`, is five lines — checkout, `python3
53+
script`. Four of the five jobs in `rust.yml` need **no toolchain** (`worklog_json`, `merge_drops`,
54+
`dod_parity`, `named_exception_classes`); only `rust_ci` does. (Line counts differ among those four —
55+
`merge_drops` is seven, carrying `fetch-depth: 0` — which is why the shared property named here is the
56+
toolchain, not the length.) Reading from the loader moves this
57+
check across that line, in CI and in the local DoD both.
58+
59+
The proposal names the drift risk itself: a generated list goes stale when the emitter is not re-run.
60+
This repo already runs that pattern once — `test-data/class-file-versions.txt` with
61+
`record-class-file-versions.py` — and `AGENTS.md` has to spell out that adding a fixture means
62+
editing the freeze file *in the same commit*, because otherwise it silently rots.
63+
64+
## The deciding measurement: the failure mode is cheap to catch anyway
65+
66+
The proposal's fear is that a re-derivation bug produces a false green. That bug has a signature —
67+
the parse comes out *short* — and one invariant sees it:
68+
69+
```
70+
registration lines in loader.rs == registrations the checker parsed == distinct names it resolved
71+
```
72+
73+
Run against both versions of the checker on today's tree:
74+
75+
| checker | parsed registrations | distinct names | vs 268 registration lines |
76+
|---|---|---|---|
77+
| first draft (`38cbab7e`) | **265** | **263** | **breaks — caught on the spot** |
78+
| current | 268 | 268 | passes |
79+
80+
So defects 1 and 3 would have been caught in the first round, by a check that keeps the
81+
independence, the 0.8 s, and the zero build steps. That is not built here — the adopted proposal
82+
asked for a decision, not a third mechanism — and is filed as a follow-up.
83+
84+
*Not claimed*: that this invariant catches every mis-attribution. A registration mapped to a wrong
85+
but still distinct name can keep the count at 268. And it catches an undercount only on the
86+
**loadable** side: all three of its terms — registration lines, parsed registrations, resolved names —
87+
are read from `loader.rs` and `classes/`, so it never sees the `named` count at all. Of the two
88+
measured false greens it therefore catches **one** (defect 3, the colliding bare-name key) and misses
89+
defect 4, whose undercount was on the `named` side (812 against 846). What it does catch is one false
90+
green and one false red.
91+
92+
## What would reopen this
93+
94+
The proposal states the deciding count itself: *"how many more re-derivation defects show up — if
95+
this round was the last one, the parsing version is cheaper."* Today that count is **0 since the fix
96+
landed**, and that number is worth exactly what one day of evidence is worth — which is why it is not
97+
the argument above.
98+
99+
**Re-measure at the third defect.** If a third defect is found in the loadable re-derivation (the
100+
`REGISTERED` / `PROTO_FN` / keying path — not the `named` scan, which reading the loader does not
101+
fix), this decision reopens and the emitter becomes the cheaper option. Count them the same way this
102+
document did: from the fix commits, classified by which half they lived in.
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-loadable-set-from-loader-vs-rederive-decision",
4+
"summary": "Decision round, no code changed. Keep re-deriving the loadable set from Rust source rather than reading it from the loader. The proposal's premise checks out - 3 of the 4 defects were in the re-derivation - but reading the loader removes one of the two parsers and not the parsing, needs a production API that only the check would use, and puts a build under a check that costs 0.8s with none. The failure mode it targets is catchable by a single invariant that would have broken in round 1.",
5+
"decision": "keep re-derivation; do not read the loadable set from the loader",
6+
"measurements": {
7+
"defects_total": 4,
8+
"defects_in_loadable_rederivation": 3,
9+
"defects_in_named_scan": 1,
10+
"loader_registration_lines": 268,
11+
"prefix_checker_parsed_registrations": 265,
12+
"prefix_checker_distinct_names": 263,
13+
"current_checker_parsed_registrations": 268,
14+
"current_checker_distinct_names": 268,
15+
"checker_runtime_seconds": [0.96, 0.75, 0.77],
16+
"public_enumeration_apis_for_protos": 0,
17+
"ci_jobs_total": 5,
18+
"ci_jobs_needing_toolchain": 1,
19+
"rederivation_defects_since_the_fix": 0,
20+
"days_of_that_evidence": 1
21+
},
22+
"verification": [
23+
"the 3-of-4 claim was checked against fix commit 89c2e83c itself, not the prose: REGISTERED as_proto->(as|list)_proto, IMPL_BLOCK->IMPL_START+PROTO_FN, bare-name key->(module,type,function) are loadable-side; NAMED.finditer(line)->finditer(text) is named-side",
24+
"invariant test: ran the pre-fix checker (38cbab7e) against today's tree - parsed 265 registrations, resolved 263 names, against 268 registration lines in loader.rs; current checker gives 268/268/268",
25+
"no enumeration API: get_runtime_class_proto holds the 268 protos in a local array consumed by .find(|proto| proto.name == name); grep for public functions returning the proto list returns nothing",
26+
"checker cost measured 3x with /usr/bin/time; CI job shapes read from rust.yml (4 of 5 jobs are checkout + python3, only rust_ci needs a toolchain)"
27+
],
28+
"changes": [
29+
"docs/loadable-set-source-of-truth.md - the decision, its measurements and the re-measure trigger",
30+
"docs/worklog/2026-09-19-loadable-set-source-of-truth.{md,json}, REPORT.md, STATE.md",
31+
"no .rs and no scripts/ changes"
32+
],
33+
"issues": [
34+
"The brief's machine-independence premise does not hold for this repo: RustJava has no machine-independence-guard (git ls-files grep is empty; rust.yml has five jobs). That axis belongs to another repo, so the cost weighed here is the toolchain/build dependency instead.",
35+
"The '0 re-derivation defects since the fix' count is one day old and is deliberately not used as the argument; the re-measure trigger is stated instead.",
36+
"The invariant that would have caught defects 1 and 3 is not claimed to catch every mis-attribution: a registration mapped to a wrong but distinct name keeps the count at 268."
37+
],
38+
"adoptedProposals": [
39+
"2026-09-18-named-exception-classes-are-loadable#p2"
40+
],
41+
"proposals": [
42+
{
43+
"title": "Assert the loadable re-derivation did not come up short, instead of trusting it",
44+
"plainSummary": "The safety check works out which classes the runtime can load by reading the source. When that reading quietly misses some, the check still says everything is fine - which is the one thing it must never do wrongly.",
45+
"userBenefit": "A silent miscount in the check would go back to being visible immediately rather than waiting for someone to read the script alongside the source.",
46+
"why": "Measured in this round: the first draft of the checker parsed 265 registrations and resolved 263 names against 268 registration lines in loader.rs. One invariant - registration lines == parsed registrations == distinct names - breaks on both of those, so two of the three loadable-side defects would have been caught in round 1 without giving up source independence, the 0.8s runtime, or the build-free CI job. This round decided against reading the loader; this is the cheap half of what that would have bought.",
47+
"tradeoff": "It adds a third number the checker has to keep true, and it is a floor rather than a proof: a registration mapped to a wrong but still distinct name keeps the count at 268 and passes. It also couples the check to the textual shape of loader.rs (one registration per line), which is true today and is not guaranteed.",
48+
"effort": "S",
49+
"target": "scripts/check-named-exception-classes-are-loadable.py"
50+
}
51+
]
52+
}

0 commit comments

Comments
 (0)