Skip to content

The shape matrix: enumerate what crosses, by running the generators (#198) - #400

Merged
milyin merged 3 commits into
shape-coveragefrom
shape-matrix
Aug 13, 2026
Merged

The shape matrix: enumerate what crosses, by running the generators (#198)#400
milyin merged 3 commits into
shape-coveragefrom
shape-matrix

Conversation

@milyin

@milyin milyin commented Aug 13, 2026

Copy link
Copy Markdown
Owner

First step of #198, tracked by #399.

What it does

examples/shape-matrix enumerates (shape × position × target), synthesizes a Rust fixture for each cell, and pushes it through the real generators — prebindgen-c and prebindgen-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.

cargo run -p shape-matrix     # rewrites REPORT.md
cargo test -p shape-matrix    # coverage gate + report freshness

The gate

The type axis is derived from the model, not listed. tag_of is an exhaustive match over TypeKind, so adding an accepted Rust form stops this crate compiling; every_type_form_is_covered then 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, naming Box<T>.

no fixture writes these accepted type forms: ["Box<T>"]
Add a shape to `corpus::SHAPES` that spells one.

children is a second exhaustive match rather than TypeRef::walk, which descends through transparent wrappers on purpose: Box<Vec<T>> reaches T without ever yielding the Vec node, 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:

  • C needs .panic() on the probe function — it returns no Result, and a C binding whose inputs can fail must say what happens. Without it, 11 cells reported "add .panic() after its .function(...) declaration".
  • A Result's error type must be declared the way each target documents it: C .opaque_error plus 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

Target plan rejected panic n/a
C 50 41 35 18
Kotlin/JNI 82 34 10 18

The regression gate

REPORT.md is committed and added to regen-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 rejected would 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

plan means generation succeeded. Nothing here compiles, links or runs the result. ToolchainCompiled and RuntimeExercised are 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-targets clean, cargo fmt, and examples/regen-check.sh passes with every other committed artifact byte-identical.

…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.
milyin added 2 commits August 13, 2026 22:13
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.
@milyin

milyin commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Follow-up: the declaration axis is gated too

The gap in the first commit was additive API change. The type axis could not drift — tag_of is exhaustive over TypeKind — but 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, and nothing would fail.

Half of it closed 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 in this crate. Two tests complete the chain: a round trip proving each kind builds the declaration it names (so the two cannot drift the other way), and every_class_kind_is_exercised, which fails if a kind is enumerated but no cell ever declares a type as one.

The harness speaks the JNI vocabulary and to_c translates. That direction is deliberate rather than incidental: 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 when the rework lands. I did not add a closed kind enum to prebindgen-c as I'd floated earlier — no point hardening an API that is on its way out.

Also split two questions that were conflated: a Result's error type is not a fifth class kind but a role a declared type plays, so Decl carries class kind and error role separately.

Answer-preserving, which is the property worth checking on a refactor of a measuring instrument: REPORT.md gains the declaration-kind coverage table and not one cell changed — 11 insertions, 0 deletions.

Remaining ungated: a new C declarator, and new options on either builder (.assume_c_field_validity() and friends). Both are the adapter-policy axis, which #198 tracks separately.

@milyin

milyin commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Retargeted to shape-coverage (#402), the integration branch for this work — this PR is step 1 of six, and the branch keeps one reviewable thread for the rest. The step map lives in docs/shape-matrix.md on that branch; main gets it all at once when the steps are done.

@milyin
milyin merged commit 7208923 into shape-coverage Aug 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant