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

## [2026-09-18] 「조용한 실패」를 잡는 검사기에 «조용히 통과하는 길»이 있었다 (rustjava-merge-dropped-symbols-checker-swallows-git-failures)
- 무엇을: `scripts/check-merge-dropped-symbols.py` 의 `run()` 이 git 실패를 `None` 으로 삼키고 호출부가 전부 `(… or "")` 로 받아 ★**「git 이 못 답했다」가 「없다고 답했다」로 접혔다** ⇒ `✓ (0 file(s) examined)` · **rc=0**.
- ★**재현은 합성이 아니라 «진짜 얕은 클론»이다**(`--depth 10`): **전 rc=0** 에 `✓ 97660921 (0 …)` · `✓ 56bb54fa (0 …)` ↔ ★**완전 클론에서 `56bb54fa` 는 examined «20»** 이다. ⇒ 20개를 보던 머지가 0으로 접히고 run 전체가 green 이었다. **후 rc=2** `cannot measure: shallow clone: …(git fetch --unshallow)`.
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-count-nonliteral-exception-call-sites] ★★**사각의 «크기»를 쟀다 — 비리터럴 exception() 호출부는 «0» 이다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`. ★**순수 측정 · `.rs` 0줄 · `scripts/` 0줄.**
★**수**: bare `exception(` **847** = 정의 1 + **리터럴 846** + 그 밖 리터럴 **0** + ★**비리터럴 0** ⇒ 검사기가 보는 집합 = 실제 호출부 집합(지금은 일치).
★★**술어를 갈라야 답이 맞는다** — `exception(` 부분일치가 `assert_exception(` 등 **다른 함수 8종 41자리**(첫 인자가 `jvm`)를 쓸어담는다. 안 갈랐으면 ★**M=33 이라는 틀린 답**이었다.
★**양방향으로 술어를 시험했다**: 대조군 = 한 줄 리터럴만 세면 **812** = 검사기 초판 수와 정확히 일치 · 개악 주입(변수·`format!`·`const`·raw string) → 전건 `nonliteral` **0→4**, 비-java 리터럴 → `literal_other` **0→1**, 원복 후 **0/0**·트리 클린.
★**단위 주의**: 매크로 본문 **3자리 × 22전개** ⇒ 전개 기준이면 **865**(소스 기준 846). 이름이 전부 리터럴이라 **답은 불변** — 사각이 아니라 단위 차이다.
★**잃는 것**: 토큰 붙이기 매크로는 어떤 텍스트 술어도 못 본다(이 트리 0) · 「리터럴인데 오타」는 종전 한계 그대로 · `new_class(`·`find_class(` 는 요지 밖이라 미계수.
★**게이트 승격은 «안 했다»** — 제안이 요구한 것은 계수이고, 베이스라인 0 관문은 별 결정이라 후속 카드로 남겼다.
- [rustjava-merge-dropped-symbols-checker-swallows-git-failures] ★★**「조용한 실패」 검사기에 «조용히 통과하는 길»이 있었다 — 닫았다.**
★**재현 = 진짜 얕은 클론**(`--depth 10`): 전 **rc=0** `✓ 56bb54fa (0 file(s) examined)` ↔ ★완전 클론에선 **examined 20** ⇒ 20→0 으로 접히고 green. 후 **rc=2 `cannot measure: shallow clone…`**.
★raise 경로도 쟀다 — 범위 오류·루프 내 diff 실패·비-git **전부 rc=2**(git stderr 동봉).
Expand Down
48 changes: 48 additions & 0 deletions docs/worklog/2026-09-18-nonliteral-exception-call-sites.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"date": "2026-09-18",
"taskId": "rustjava-count-nonliteral-exception-call-sites",
"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.",
"measurements": {
"bare_exception_sites_total": 847,
"definition": 1,
"literal_java": 846,
"literal_other": 0,
"nonliteral": 0,
"suffixed_helper_sites_excluded": 41,
"suffixed_helper_nonliteral_if_wrongly_counted": 33,
"single_line_literal_control": 812,
"macro_body_sites": 3,
"macro_invocations": 22,
"expansion_basis_total": 865,
"literal_java_in_product": 781,
"literal_java_in_tests": 65
},
"verification": [
"control: single-line-only literal count = 812 = the checker's own pre-fix figure (846 - 34 rustfmt-split), same file set and anchor",
"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",
"negative probes: 'exception (' with space = 0, Jvm::exception as value or UFCS = 0, macro_rules! files = 2 and neither pastes an identifier"
],
"changes": [
"docs/worklog/2026-09-18-nonliteral-exception-call-sites.{md,json} (this pair)",
"REPORT.md, STATE.md — round record",
"no .rs and no scripts/ changes: this is a measurement round"
],
"issues": [
"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.",
"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."
],
"adoptedProposals": [
"2026-09-18-named-exception-classes-are-loadable#p0"
],
"proposals": [
{
"title": "Decide whether nonliteral exception() call sites should be a check, now that the baseline is 0",
"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.",
"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.",
"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.",
"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.",
"effort": "S",
"target": "scripts/check-named-exception-classes-are-loadable.py, .github/workflows/rust.yml, CLAUDE.md"
}
]
}
109 changes: 109 additions & 0 deletions docs/worklog/2026-09-18-nonliteral-exception-call-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 2026-09-18 — How big is the literal-only blind spot? Zero, and here is the predicate that says so

Round: `rustjava-count-nonliteral-exception-call-sites`
Adopted proposal: `2026-09-18-named-exception-classes-are-loadable#p0`
— *"The new check only sees class names written out in full; nobody knows yet how many are built at
run time instead, so we cannot say how big the blind spot is."*

**This is a measurement round. Nothing in `scripts/` or any `.rs` changed.** The output is a number
and the predicate that produced it.

## Answer

| bucket (bare `exception(` = the `Jvm::exception` axis) | count |
|---|---|
| `definition` — `pub async fn exception(&self, r#type: &str, …)` in `jvm/src/jvm.rs:944` | 1 |
| `literal_java` — first argument is a `"java/…"`/`"javax/…"` string literal | **846** |
| `literal_other` — first argument is a string literal with any other prefix | **0** |
| **`nonliteral`** — **first argument is built at run time (variable, `const`, `format!`, …)** | **0** |
| total `exception(` occurrences in tracked `*.rs` | 847 |

**M = 0. The blind spot is empty today.** Every one of the 846 call sites spells its class name as a
`java/`- or `javax/`-prefixed literal, which is exactly the set
`scripts/check-named-exception-classes-are-loadable.py` already reads — so the check's floor and its
ceiling currently coincide.

Where the 846 live: **781** in product code, **65** under test trees, **0** on a commented-out line.
(The checker counts all three the same way; the split is here only so the number is not mistaken for
a product-only figure.)

`literal_other = 0` is worth stating separately: the checker *also* skips a literal that is not
`java/`-prefixed (e.g. `"org/rustjava/…"`), and there are none of those either. So the checker is not
missing literals for prefix reasons, only for run-time-assembly reasons — of which there are none.

## The predicate

Kept deliberately close to the checker's own, so the two numbers are comparable rather than merely
similar: same file set (workspace `*.rs`, `target/` and `.git` pruned), same whole-file matching so
rustfmt's line break after `exception(` is crossed. Two things differ, and both are necessary:

```python
SITE = re.compile(r'(?P<prefix>[A-Za-z0-9_]*)exception\(\s*') # the checker's anchor
LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"') # a plain "…" first argument
DEFN = re.compile(r'\bfn\s+$') # `fn exception(` is not a call

# bucket = literal_java if the literal starts java/ or javax/
# literal_other if it is a literal with another prefix
# nonliteral otherwise <-- anything unrecognised lands HERE, not in a safe bucket
```

1. The `java/` requirement is **dropped** — we look at whatever the first argument *is*. The checker
asks "is this name loadable"; this asks "is there a name here at all".
2. `exception(` as a bare substring also matches **eight other functions** —
`assert_exception(`, `suppress_io_exception(`, `assert_null_pointer_exception(` and five more,
41 sites in total, whose first parameter is `jvm`, not a class name. Counting those as
"names built at run time" would have produced **M = 33**, which is a wrong answer to the
question asked: they are a different function. They are split into their own bucket.

Full script: `~/orchestrator/reports/evidence/rustjava-count-nonliteral-exception-call-sites/enumerate.py`.

## Why the zero is a measured zero

A zero from a predicate that cannot see anything is worthless, so the predicate was tested in both
directions before the number was believed.

**Control** — the predicate must reproduce a number the checker already vouches for. Counting only
literal calls that fit on *one* line gives **812**, which is precisely the checker's own pre-fix
figure (846 total − 34 that rustfmt had broken across a newline, recorded in its docstring). Same
file set, same anchor.

**Mutation probe** — five shapes injected into a product file (`jvm/src/jvm.rs`), measured, reverted:

| injected first argument | bucket it landed in |
|---|---|
| `name` (a `&str` variable) | `nonliteral` ✔ |
| `&format!("java/lang/{}", name)` | `nonliteral` ✔ |
| `SOME_CONST` | `nonliteral` ✔ |
| `r#"java/lang/RawString"#` (raw string) | `nonliteral` ✔ |
| `"org/rustjava/NotJavaPrefixed"` | `literal_other` ✔ |

`nonliteral 0 → 4`, `literal_other 0 → 1`; after revert, back to `0 / 0` with a clean tree. The raw
string landing in `nonliteral` rather than being read as a literal is the intended bias: an
unrecognised spelling is reported as blind spot, never silently as safe.

## What the predicate still cannot see

- **Macro expansion is counted once, at the body.** Four `macro_rules!` in
`rustjava-runtime/src/classes/java/util/arrays.rs` contain **3** `exception(` sites between them and
are invoked **22** times, so an expansion-basis count is **865**, not 846. Every one of those names
is a literal inside the macro body, so this changes the *site* count and not the answer: it is not
a blind spot, it is a units mismatch, and both this round and the checker use source units.
- **Token-pasted call sites** (`concat_idents!`/`paste!` building the identifier `exception`) would be
invisible to any text predicate. Measured: this tree has `macro_rules!` in **2** files total, and
neither constructs a function name. Also 0 for `Jvm::exception` passed as a value or called UFCS,
and 0 for `exception (` written with a space.
- **A literal that is simply wrong** — a typo matching some other real class — is the checker's own
documented limit, unchanged here.
- **Other panic paths** (`new_class(`, `find_class(`) are outside the adopted proposal's point and
were not counted; those return `Result` to their caller rather than unwrapping.

## Judgement on the adopted proposal

The premise was **true and worth asking**: nobody had measured this, and the checker's docstring
asserts the limitation without sizing it. The answer happens to be 0 — which does **not** make the
checker's floor fictional, it makes it *currently tight*. Nothing prevents the next round from
writing `jvm.exception(&name, …)`; the predicate above is what would notice.

Whether to promote that predicate into a check (fail when `nonliteral > 0`) is **deliberately left
open** — the adopted proposal asked for a count, not a gate, and a gate on a baseline of 0 is a
separate decision with its own cost. It is filed below as a proposal instead of being built here.
Loading