|
| 1 | +## E-PANAMA-TIMES-VALHALLA-IS-ONE-MEMBRANE-AND-THE-LAB-FINALLY-HAS-ONE-VARIABLE-1 (2026-09-19) |
| 2 | + |
| 3 | +**Operator reframing, and it is the correct one:** this was never a JDK 26→28 |
| 4 | +migration. It is **Panama-only production + Valhalla-lab arm → one production |
| 5 | +Panama × Valhalla membrane.** JDK 28 is the enabling toolchain, not the point. |
| 6 | +Panama answers *where are the bytes and how does Java reach them without |
| 7 | +copying*; Valhalla answers *how does Java name and carry semantic values over |
| 8 | +those bytes without identity-heavy heap objects*. lance-graph / T0 remains the |
| 9 | +only owner of canonical storage. |
| 10 | + |
| 11 | +### Measured on JDK 28 (`/opt/jdks/jdk-28+16`), production value-shaped |
| 12 | + |
| 13 | +| gate | result | |
| 14 | +|---|---| |
| 15 | +| native `lgj-abi` | **183 tests** green (182 + the new Range falsifier) | |
| 16 | +| Java `AllTests`, production as `value record` | **ALL PASSED (409 checks)** — identical to the unflipped baseline on the same JDK | |
| 17 | +| all six vocabulary types | `isValue() == true`; substitutability holds | |
| 18 | + |
| 19 | +### The flattening cliff is UNCHANGED on JDK 28 — and it bites one of our six |
| 20 | + |
| 21 | +Measured by the lab, both arms on one JDK: |
| 22 | + |
| 23 | +| type | payload | array | |
| 24 | +|---|---|---| |
| 25 | +| `LaneId` (1 int) | 4 B | **FLAT** | |
| 26 | +| `Ordinal` (1 int) | 4 B | **FLAT** | |
| 27 | +| `MaskId` (1 long) | 8 B | **FLAT** | |
| 28 | +| `RowRange` (2 long) | 16 B | **NOT-FLAT** | |
| 29 | +| `Row` (1 long + 2 int) | 16 B | **NOT-FLAT** | |
| 30 | + |
| 31 | +The ≤8-byte cliff measured on the 27-jep401ea3 build **reproduces on JDK 28**. |
| 32 | +So `value semantics` and `flattening` remain two different claims, and |
| 33 | +**`RowRange` is a real production type on the wrong side of the cliff.** No |
| 34 | +performance claim may be made for it from `isValue() == true`. |
| 35 | + |
| 36 | +### Allocation deltas, record arm → value arm (1M ops, same JDK) |
| 37 | + |
| 38 | +| measurement | record | value | delta | |
| 39 | +|---|---|---|---| |
| 40 | +| construct N `LaneId` into an array | 15.26 MiB | **1.87 MiB** | **8.2× less** (16 B → 1.96 B each) | |
| 41 | +| allocate+fill `LaneId[N]` | 19.07 MiB | **5.65 MiB** | 3.4× less | |
| 42 | +| construct N `Descriptor` (2 wrappers) | 45.78 MiB | **27.89 MiB** | 1.6× less | |
| 43 | +| construct N `LaneId`, never escaping | 4.73 MiB | **5.75 MiB** | ⊘ **value arm allocates MORE** — escape analysis already erased the record case | |
| 44 | +| hydrate 65,536 `Row` | 1.50 MiB | **2.00 MiB** | ⊘ **worse** — `Row` is 16 B, NOT-FLAT | |
| 45 | + |
| 46 | +**Two of the five went the wrong way, and both are honest.** Where EA already |
| 47 | +won, value classes add nothing; where the shape does not flatten, hydration |
| 48 | +costs more. Production does not hydrate rows, so the second is a warning about |
| 49 | +a path we do not take — not a regression we shipped. |
| 50 | + |
| 51 | +### The Panama path is byte-identical across the object model |
| 52 | + |
| 53 | +| FFM measurement | record arm | value arm | |
| 54 | +|---|---|---| |
| 55 | +| Java bytes allocated per query (warm) | 712 B | **712 B** | |
| 56 | +| Java objects per row | 0 | **0** | |
| 57 | +| native lane bytes / mask bytes | 1.00 MiB / 8.0 KiB | **identical** | |
| 58 | + |
| 59 | +**That is the integration's central invariant, measured rather than asserted:** |
| 60 | +Valhalla changes what the *vocabulary* costs; it does not touch the membrane. |
| 61 | +No second graph representation appeared, and no row hydration was introduced |
| 62 | +to "use Valhalla". |
| 63 | + |
| 64 | +### The lab's re-scope removed a confound it had carried since it was built |
| 65 | + |
| 66 | +The old A/B was `(record, JDK 26)` vs `(value record, JDK 27 EA)` — **two |
| 67 | +variables**. Both arms now run on JDK 28, so the object model is the only |
| 68 | +difference and the diff is attributable. The lab stops being |
| 69 | +present-vs-future and becomes a VM/representation probe suite over the real |
| 70 | +production vocabulary. |
| 71 | + |
| 72 | +Two things the re-scope forced, both real: |
| 73 | +- **Preview marking is transitive.** Production classfiles are preview-marked, |
| 74 | + so the `record` arm *also* runs `--enable-preview`. It is a record arm on a |
| 75 | + preview-enabled JVM, not a preview-free arm; the "stable" label survives only |
| 76 | + as a path. |
| 77 | +- **A JDK 28 internal-API change.** `jdk.internal.value.ValueClass.isFlatArray` |
| 78 | + narrowed from `Object` to `Object[]` since 27-jep401ea3; the flattening probe |
| 79 | + was adapted (a non-`Object[]` argument answers `NOT-FLAT` without reaching |
| 80 | + the internal API). |
| 81 | + |
| 82 | +### The `RangeOutOfBounds` fix is compatibility plumbing, with a falsifier |
| 83 | + |
| 84 | +`mask_risc::ExecError` gained `RangeOutOfBounds` with lance-graph's |
| 85 | +`Pred::Range` work and `exec_error_to_status` matches exhaustively, so |
| 86 | +`lgj-abi` **did not compile against lance-graph `main`**. Mapped into the |
| 87 | +documented *"bug in THIS file"* family — no new public status, no ABI bump — |
| 88 | +and backed by `plan_lower::range_falsifier::no_opcode_lowers_to_pred_range`, |
| 89 | +which sweeps every opcode through the real `lower_plan` and reads every |
| 90 | +emitted predicate. **Disable-verified red-then-green:** pointing `LGJ_OP_EQ_U32` |
| 91 | +at `Pred::Range` fails it with the intended message; restoring passes. When LGJ |
| 92 | +gains a Range lowering this goes red and forces a deliberate caller-visible |
| 93 | +mapping instead of inheriting `LGJ_ERR_ALLOCATION_FAILED`, which would then be |
| 94 | +a lie. |
| 95 | + |
| 96 | +### What could not be measured here, stated rather than implied |
| 97 | + |
| 98 | +`bench/` cannot run in this container — `bench/lib` does not exist, so the JMH |
| 99 | +jars are absent. The crossing-count and per-call allocation figures in |
| 100 | +`bench/RESULTS.md` are **historical JDK 26 measurements, correct when taken**, |
| 101 | +and are deliberately left untouched; they have NOT been re-taken on JDK 28. |
| 102 | +`gate-run.sh` carries the new pin and flags so the re-run is one command once |
| 103 | +the jars are present. |
| 104 | + |
| 105 | +## E-THE-VALHALLA-FLIP-COST-ONE-WORD-SIX-TIMES-AND-THE-GATES-DID-NOT-MOVE-1 (2026-09-19) |
| 106 | + |
| 107 | +**Finding, measured end to end today.** The P0 mandate (JDK 28 + Valhalla + |
| 108 | +Panama) is implemented. `/opt/jdks/jdk-28+16`, Temurin `28+16-ea`. |
| 109 | + |
| 110 | +| arm | result | |
| 111 | +|---|---| |
| 112 | +| JDK 28, sources unchanged, no preview | compiles; **409 checks, 0 failed** | |
| 113 | +| JDK 28, six types `public value record`, `--enable-preview` | compiles; **409 checks, 0 failed** | |
| 114 | +| `isValue()` on all six at runtime | **true**; `new LaneId(7) == new LaneId(7)` → true | |
| 115 | +| FFM without any preview flag | final; `SysVx64Linker`; `MemorySegment` round-trip clean | |
| 116 | + |
| 117 | +**The lab's central claim is now demonstrated on production sources:** the |
| 118 | +migration is one word, six times, and the gate suite does not move. It was |
| 119 | +previously proven only on `valhalla-lab`'s parallel vocabulary. |
| 120 | + |
| 121 | +**A cross-repo break found on the way, and it was blocking everything.** |
| 122 | +`lgj-abi` did not compile against current lance-graph `main`: |
| 123 | +`mask_risc::ExecError` gained `RangeOutOfBounds { lo, hi, n_rows }` with the |
| 124 | +`Pred::Range` work, and `exports.rs`'s `exec_error_to_status` maps `ExecError` |
| 125 | +**exhaustively**. Fixed by joining the documented *"would be a bug in THIS |
| 126 | +file, not in a caller's plan"* family (`LGJ_ERR_ALLOCATION_FAILED`) — the ABI |
| 127 | +lowering emits no `Pred::Range`, exactly as it emits no sum terminal and no |
| 128 | +blend — and the doc comment's enumeration was extended so the list stays |
| 129 | +exhaustive in prose too. **An exhaustive match across a repo boundary is a |
| 130 | +tripwire, not a safety net:** it converts an upstream additive change into a |
| 131 | +downstream build failure, and nothing in either repo's CI noticed, because no |
| 132 | +CI job here compiles the native crate. |
| 133 | + |
| 134 | +**Two false failures I nearly reported, both mine, both caught by reading.** |
| 135 | +The flipped suite showed `1 FAILED` twice. Both times it was |
| 136 | +`DoctrineFenceTest` refusing to run — it requires `java/src/main/java` AND |
| 137 | +`native/lgj-abi/src` relative to CWD, and I had run the flipped build from a |
| 138 | +scratch directory that had neither. The fence is *correct*: its first check is |
| 139 | +*"a fence that cannot find its corpus must fail, not skip."* Running the flip |
| 140 | +in the real tree (committed first, restored with `git checkout`) gave the true |
| 141 | +result. **A harness artifact and a regression look identical in a summary |
| 142 | +line** — this is the ruff trap "a disable that does not apply is |
| 143 | +indistinguishable from a guard that is not load-bearing", met from the other |
| 144 | +direction. |
| 145 | + |
| 146 | +## E-VALHALLA-IS-THE-STORAGE-MEMBRANE-AND-IT-IS-MANDATORY-1 (2026-09-19) |
| 147 | + |
| 148 | +**Operator ruling.** *LGJ MUST use JDK 28 and MUST use Valhalla and Panama.* |
| 149 | +Recorded as canon in `CLAUDE.md` § P0; the superseded decision is struck in |
| 150 | +place in `.claude/knowledge/jdk-toolchain-facts.md`. |
| 151 | + |
| 152 | +**The violation it corrects, in my own words this session:** *"Production lgj |
| 153 | +does not depend on Valhalla at all."* That sentence is wrong twice. It is |
| 154 | +wrong about the design — the vocabulary types are written identity-free |
| 155 | +precisely so the same source compiles as `value record` with one word changed, |
| 156 | +which is a dependency in shape that `valhalla-lab/docs/three-truths.md` |
| 157 | +already measured (every behaviour the API uses is identical across both object |
| 158 | +models; the rows that differ — `==`, identity hash, `synchronized`, |
| 159 | +null-restricted arrays — are exactly the ones the API never touches). And it |
| 160 | +is wrong about the architecture: **Panama carries the verb, Valhalla carries |
| 161 | +the noun, lance-graph owns the reality.** Valhalla is the STORAGE MEMBRANE, |
| 162 | +never the storage owner. Drop it and Java can still reach the bytes, but it |
| 163 | +reaches them as offset/stride/segment/handle — it has not adopted the |
| 164 | +substrate's storage vocabulary, and zero-copy stops being a programming model |
| 165 | +and goes back to being an FFI trick. |
| 166 | + |
| 167 | +**How the error got in, which is the transferable part.** The knowledge doc |
| 168 | +called the GA-JDK target *"a real, deliberate strength of the design."* A |
| 169 | +release constraint dressed as a virtue is an architectural claim, and a later |
| 170 | +session (this one) read it back as one. A toolchain doc may record what a |
| 171 | +toolchain CAN do; the moment it says what the design SHOULD therefore be, it |
| 172 | +has started legislating outside its evidence. |
| 173 | + |
| 174 | +**Unaffected by the ruling, stated so the next session does not over-apply |
| 175 | +it:** E4 — the Vector API remains a lab arm; JDK 28 finalizing it gives Java |
| 176 | +no backend, because Java has no backends (kernels stay in `ndarray::simd`). |
| 177 | +`--enable-preview` remains classfile-poisoning, so the flag posture is |
| 178 | +repo-wide and uniform, never mixed. Value classes make a DESCRIPTOR cheap; |
| 179 | +they never make a population crossable. |
| 180 | + |
| 181 | +**Status: MANDATED, NOT IMPLEMENTED.** No JDK 28 in this container |
| 182 | +(`/opt/jdks` absent, system `java` 21.0.10) and `jdk.java.net` is proxy-blocked, |
| 183 | +so the migration is filed, not done: `ISS-LGJ-TOOLCHAIN-MUST-BE-JDK28-VALHALLA-PANAMA`. |
| 184 | + |
1 | 185 | ## E-THE-PIN-THAT-CLAIMED-ONE-PLACE-CREATED-A-SECOND-1 (2026-09-05) |
2 | 186 |
|
3 | 187 | **Finding.** The first CI gate this repo ever had went red on its first |
|
0 commit comments