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

**Decision: no. `check-named-exception-classes-are-loadable.py` keeps parsing the Rust source.**
The proposal's premise is correct — three of the four defects really were in the re-derivation — but
reading from the loader buys less than it looks and costs a production API plus a build dependency,
and the failure mode it aims at is detectable for nothing without giving either up.

Adopted proposal: `2026-09-18-named-exception-classes-are-loadable#p2`. Measured 2026-09-19 against
`origin/main` @ `ddc6c4ce`.

## First: is "three of four" true?

It is. Read from the fix commit `89c2e83c`, not from the prose about it — four distinct defects, and
which half of the checker each lived in:

| # | defect | fixed by | half |
|---|---|---|---|
| 1 | only `as_proto()` matched, so three `list_proto()` registrations read as absent | `REGISTERED` regex gains `(?:as\|list)_proto` | **loadable (re-derivation)** |
| 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)** |
| 3 | bare type names as keys, so one of a colliding pair answered for its twin | key becomes `(module, type, function)` | **loadable (re-derivation)** |
| 4 | line-by-line scan missed 34 calls rustfmt had broken across a newline | `NAMED.finditer(line)` → `finditer(text)` | **named (call-site scan)** |

So 3/4, as claimed. What the claim does *not* say, and what decides this: **the fourth is in the half
that reading the loader cannot remove.**

## Why reading from the loader buys less than it looks

The check compares two sets. Reading the loader replaces one of them:

- `loadable` — which classes the runtime can resolve. This *could* come from the runtime.
- `named` — which class names the Rust code passes to `Jvm::exception`. This **cannot**. There is no
way to learn it except by reading the source, short of executing all 846 call sites.

Defect 4 was in `named`, and that half keeps every hazard that made it: 846 call sites, rustfmt
splitting calls across lines, and — found **4 h 31 min later** by
`2026-09-18-nonliteral-exception-call-sites` (`89c2e83c` 19:49 → `128e0fe5` 00:21, which is "the next
day" only by the calendar) — the same anchor matching **eight other function names**: `exception(` is
a substring of `assert_exception(`, `suppress_io_exception(` and six more, **41 sites** whose first
argument is `jvm` rather than a class name. Counting them would have answered **33** instead of the
correct **0**. Reading the loader removes three defects' worth of parsing and leaves the parser.

## What it would cost

**A production API that exists only for the check.** `get_runtime_class_proto` builds its 268
registrations as a **local array inside the function** and consumes it with
`protos.into_iter().find(|proto| proto.name == name)`. There is no enumeration — measured: zero
public functions returning the proto list. So emitting the names means either exporting that array
from `rustjava-runtime`, or writing a test that restates the list, which re-creates the two-sources
problem the proposal is trying to remove.

**A build dependency on a check that has none.** Measured: the checker runs in **0.75–0.96 s** on
nothing but source text. Its own job, `named_exception_classes`, is five lines — checkout, `python3
script`. Four of the five jobs in `rust.yml` need **no toolchain** (`worklog_json`, `merge_drops`,
`dod_parity`, `named_exception_classes`); only `rust_ci` does. (Line counts differ among those four —
`merge_drops` is seven, carrying `fetch-depth: 0` — which is why the shared property named here is the
toolchain, not the length.) Reading from the loader moves this
check across that line, in CI and in the local DoD both.

The proposal names the drift risk itself: a generated list goes stale when the emitter is not re-run.
This repo already runs that pattern once — `test-data/class-file-versions.txt` with
`record-class-file-versions.py` — and `AGENTS.md` has to spell out that adding a fixture means
editing the freeze file *in the same commit*, because otherwise it silently rots.

## The deciding measurement: the failure mode is cheap to catch anyway

The proposal's fear is that a re-derivation bug produces a false green. That bug has a signature —
the parse comes out *short* — and one invariant sees it:

```
registration lines in loader.rs == registrations the checker parsed == distinct names it resolved
```

Run against both versions of the checker on today's tree:

| checker | parsed registrations | distinct names | vs 268 registration lines |
|---|---|---|---|
| first draft (`38cbab7e`) | **265** | **263** | **breaks — caught on the spot** |
| current | 268 | 268 | passes |

So defects 1 and 3 would have been caught in the first round, by a check that keeps the
independence, the 0.8 s, and the zero build steps. That is not built here — the adopted proposal
asked for a decision, not a third mechanism — and is filed as a follow-up.

*Not claimed*: that this invariant catches every mis-attribution. A registration mapped to a wrong
but still distinct name can keep the count at 268. And it catches an undercount only on the
**loadable** side: all three of its terms — registration lines, parsed registrations, resolved names —
are read from `loader.rs` and `classes/`, so it never sees the `named` count at all. Of the two
measured false greens it therefore catches **one** (defect 3, the colliding bare-name key) and misses
defect 4, whose undercount was on the `named` side (812 against 846). What it does catch is one false
green and one false red.

## What would reopen this

The proposal states the deciding count itself: *"how many more re-derivation defects show up — if
this round was the last one, the parsing version is cheaper."* Today that count is **0 since the fix
landed**, and that number is worth exactly what one day of evidence is worth — which is why it is not
the argument above.

**Re-measure at the third defect.** If a third defect is found in the loadable re-derivation (the
`REGISTERED` / `PROTO_FN` / keying path — not the `named` scan, which reading the loader does not
fix), this decision reopens and the emitter becomes the cheaper option. Count them the same way this
document did: from the fix commits, classified by which half they lived in.
52 changes: 52 additions & 0 deletions docs/worklog/2026-09-19-loadable-set-source-of-truth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"date": "2026-09-19",
"taskId": "rustjava-loadable-set-from-loader-vs-rederive-decision",
"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.",
"decision": "keep re-derivation; do not read the loadable set from the loader",
"measurements": {
"defects_total": 4,
"defects_in_loadable_rederivation": 3,
"defects_in_named_scan": 1,
"loader_registration_lines": 268,
"prefix_checker_parsed_registrations": 265,
"prefix_checker_distinct_names": 263,
"current_checker_parsed_registrations": 268,
"current_checker_distinct_names": 268,
"checker_runtime_seconds": [0.96, 0.75, 0.77],
"public_enumeration_apis_for_protos": 0,
"ci_jobs_total": 5,
"ci_jobs_needing_toolchain": 1,
"rederivation_defects_since_the_fix": 0,
"days_of_that_evidence": 1
},
"verification": [
"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",
"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",
"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",
"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)"
],
"changes": [
"docs/loadable-set-source-of-truth.md - the decision, its measurements and the re-measure trigger",
"docs/worklog/2026-09-19-loadable-set-source-of-truth.{md,json}, REPORT.md, STATE.md",
"no .rs and no scripts/ changes"
],
"issues": [
"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.",
"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.",
"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."
],
"adoptedProposals": [
"2026-09-18-named-exception-classes-are-loadable#p2"
],
"proposals": [
{
"title": "Assert the loadable re-derivation did not come up short, instead of trusting it",
"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.",
"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.",
"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.",
"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.",
"effort": "S",
"target": "scripts/check-named-exception-classes-are-loadable.py"
}
]
}
Loading
Loading