Skip to content

Commit 128e0fe

Browse files
author
jun0
committed
[rustjava-count-nonliteral-exception-call-sites] docs: 리터럴이 아닌 exception() 호출부를 센다 — 답은 0 이다
채택 제안 2026-09-18-named-exception-classes-are-loadable#p0 의 요지(「사각의 크기를 아무도 모른다」)에 수로 답한다. 측정 회차라 .rs 0줄 · scripts/ 0줄. bare exception( 847 = 정의 1 + 리터럴(java/javax) 846 + 리터럴(그 밖) 0 + 비리터럴 0. ⇒ 검사기가 보는 집합과 실제 호출부 집합이 지금은 일치한다. 술어를 양방향으로 시험했다: 대조군(한 줄 리터럴만) 812 = 검사기 초판 수와 정확히 일치 · 개악 주입(변수·format!·const·raw string) → 전건 nonliteral 0→4, 비-java 리터럴 → literal_other 0→1, 원복 후 0/0. exception( 부분일치가 다른 함수 8종 41자리를 쓸어담는다 — 안 가르면 M=33 이라는 틀린 답이 나온다. 게이트 승격은 하지 않았다(제안이 요구한 것은 계수) — 후속 제안 카드로 남겼다.
1 parent 117c875 commit 128e0fe

4 files changed

Lines changed: 180 additions & 0 deletions

File tree

‎REPORT.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
# REPORT
2+
## [2026-09-18] 리터럴이 «아닌» 이름으로 exception() 을 부르는 자리는 몇 개인가 — ★**0 이다**(rustjava-count-nonliteral-exception-call-sites)
3+
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`(worklog json `adoptedProposals` 기록). ★**순수 측정 회차 — `.rs` 0줄 · `scripts/` 0줄.** 산출은 «수»와 «술어»다.
4+
- ★**답**: bare `exception(` **847** = 정의 **1** + ★**리터럴(java/javax) 846** + 리터럴(그 밖) **0** + ★★**비리터럴 «0»**.
5+
⇒ 검사기가 보는 집합과 실제 호출부 집합이 **지금은 일치한다** — 사각의 «크기»는 **0**이다.
6+
- ★`literal_other` 도 **0** 이라 따로 적는다: 검사기는 `java/` 접두가 아닌 리터럴(`org/rustjava/…`)도 건너뛰는데 **그런 것도 없다** ⇒ 새는 축은 «접두»가 아니라 «런타임 조립»뿐이고, 그것이 0이다.
7+
- ★★**술어를 검사기의 것과 «같게» 맞췄다**(수가 비교 가능해야 한다): 같은 파일 집합(`target/`·`.git` 가지치기) · 같은 전파일 매칭(rustfmt 줄바꿈을 넘는다). 다른 것은 둘뿐 — ⑴`java/` 요구를 **뺐다**(「실을 수 있나」가 아니라 「이름이 있기는 한가」를 묻는다) ⑵★`exception(` 부분일치가 **다른 함수 8종**(`assert_exception(`·`suppress_io_exception(` 등 **41자리**)을 함께 쓸어담는다 — 그 첫 인자는 `jvm` 이지 클래스 이름이 아니다. ⇒ **분리했다**. 안 갈랐으면 ★**M=33 이라는 «틀린 답»**이 나온다.
8+
- ★★**「내가 찾은 게 전부다」로 주장하지 않았다 — 술어를 양방향으로 시험했다**:
9+
⒜**대조군** — 한 줄에 든 리터럴만 세면 **812**, 이는 검사기 docstring 이 적은 **자기 초판 수**(846 − rustfmt 가 쪼갠 34)와 **정확히 일치**한다 ⇒ 파일 집합·앵커가 같다는 증거.
10+
⒝**개악 주입**(제품 파일 `jvm/src/jvm.rs` · 측정 후 원복 · 트리 클린): 변수 · `&format!` · `const` · **raw string** → 전건 `nonliteral`(**0→4**) · 비-java 리터럴 → `literal_other`(**0→1**). ★raw string 이 «리터럴»이 아니라 «사각»으로 잡히는 것이 의도한 편향이다 — **모르는 철자는 안전 칸이 아니라 사각 칸으로 떨어진다**.
11+
⒞**음성 탐침**: `exception (`(공백) **0** · `Jvm::exception` 값·UFCS 전달 **0** · `macro_rules!` 보유 파일 **2**(어느 쪽도 식별자를 조립하지 않는다).
12+
- ★**못 보는 것**: ⑴★**매크로는 «본문에서 한 번» 세어진다** — `arrays.rs` 의 매크로 4개가 `exception(` **3자리**를 갖고 **22회** 전개되므로 전개 기준이면 **865**다(846 아님). ★이름은 전부 리터럴이라 **답(M=0)은 안 바뀐다** — 사각이 아니라 «단위» 차이이고, 검사기도 이 회차도 소스 단위다. ⑵토큰 붙이기 매크로가 식별자 `exception` 을 조립하면 어떤 텍스트 술어도 못 본다(이 트리에선 0 — 바닥이지 증명이 아니다) ⑶「리터럴인데 오타」는 검사기의 기존 한계 그대로 ⑷`new_class(`·`find_class(` 는 제안의 요지 밖이라 세지 않았다.
13+
- ★**제안 판정**: 전제(「아무도 크기를 모른다」)는 **참이었다** — 아무도 재지 않았다. 답이 0이라는 것은 검사기의 바닥이 «허구»라는 뜻이 아니라 ★**지금은 «딱 맞는다»**는 뜻이다. 다음 회차가 `jvm.exception(&name, …)` 을 쓰는 것을 막는 것은 아무것도 없고, 위 술어가 그것을 알아챌 물건이다.
14+
- ★**게이트로 «승격하지 않았다»** — 제안이 요구한 것은 «계수»이지 «관문»이 아니고, 베이스라인 0 인 관문은 **자기 대가**(변수로 이름을 넘기는 정상 리팩터가 red 가 된다)를 갖는 별 결정이다. ⇒ 후속 제안 카드로 남겼다(계약 「범위를 넓히지 마라」).
15+
- 검증: 아래 «검증» 절 참조(DoD 9명령).
16+
- ★후속 추천: **베이스라인이 0인 지금 이 술어를 관문으로 올릴 것인가**(S). 상세 = `docs/worklog/2026-09-18-nonliteral-exception-call-sites.md`.
17+
218
## [2026-09-18] 「조용한 실패」를 잡는 검사기에 «조용히 통과하는 길»이 있었다 (rustjava-merge-dropped-symbols-checker-swallows-git-failures)
319
- 무엇을: `scripts/check-merge-dropped-symbols.py` 의 `run()` 이 git 실패를 `None` 으로 삼키고 호출부가 전부 `(… or "")` 로 받아 ★**「git 이 못 답했다」가 「없다고 답했다」로 접혔다** ⇒ `✓ (0 file(s) examined)` · **rc=0**.
420
- ★**재현은 합성이 아니라 «진짜 얕은 클론»이다**(`--depth 10`): **전 rc=0** 에 `✓ 97660921 (0 …)` · `✓ 56bb54fa (0 …)` ↔ ★**완전 클론에서 `56bb54fa` 는 examined «20»** 이다. ⇒ 20개를 보던 머지가 0으로 접히고 run 전체가 green 이었다. **후 rc=2** `cannot measure: shallow clone: …(git fetch --unshallow)`.

‎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-count-nonliteral-exception-call-sites] ★★**사각의 «크기»를 쟀다 — 비리터럴 exception() 호출부는 «0» 이다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`. ★**순수 측정 · `.rs` 0줄 · `scripts/` 0줄.**
11+
★**수**: bare `exception(` **847** = 정의 1 + **리터럴 846** + 그 밖 리터럴 **0** + ★**비리터럴 0** ⇒ 검사기가 보는 집합 = 실제 호출부 집합(지금은 일치).
12+
★★**술어를 갈라야 답이 맞는다** — `exception(` 부분일치가 `assert_exception(` 등 **다른 함수 8종 41자리**(첫 인자가 `jvm`)를 쓸어담는다. 안 갈랐으면 ★**M=33 이라는 틀린 답**이었다.
13+
★**양방향으로 술어를 시험했다**: 대조군 = 한 줄 리터럴만 세면 **812** = 검사기 초판 수와 정확히 일치 · 개악 주입(변수·`format!`·`const`·raw string) → 전건 `nonliteral` **0→4**, 비-java 리터럴 → `literal_other` **0→1**, 원복 후 **0/0**·트리 클린.
14+
★**단위 주의**: 매크로 본문 **3자리 × 22전개** ⇒ 전개 기준이면 **865**(소스 기준 846). 이름이 전부 리터럴이라 **답은 불변** — 사각이 아니라 단위 차이다.
15+
★**잃는 것**: 토큰 붙이기 매크로는 어떤 텍스트 술어도 못 본다(이 트리 0) · 「리터럴인데 오타」는 종전 한계 그대로 · `new_class(`·`find_class(` 는 요지 밖이라 미계수.
16+
★**게이트 승격은 «안 했다»** — 제안이 요구한 것은 계수이고, 베이스라인 0 관문은 별 결정이라 후속 카드로 남겼다.
1017
- [rustjava-merge-dropped-symbols-checker-swallows-git-failures] ★★**「조용한 실패」 검사기에 «조용히 통과하는 길»이 있었다 — 닫았다.**
1118
★**재현 = 진짜 얕은 클론**(`--depth 10`): 전 **rc=0** `✓ 56bb54fa (0 file(s) examined)` ↔ ★완전 클론에선 **examined 20** ⇒ 20→0 으로 접히고 green. 후 **rc=2 `cannot measure: shallow clone…`**.
1219
★raise 경로도 쟀다 — 범위 오류·루프 내 diff 실패·비-git **전부 rc=2**(git stderr 동봉).
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"date": "2026-09-18",
3+
"taskId": "rustjava-count-nonliteral-exception-call-sites",
4+
"summary": "Counted every exception( call site and classified its first argument. Literal java/javax: 846 (exactly what the checker reads). Non-literal (name built at run time): 0. The blind spot the adopted proposal asked about is empty today; the predicate that says so was validated by a control (reproduces the checker's pre-fix 812) and a five-shape mutation probe.",
5+
"measurements": {
6+
"bare_exception_sites_total": 847,
7+
"definition": 1,
8+
"literal_java": 846,
9+
"literal_other": 0,
10+
"nonliteral": 0,
11+
"suffixed_helper_sites_excluded": 41,
12+
"suffixed_helper_nonliteral_if_wrongly_counted": 33,
13+
"single_line_literal_control": 812,
14+
"macro_body_sites": 3,
15+
"macro_invocations": 22,
16+
"expansion_basis_total": 865,
17+
"literal_java_in_product": 781,
18+
"literal_java_in_tests": 65
19+
},
20+
"verification": [
21+
"control: single-line-only literal count = 812 = the checker's own pre-fix figure (846 - 34 rustfmt-split), same file set and anchor",
22+
"mutation probe in jvm/src/jvm.rs: variable / format! / const / raw-string -> nonliteral 0->4; non-java literal -> literal_other 0->1; reverted, tree clean, back to 0/0",
23+
"negative probes: 'exception (' with space = 0, Jvm::exception as value or UFCS = 0, macro_rules! files = 2 and neither pastes an identifier"
24+
],
25+
"changes": [
26+
"docs/worklog/2026-09-18-nonliteral-exception-call-sites.{md,json} (this pair)",
27+
"REPORT.md, STATE.md — round record",
28+
"no .rs and no scripts/ changes: this is a measurement round"
29+
],
30+
"issues": [
31+
"Counts are in source units, not expansion units: 3 exception( sites live in macro bodies invoked 22 times, so an expansion-basis total would be 865. All literal either way, so the answer M=0 is unaffected.",
32+
"A token-pasting macro that builds the identifier `exception` would be invisible to any text predicate; measured 0 in this tree but it is a floor, not a proof."
33+
],
34+
"adoptedProposals": [
35+
"2026-09-18-named-exception-classes-are-loadable#p0"
36+
],
37+
"proposals": [
38+
{
39+
"title": "Decide whether nonliteral exception() call sites should be a check, now that the baseline is 0",
40+
"plainSummary": "Right now every place that raises a Java error spells the class name out in full, so the existing safety check sees all of them. Nothing stops someone from building a name at run time in future, which would slip past unseen.",
41+
"userBenefit": "Keeps the guarantee that an unknown class name throws a Java exception instead of crashing the whole runtime, even as new code is written.",
42+
"why": "This round measured the blind spot at exactly 0 and produced the predicate that detects it (control-tested against the checker's own numbers, plus a five-shape mutation probe). A gate is therefore cheap to add and would start green. The reason it was not added here: the adopted proposal asked for a count, not a gate, and a gate whose baseline is 0 has its own cost — it turns a legitimate future refactor (passing a name through a variable) into a red that must be argued down, and this repo's rule is that a lock should be decided on its own merits rather than added because the number happened to be convenient.",
43+
"tradeoff": "Adding it costs one more DoD command and makes run-time-assembled names a build failure rather than a review comment; not adding it means the floor stays unmeasured between rounds and the next non-literal call site lands silently.",
44+
"effort": "S",
45+
"target": "scripts/check-named-exception-classes-are-loadable.py, .github/workflows/rust.yml, CLAUDE.md"
46+
}
47+
]
48+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# 2026-09-18 — How big is the literal-only blind spot? Zero, and here is the predicate that says so
2+
3+
Round: `rustjava-count-nonliteral-exception-call-sites`
4+
Adopted proposal: `2026-09-18-named-exception-classes-are-loadable#p0`
5+
— *"The new check only sees class names written out in full; nobody knows yet how many are built at
6+
run time instead, so we cannot say how big the blind spot is."*
7+
8+
**This is a measurement round. Nothing in `scripts/` or any `.rs` changed.** The output is a number
9+
and the predicate that produced it.
10+
11+
## Answer
12+
13+
| bucket (bare `exception(` = the `Jvm::exception` axis) | count |
14+
|---|---|
15+
| `definition` — `pub async fn exception(&self, r#type: &str, …)` in `jvm/src/jvm.rs:944` | 1 |
16+
| `literal_java` — first argument is a `"java/…"`/`"javax/…"` string literal | **846** |
17+
| `literal_other` — first argument is a string literal with any other prefix | **0** |
18+
| **`nonliteral`** — **first argument is built at run time (variable, `const`, `format!`, …)** | **0** |
19+
| total `exception(` occurrences in tracked `*.rs` | 847 |
20+
21+
**M = 0. The blind spot is empty today.** Every one of the 846 call sites spells its class name as a
22+
`java/`- or `javax/`-prefixed literal, which is exactly the set
23+
`scripts/check-named-exception-classes-are-loadable.py` already reads — so the check's floor and its
24+
ceiling currently coincide.
25+
26+
Where the 846 live: **781** in product code, **65** under test trees, **0** on a commented-out line.
27+
(The checker counts all three the same way; the split is here only so the number is not mistaken for
28+
a product-only figure.)
29+
30+
`literal_other = 0` is worth stating separately: the checker *also* skips a literal that is not
31+
`java/`-prefixed (e.g. `"org/rustjava/…"`), and there are none of those either. So the checker is not
32+
missing literals for prefix reasons, only for run-time-assembly reasons — of which there are none.
33+
34+
## The predicate
35+
36+
Kept deliberately close to the checker's own, so the two numbers are comparable rather than merely
37+
similar: same file set (workspace `*.rs`, `target/` and `.git` pruned), same whole-file matching so
38+
rustfmt's line break after `exception(` is crossed. Two things differ, and both are necessary:
39+
40+
```python
41+
SITE = re.compile(r'(?P<prefix>[A-Za-z0-9_]*)exception\(\s*') # the checker's anchor
42+
LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"') # a plain "…" first argument
43+
DEFN = re.compile(r'\bfn\s+$') # `fn exception(` is not a call
44+
45+
# bucket = literal_java if the literal starts java/ or javax/
46+
# literal_other if it is a literal with another prefix
47+
# nonliteral otherwise <-- anything unrecognised lands HERE, not in a safe bucket
48+
```
49+
50+
1. The `java/` requirement is **dropped** — we look at whatever the first argument *is*. The checker
51+
asks "is this name loadable"; this asks "is there a name here at all".
52+
2. `exception(` as a bare substring also matches **eight other functions** —
53+
`assert_exception(`, `suppress_io_exception(`, `assert_null_pointer_exception(` and five more,
54+
41 sites in total, whose first parameter is `jvm`, not a class name. Counting those as
55+
"names built at run time" would have produced **M = 33**, which is a wrong answer to the
56+
question asked: they are a different function. They are split into their own bucket.
57+
58+
Full script: `~/orchestrator/reports/evidence/rustjava-count-nonliteral-exception-call-sites/enumerate.py`.
59+
60+
## Why the zero is a measured zero
61+
62+
A zero from a predicate that cannot see anything is worthless, so the predicate was tested in both
63+
directions before the number was believed.
64+
65+
**Control** — the predicate must reproduce a number the checker already vouches for. Counting only
66+
literal calls that fit on *one* line gives **812**, which is precisely the checker's own pre-fix
67+
figure (846 total − 34 that rustfmt had broken across a newline, recorded in its docstring). Same
68+
file set, same anchor.
69+
70+
**Mutation probe** — five shapes injected into a product file (`jvm/src/jvm.rs`), measured, reverted:
71+
72+
| injected first argument | bucket it landed in |
73+
|---|---|
74+
| `name` (a `&str` variable) | `nonliteral` ✔ |
75+
| `&format!("java/lang/{}", name)` | `nonliteral` ✔ |
76+
| `SOME_CONST` | `nonliteral` ✔ |
77+
| `r#"java/lang/RawString"#` (raw string) | `nonliteral` ✔ |
78+
| `"org/rustjava/NotJavaPrefixed"` | `literal_other` ✔ |
79+
80+
`nonliteral 0 → 4`, `literal_other 0 → 1`; after revert, back to `0 / 0` with a clean tree. The raw
81+
string landing in `nonliteral` rather than being read as a literal is the intended bias: an
82+
unrecognised spelling is reported as blind spot, never silently as safe.
83+
84+
## What the predicate still cannot see
85+
86+
- **Macro expansion is counted once, at the body.** Four `macro_rules!` in
87+
`rustjava-runtime/src/classes/java/util/arrays.rs` contain **3** `exception(` sites between them and
88+
are invoked **22** times, so an expansion-basis count is **865**, not 846. Every one of those names
89+
is a literal inside the macro body, so this changes the *site* count and not the answer: it is not
90+
a blind spot, it is a units mismatch, and both this round and the checker use source units.
91+
- **Token-pasted call sites** (`concat_idents!`/`paste!` building the identifier `exception`) would be
92+
invisible to any text predicate. Measured: this tree has `macro_rules!` in **2** files total, and
93+
neither constructs a function name. Also 0 for `Jvm::exception` passed as a value or called UFCS,
94+
and 0 for `exception (` written with a space.
95+
- **A literal that is simply wrong** — a typo matching some other real class — is the checker's own
96+
documented limit, unchanged here.
97+
- **Other panic paths** (`new_class(`, `find_class(`) are outside the adopted proposal's point and
98+
were not counted; those return `Result` to their caller rather than unwrapping.
99+
100+
## Judgement on the adopted proposal
101+
102+
The premise was **true and worth asking**: nobody had measured this, and the checker's docstring
103+
asserts the limitation without sizing it. The answer happens to be 0 — which does **not** make the
104+
checker's floor fictional, it makes it *currently tight*. Nothing prevents the next round from
105+
writing `jvm.exception(&name, …)`; the predicate above is what would notice.
106+
107+
Whether to promote that predicate into a check (fail when `nonliteral > 0`) is **deliberately left
108+
open** — the adopted proposal asked for a count, not a gate, and a gate on a baseline of 0 is a
109+
separate decision with its own cost. It is filed below as a proposal instead of being built here.

0 commit comments

Comments
 (0)