|
| 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