The shape matrix: enumerate what crosses, by running the generators (#198) - #400
Conversation
…198) Which Rust shapes survive the trip to C or to Kotlin — `Option<&T>`, `Vec<Option<T>>`, a payload-carrying enum inside a struct — was knowable only by reading the generators, or by writing the Rust and finding out. So a gap is discovered by hitting it, a reviewer cannot tell whether a change closed a hole or moved it, and a regression that quietly drops a shape looks like nothing. `examples/shape-matrix` enumerates (shape × position × target), synthesizes a fixture for each, and runs it through the **real** generators. There is no table of expected support anywhere in it: a second authority on legality can disagree with the first, and then neither is trustworthy. **The type axis comes from the model, not from a list.** `tag_of` is an exhaustive match over `TypeKind`, so a new accepted form stops this crate compiling; `every_type_form_is_covered` then fails until the form also has a fixture. Both halves are needed — a form could otherwise be tagged and never enumerated. That is precisely how the hand-written grammar this replaces came to name 8 of the 15 forms, and the same drift shipped once before: a spec listing `Vec | [T] | Cow<[T]>` as the sequence forms let `[u8; 16]` degrade to an opaque leaf until something needed it (#190). Verified to discriminate — deleting the one `Box<u64>` fixture fails the test naming `Box<T>`. `children` is a second exhaustive match rather than `TypeRef::walk`, which descends *through* transparent wrappers by design: `Box<Vec<T>>` reaches `T` without ever yielding the `Vec`, and coverage has to see every form written. **Declarations are a separate axis**, because the model records a field's type as "a named type called `Rec`" and stops. A cell about a struct field emits a struct; one about a payload emits an enum. **A panic is an outcome, not a crash.** Unsupported shapes are reported both ways today — a named rejection, or a `panic!` — so the runner catches unwinds and the table distinguishes them. That column is #191's evidence, per cell. **The declaration policy is the documented one**, or the table would measure this harness: C gets `.panic()` (the probe returns no `Result`, and a C binding with fallible inputs must say what happens), and a `Result`'s error type is declared the way each target documents — C `.opaque_error` with its message function, JNI a handle whose declared return fields feed the generated error handler. Without those two, 46 C cells reported this crate's omissions. `REPORT.md` is committed and already covered by `regen-check.sh`, which CI runs. That is the primary gate: the generators both decide legality and write the report, so "fails until new cells are classified" is vacuous alone — a regression flipping a working cell to `rejected` would be recorded as a successful classification. A committed report makes every moved answer a reviewed diff. What it already shows, from 288 cells: a sum in a `Result` error position fails in both targets (#162, open); a sum as a sum's payload panics in JNI; `Vec` of handles panics rather than reporting; and C answers 35 cells by panic where JNI answers 10. What this does **not** claim: `plan` means generation succeeded. Nothing here compiles, links or runs the result — `ToolchainCompiled` and `RuntimeExercised` are separate states and are not collected yet. Part of #198, tracked by #399.
CI runs rustfmt with `imports_granularity=Crate,group_imports=StdExternalCrate`; a plain `cargo fmt` does not.
The type axis could not drift — `tag_of` is exhaustive over `TypeKind`. The declaration axis could: a new way to declare a type would leave the matrix using the old one, cells that had started passing would keep reporting `rejected` or `panic`, and nothing would fail. An additive API change was the one class of change this crate could not notice. Half of it closes for free. `prebindgen_jni::ClassDecl` is a closed, public four-variant enum, so `kind_of` is exhaustive over it and a fifth class kind is a compile error here — the same chain the type axis has, one axis over. The round-trip test then stops the two drifting the other way, by proving each kind builds the declaration it names, and `every_class_kind_is_exercised` fails if a kind is enumerated but never declared by any cell. The harness therefore speaks the JNI vocabulary, and `to_c` translates it. That direction is deliberate: the C build-script API is the older surface — eleven declarator methods, no type unifying them — and it is to be reworked in the JNI style (#192, #399). Shaping the harness around C's current spelling would bake a quirk of the API being replaced into the thing meant to outlive it, so exactly one function knows that spelling and is expected to be rewritten wholesale. `Kind` also stops conflating two questions: a `Result`'s error type is not a fifth class kind but a *role* a declared type plays, so `Decl` carries the class kind and the error role separately. Answer-preserving: `REPORT.md` gains the declaration-kind coverage table and not one cell changed. Part of #198, tracked by #399.
Follow-up: the declaration axis is gated tooThe gap in the first commit was additive API change. The type axis could not drift — Half of it closed for free. The harness speaks the JNI vocabulary and Also split two questions that were conflated: a Answer-preserving, which is the property worth checking on a refactor of a measuring instrument: Remaining ungated: a new C declarator, and new options on either builder ( |
|
Retargeted to |
First step of #198, tracked by #399.
What it does
examples/shape-matrixenumerates (shape × position × target), synthesizes a Rust fixture for each cell, and pushes it through the real generators —prebindgen-candprebindgen-jni, declared the way a build script would. It records the answer that came back. 288 cells today.There is deliberately no table of expected support anywhere in the crate. A second authority on legality can disagree with the first, and then neither is trustworthy.
The gate
The type axis is derived from the model, not listed.
tag_ofis an exhaustivematchoverTypeKind, so adding an accepted Rust form stops this crate compiling;every_type_form_is_coveredthen fails until the form also has a fixture.Both halves are load-bearing — a form could otherwise be tagged and never enumerated, which is exactly how the hand-written grammar this replaces came to name 8 of the 15 forms. And the same drift has shipped once for real: a spec listing
Vec | [T] | Cow<[T]>as the sequence forms let[u8; 16]degrade silently to an opaque leaf until something finally needed it (#190).Verified to discriminate, per this issue's own rule about #175: deleting the single
Box<u64>fixture fails the test, namingBox<T>.childrenis a second exhaustive match rather thanTypeRef::walk, which descends through transparent wrappers on purpose:Box<Vec<T>>reachesTwithout ever yielding theVecnode, and coverage has to see every form the fixture writes.Two things found by building it
A panic is an outcome. Unsupported shapes are reported two different ways today — a named rejection, or a
panic!. The runner catches unwinds so the table can distinguish them, and that column is #191's evidence per cell rather than in prose.The declaration policy has to be the documented one, or the table measures the harness. Two corrections during development, both visible in the numbers:
.panic()on the probe function — it returns noResult, and a C binding whose inputs can fail must say what happens. Without it, 11 cells reported "add.panic()after its.function(...)declaration".Result's error type must be declared the way each target documents it: C.opaque_errorplus the message function, JNI a handle whose declared return fields feed the generated error handler. Without it, every fallible cell reported an unresolved type.Together those were 46 C cells of my own omissions before the fix; 35 remain, and they are the generator's.
What it already shows
Result<u64, Sum>— a payload-carrying enum in an error position — fails in both targets. That is jnigen: a sum in a Result error position is delivered as a string, not the sealed hierarchy #162, open since the sum-type work, now a tracked cell instead of a note.Vec<Handle>/Vec<&Handle>answer by panic, in both targets, in three of four positions.Drawing.shapeinexample-cbindgen). That asymmetry was not written down anywhere.The regression gate
REPORT.mdis committed and added toregen-check.sh, which CI already runs — no workflow change needed.This is the primary gate and it is not decoration: the generators both decide legality and produce the report, so "the build fails until new cells are classified" would be vacuous on its own — a regression that flips a working cell to
rejectedwould be recorded as a successful classification. A committed report makes every moved answer a reviewed diff. Output verified byte-identical between debug and release profiles, and it contains no paths or timestamps.What this does not claim
planmeans generation succeeded. Nothing here compiles, links or runs the result.ToolchainCompiledandRuntimeExercisedare the two states that need a toolchain and a runtime; neither is collected yet, and the report says so in its own header.Not in this PR
From #198's list: the receipt mechanism for the two higher states, the minimum-guarantees table, multi-parameter aliasing fixtures, and the adapter-policy axis (each declared type is presented one canonical way here). The plan-level invariants wait on the plan work in #192/#193.
Checks
cargo test -p shape-matrix(6 tests),cargo clippy -p shape-matrix --all-targetsclean,cargo fmt, andexamples/regen-check.shpasses with every other committed artifact byte-identical.