diff --git a/REPORT.md b/REPORT.md index 11818e27..9f732458 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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)`. diff --git a/STATE.md b/STATE.md index e21fd100..410577b6 100644 --- a/STATE.md +++ b/STATE.md @@ -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 동봉). diff --git a/docs/worklog/2026-09-18-nonliteral-exception-call-sites.json b/docs/worklog/2026-09-18-nonliteral-exception-call-sites.json new file mode 100644 index 00000000..a7315d1e --- /dev/null +++ b/docs/worklog/2026-09-18-nonliteral-exception-call-sites.json @@ -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" + } + ] +} diff --git a/docs/worklog/2026-09-18-nonliteral-exception-call-sites.md b/docs/worklog/2026-09-18-nonliteral-exception-call-sites.md new file mode 100644 index 00000000..3f5d46c9 --- /dev/null +++ b/docs/worklog/2026-09-18-nonliteral-exception-call-sites.md @@ -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[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.