Skip to content

Commit 079c2c2

Browse files
author
jun0
committed
[2026-09-18-nonliteral-exception-call-sites-p0] feat(scripts): 비리터럴 사각을 «세어서 찍는다» — 관문으로 올리지 않는다
채택 제안 2026-09-18-nonliteral-exception-call-sites#p0 — 「baseline 이 0 이니 관문으로 올릴까」. ★전제 «둘 다» 하루 만에 소멸했다(origin/main @ e9910a7 재측): ⑴「baseline is 0」 → 실제 1. jvm/tests/test_exception_construction.rs:19 이고, 그 자리는 «비리터럴이어야만» 한다 — 리터럴로 쓰면 바로 이 검사기가 red 다. ⇒ 관문을 0으로 걸었으면 제안된 날 main 이 red 였고 대상은 정상 코드다. ★제안이 자기 why 에 그 비용을 예고했고 약 4시간 뒤 현실이 됐다. ⑵「crashing the whole runtime 대신」 → 더는 죽지 않는다(…-exception-throws-instead-of-unwrap 착지). 못 싣는 이름은 NoClassDefFoundError 를 낸다 ⇒ 해악이 «죽음»에서 «틀린 catch»로 내려갔다. ⇒ 관문 대신 제안의 진짜 걱정(tradeoff: 「회차 사이에 바닥이 측정되지 않는다」)만 값싸게 고친다: 매 실행에 사각을 세어 한 줄로 찍고 ★절대 실패시키지 않는다. 종료코드 의미 불변(0/1/2). 1파일 +90/−6 · 새 DoD 명령 0 · 새 CI 잡 0. 축(제품 코드 · 양방향): jvm/src/jvm.rs 에 런타임 조립 호출 주입 → 1→2 이고 파일:줄을 지목 · 원복 → 1 · rc 양쪽 0(그것이 «관문이 아니다»의 뜻이다). 술어 민감도: 8종 분리를 지우면 42(헬퍼 호출 33 + 정의 8 + 진짜 1) ⇒ 느슨한 앵커의 산물이 아니다. ★내가 만든 회귀 둘을 재서 걷어냈다: 두 번 걷기(3.3~4.3s → 10.0~13.3s) · 개행 인덱스(~2배 잔존). 최종 비용 유의차 없음(전 1.33/3.20/1.55/1.58s ↔ 후 1.73/1.88/1.44/1.54s · 구간 겹침). 결정은 검사기 docstring 에 못박았다 — 다음 회차가 관문을 다시 제안하기 전에 읽는 자리다.
1 parent e9910a7 commit 079c2c2

5 files changed

Lines changed: 267 additions & 6 deletions

‎REPORT.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
# REPORT
2+
## [2026-09-19] 비리터럴 사각을 «관문»으로 올릴까 — ★**아니다. 제안의 전제 «둘 다» 하루 만에 소멸했다**(2026-09-18-nonliteral-exception-call-sites-p0)
3+
- 무엇을: 채택 제안 `2026-09-18-nonliteral-exception-call-sites#p0`(worklog json 기록). 검사기가 사각을 **세어서 찍고 ★절대 실패시키지 않는다**. ★결정을 **검사기 docstring 에 못박았다**(다음 회차가 관문을 다시 제안하기 «전»에 읽는 자리).
4+
- ★★**전제 재측(`origin/main` @ `e9910a7a`)**: ⑴「baseline is **0**」 → ★**1이다**(`jvm/tests/test_exception_construction.rs:19`) ⑵「**crashing the whole runtime** 대신」 → ★**더는 죽지 않는다**(`…-exception-throws-instead-of-unwrap` 착지).
5+
- ★**그 1자리는 «옳고», «비리터럴이어야만» 한다**: 그 테스트는 못 싣는 이름을 부르는데 리터럴로 쓰면 ★**바로 이 검사기가 red** 가 된다(작성 당시 실측). ⇒ ★**관문을 0으로 걸었으면 제안된 날 main 이 red** 였고, 그 대상은 **정상 코드**다.
6+
★**제안이 자기 `why` 에서 이 비용을 예고했다** — 「변수로 이름을 넘기는 정당한 리팩터가 red 가 되어 논박당한다」. ★그 예고가 **약 4시간 뒤** 현실이 됐다.
7+
- ★**막으려던 해악도 작아졌다**: 이제 못 싣는 이름은 **프로세스를 죽이지 않고** `NoClassDefFoundError` 를 낸다 ⇒ ★**「catch 가 안 걸린다」는 조용한 오동작**이지 «죽음»이 아니다(제안의 `userBenefit` 근거가 그만큼 약해졌다).
8+
- ★**대신 «값싼 절반»을 지었다** — 제안의 진짜 걱정은 `tradeoff` 의 「회차 사이에 바닥이 측정되지 않는다」이고, 그것은 **관문 없이** 고쳐진다: 매 실행에 한 줄로 찍는다(pass·fail 무관).
9+
`Blind spot: 1 call site(s) … ? jvm/tests/test_exception_construction.rs:19` + 기존 `✓ 43 … all 268 loadable`.
10+
- ★**바꾼 수**: **1파일 · +90/−6** · ★**새 DoD 명령 0 · 새 CI 잡 0 · 종료코드 의미 불변**(0/1/2 그대로).
11+
- ★**양방향 축(제품 코드)**: `jvm/src/jvm.rs` 에 런타임 조립 호출 주입 → **1 → 2** 이고 ★**`jvm/src/jvm.rs:1390` 을 이름으로 지목** · 원복 → **1**(트리 클린) · ★**rc 는 양쪽 다 0**(그것이 «관문이 아니다»의 뜻이다).
12+
★**술어 민감도**: 「다른 함수 8종 분리」를 지우면 **42**(헬퍼 호출 33 + 헬퍼 «정의» 8 + 진짜 1) ⇒ ★그 수가 «느슨한 앵커의 산물»이 아님을 보인다.
13+
- ★**대가(숨기지 않는다)**: ⒜**찍힌 수는 «무시할 수 있다»** — 관문은 강제하고 한 줄은 스크롤로 지나친다(그것이 반대편의 최강 논거다) ⒝**수는 술어만큼만 정확하다**(42가 그 실수의 모습이고, 이 회차 프로브 말고는 잠그는 것이 없다) ⒞**제품/테스트를 구별하지 않는다**(오늘 전건이 테스트인데 표시가 없다).
14+
- ★★**내가 만든 회귀 둘을 재서 걷어냈다**(눈으로 잡은 것이 아니다): ⑴**`.rs` 전수를 두 번 걷기** 3.3~4.3s → **10.0~13.3s** ⇒ 단일 패스로 병합 ⑵**파일마다 개행 인덱스를 파이썬 루프로** 만들기(남은 ~2배의 «진짜» 원인) ⇒ `text.count` 로 되돌림(전 트리 매치가 ~850이라 «매치당 세기»가 «문자당 인덱싱»보다 싸다) ⑶앵커 `[A-Za-z0-9_]*exception\(` 가 단어문자 위치마다 시도하게 만든다 ⇒ 리터럴 앵커 + 앞 글자 검사로 교체. ⇒ ★**최종 비용 유의차 없음**(전 1.33/3.20/1.55/1.58s ↔ 후 1.73/1.88/1.44/1.54s · 구간 겹침).
15+
- ★**되돌릴 조건**: ⑴런타임 조립 이름이 **제품 코드**에 나타나거나 ⑵**아무도 모르게 수가 는다**(그 한 줄이 정확히 그것을 막는다).
16+
- 검증: DoD 9명령 · 아래 절.
17+
- ★후속 추천: **그 사각이 «제품인지 테스트인지»를 말할 것인가**(S). 상세 = `docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md`.
18+
219
## [2026-09-19] 일으키려던 예외를 못 만들면 «죽었다» — 그 보고를 손에 쥔 채로(rustjava-jvm-exception-throws-instead-of-unwrap)
320
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`(worklog json `adoptedProposals` 기록). `Jvm::exception` 의 `.unwrap()` 두 개를 **반환**으로 바꿨다. ★**시그니처 불변 · 새 enum variant 0 · 호출부 편집 0.**
421
- ★★**급소 — 실패가 «이미» 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
@@ -7,6 +7,13 @@
77
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)
88

99
## 완료
10+
- [2026-09-18-nonliteral-exception-call-sites-p0] ★★**비리터럴 사각은 «관문»이 아니라 «보고»다 — 제안의 전제 둘 다 소멸.** 채택 제안 `2026-09-18-nonliteral-exception-call-sites#p0`.
11+
★**전제 재측**: 「baseline 0」 → ★**1**(그 1자리는 **정상**이고 «비리터럴이어야만» 한다 — 리터럴이면 이 검사기가 red) · 「죽는다」 → ★**더는 안 죽는다**(#76 착지) ⇒ 해악이 «죽음»에서 «틀린 catch»로 내려갔다.
12+
★**관문을 0으로 걸었으면 제안된 날 main 이 red** 였다 — ★제안이 자기 `why` 에 그 비용을 예고했고 **4시간 뒤** 현실이 됐다.
13+
★**지은 것**: 매 실행에 사각을 **세어 찍는다**(never fail) · **1파일 +90/−6** · 새 DoD 명령 **0** · 새 CI 잡 **0** · 종료코드 불변.
14+
★**축**: 제품 코드 주입 → **1→2**(파일:줄 지목) · 원복 → 1 · rc 양쪽 0 · 술어 민감도 **42**.
15+
★**대가**: 찍힌 수는 무시할 수 있다 · 수는 술어만큼만 정확 · 제품/테스트 미구별(후속 카드).
16+
★**회귀 둘을 스스로 만들고 재서 걷어냈다**(두 번 걷기 · 개행 인덱스) ⇒ 최종 비용 **유의차 없음**.
1017
- [rustjava-jvm-exception-throws-instead-of-unwrap] ★★**일으키려던 예외를 못 만들면 죽던 것을 «보고»로 바꿨다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p1`. ★시그니처 불변 · variant 0 · 호출부 편집 0.
1118
★**급소**: `from_rust_string`·`new_class` 의 실패는 **이미 `JavaError`**(= 자바 예외)다 — unwrap 이 그것을 버렸다. ⇒ 그대로 돌려준다.
1219
★**실측**: `panicked … unwrap() on an Err value: JavaException(java/lang/NoClassDefFoundError)` — ★올바른 보고가 **패닉 메시지 안에** 실려 사라졌다.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "2026-09-18-nonliteral-exception-call-sites-p0",
4+
"summary": "Decided not to gate the non-literal blind spot. Both premises of the adopted proposal had expired within a day: the baseline is 1 rather than 0 (a test must pass an unloadable name through a variable, because a literal there makes this very check red), and an unloadable name no longer crashes the runtime since the exception-throws round landed - it raises NoClassDefFoundError instead of the intended exception. Built the cheap half instead: the checker now counts and prints the blind spot and never fails on it.",
5+
"decision": "report the blind spot, do not gate it",
6+
"measurements": {
7+
"baseline_claimed_by_proposal": 0,
8+
"baseline_measured_now": 1,
9+
"nonliteral_site": "jvm/tests/test_exception_construction.rs:19",
10+
"files_changed": 1,
11+
"lines_added": 90,
12+
"lines_removed": 6,
13+
"new_dod_commands": 0,
14+
"new_ci_jobs": 0,
15+
"blind_spot_with_helper_split_removed": 42,
16+
"runtime_before_seconds": [1.33, 3.20, 1.55, 1.58],
17+
"runtime_after_seconds": [1.73, 1.88, 1.44, 1.54],
18+
"runtime_after_first_draft_seconds": [10.03, 13.27, 11.13],
19+
"named_classes": 43,
20+
"literal_call_sites": 846,
21+
"loadable_classes": 268
22+
},
23+
"verification": [
24+
"axis on product code: injecting a run-time-assembled self.exception(name, ...) into jvm/src/jvm.rs moves the report from 1 to 2 and names jvm/src/jvm.rs:1390; reverting returns it to 1 with a clean tree; rc stays 0 both ways, which is the decision",
25+
"predicate sensitivity: removing the helper-name split makes the report say 42 (33 helper calls + 8 helper definitions + the 1 real site), so the number is not an artefact of a loose anchor",
26+
"premise re-measured against origin/main @ e9910a7a: baseline 1, and the axis test for the exception-throws round passes, i.e. an unloadable name returns NoClassDefFoundError rather than aborting",
27+
"cost: before 1.33/3.20/1.55/1.58s vs after 1.73/1.88/1.44/1.54s, overlapping ranges"
28+
],
29+
"changes": [
30+
"scripts/check-named-exception-classes-are-loadable.py - counts and prints run-time-assembled call sites, never fails on them; decision and its two dead premises recorded in the docstring; scan of the two axes merged into one pass",
31+
"docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.{md,json}, REPORT.md, STATE.md"
32+
],
33+
"issues": [
34+
"A printed number can be scrolled past; a gate cannot. That is the trade this round chose and it is the strongest argument for the other answer.",
35+
"The count is only as good as its predicate - the 42 above is what a one-line mistake looks like - and nothing tests it except this round's probes.",
36+
"The report does not distinguish product code from tests. Every non-literal site today is a test; a product one would read identically.",
37+
"I introduced two runtime regressions while building this (a second full walk of every .rs, then a per-character newline index) and removed both; the first draft ran 10-13s against a 1.3-3.2s baseline."
38+
],
39+
"adoptedProposals": [
40+
"2026-09-18-nonliteral-exception-call-sites#p0"
41+
],
42+
"proposals": [
43+
{
44+
"title": "Say whether a run-time-assembled exception name is in product code or in a test",
45+
"plainSummary": "The safety check now reports the places where an error class name is built while the program runs. It does not say whether those places are real product code or just test scaffolding, and only one of those is worth worrying about.",
46+
"userBenefit": "A genuinely risky site in the runtime would stand out immediately instead of blending in with test helpers that are fine.",
47+
"why": "This round decided against failing on the count, precisely because the only site today is a test that has to be written that way. That judgement depends entirely on the product/test split, and the report does not carry it - so the next reader has to re-derive it by opening each path. The repo already knows the split: the non-literal round measured 781 product versus 65 test call sites using nothing more than the path.",
48+
"tradeoff": "It is a second classification to keep honest, and path-based heuristics are exactly the kind of thing that rots when a directory is renamed; an alternative is to leave the paths and trust the reader. It also risks implying that a test site is always acceptable, which is only true while it is deliberate.",
49+
"effort": "S",
50+
"target": "scripts/check-named-exception-classes-are-loadable.py"
51+
}
52+
]
53+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# 2026-09-19 — Should the non-literal blind spot be a gate? No. Both of the proposal's premises expired.
2+
3+
Round: `2026-09-18-nonliteral-exception-call-sites-p0`
4+
Adopted proposal: `2026-09-18-nonliteral-exception-call-sites#p0` —
5+
*"Decide whether nonliteral exception() call sites should be a check, now that the baseline is 0."*
6+
7+
## Decision
8+
9+
**No gate.** `check-named-exception-classes-are-loadable.py` now **counts and prints** the blind spot
10+
and **never fails on it**. The reasoning is recorded in the checker's own docstring, where the next
11+
round will read it before re-proposing the gate.
12+
13+
## The proposal is one day old and both of its premises are already false
14+
15+
It was written by the round that measured the blind spot at 0. Re-measured against `origin/main`
16+
@ `e9910a7a`:
17+
18+
| premise, quoted from the proposal | status now |
19+
|---|---|
20+
| *"now that the baseline is **0**"* | **false — it is 1.** `jvm/tests/test_exception_construction.rs:19` |
21+
| *"instead of **crashing the whole runtime**"* | **false — it no longer crashes.** |
22+
23+
**The one non-literal site is correct, and it has to be non-literal.** That test asks
24+
`Jvm::exception` for a class no loader can provide. Written as a literal, *this very check* reports
25+
it and goes red — measured when it was first written. So it passes the name through a variable. A
26+
gate at zero would have been **red on `main` the day it was proposed**, against a site that is right.
27+
28+
The proposal predicted this in its own `why` field: *"a gate whose baseline is 0 has its own cost —
29+
it turns a legitimate future refactor (passing a name through a variable) into a red that must be
30+
argued down."* That cost stopped being hypothetical roughly four hours after the sentence was
31+
written.
32+
33+
**And the harm it guards is smaller than the proposal's `userBenefit` says.** Since
34+
`rustjava-jvm-exception-throws-instead-of-unwrap` landed (`e9910a7a`), an unloadable name does not
35+
abort the process — it returns the `NoClassDefFoundError` the loader raised. So a run-time-assembled
36+
unloadable name now means *the caller catches the wrong class*, which is a real bug and a quiet one,
37+
but it is not the crash the gate was argued for.
38+
39+
## What was built instead
40+
41+
The proposal's actual worry is in its `tradeoff`: *"not adding it means the floor stays unmeasured
42+
between rounds."* That is fixed without the gate — the number is printed on every run, pass or fail:
43+
44+
```
45+
Blind spot: 1 call site(s) build the class name at run time, so this check
46+
does not see them. Not an error -- an unloadable name there raises NoClassDefFoundError
47+
rather than the intended exception, which is a wrong catch, not a crash:
48+
? jvm/tests/test_exception_construction.rs:19
49+
✓ 43 named exception class(es) across 846 call site(s); all 268 loadable
50+
```
51+
52+
**Changed: 1 file, +90/−6 lines (`git diff --numstat`), 0 new commands, 0 new CI jobs.** Exit codes are untouched (0/1/2
53+
mean exactly what they meant).
54+
55+
## Axis — bidirectional, on product code
56+
57+
| probe | result |
58+
|---|---|
59+
| inject a run-time-assembled `self.exception(name, …)` into **`jvm/src/jvm.rs`** | `Blind spot: **2**` and it names `jvm/src/jvm.rs:1390` |
60+
| revert | `Blind spot: **1**`, tree clean |
61+
| remove the helper-name split from the predicate | `Blind spot: **42**` — 33 helper calls + 8 helper definitions + the 1 real site |
62+
63+
The third probe is the one that shows the number *means* something: `exception(` is a substring of
64+
eight helper functions in the test trees whose first parameter is `jvm`, not a class name. Without
65+
that split the report would be off by a factor of 42.
66+
67+
**Note on the axis clause**: the acceptance asks for "revert it and get red". This change is
68+
deliberately incapable of red — that is the decision. So the axis is the *number* moving and naming
69+
the new site, plus `rc` staying 0 in both directions, which is what "reported, not gated" has to
70+
mean.
71+
72+
## What this costs
73+
74+
- **A number that nobody is forced to act on.** A gate makes you deal with it; a printed line can be
75+
scrolled past. That is the trade this round chose, and it is the strongest argument for the gate.
76+
- **The count is only as good as its predicate** — the 42 above is what a one-line mistake looks
77+
like. It is not tested by anything except the probes in this round.
78+
- **Product versus test is not distinguished.** Today all non-literal sites are tests; the report
79+
does not say so, and a product site would read identically.
80+
- Runtime: **no significant change** — before 1.33 / 3.20 / 1.55 / 1.58 s, after 1.73 / 1.88 / 1.44 /
81+
1.54 s (overlapping). Getting there took two rewrites; see below.
82+
83+
## Three rewrites before this was free
84+
85+
The first two were regressions I introduced; the third is what finally paid for the feature. All
86+
measured, none spotted by eye:
87+
88+
1. **A second full walk of every `*.rs`** — the report scanned the tree again. 3.3–4.3 s → 10.0–13.3 s.
89+
Merged into one pass shared by both axes.
90+
2. **A per-character newline index** built in Python for every file, to make line numbers cheap.
91+
It *was* the remaining regression (still ~2×). Replaced with `text.count("\n", 0, …)` — there are
92+
~850 matches in the whole tree, so counting per match beats indexing per character.
93+
3. Then the anchor itself: `[A-Za-z0-9_]*exception\(` forced the engine to try every word-character
94+
position. Anchoring on the literal `exception\(` and testing the preceding character instead is
95+
the same question and restored parity.
96+
97+
## What would reopen this
98+
99+
- A run-time-assembled name appears in **product** code (every site today is a test), or
100+
- the count grows without a round noticing — which is exactly what the printed line is for.

0 commit comments

Comments
 (0)