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