diff --git a/docs/shape-matrix.md b/docs/shape-matrix.md index 41fb4e2b..fd9383da 100644 --- a/docs/shape-matrix.md +++ b/docs/shape-matrix.md @@ -44,7 +44,8 @@ not a rewrite. | # | Step | State | |---|---|---| | 1 | The enumerator, both targets, committed report + regen gate | **landed** ([#400](https://github.com/milyin/prebindgen/pull/400)) | -| 2 | Receipts: `ToolchainCompiled` and `RuntimeExercised` | not started | +| 2 | Receipts: rustc accepts the emitted Rust | **landed** ([#403](https://github.com/milyin/prebindgen/pull/403)) | +| 2b | The rest of `ToolchainCompiled`, and `RuntimeExercised` | not started | | 3 | The minimum-guarantees table | not started | | 4 | Multi-parameter aliasing fixtures | not started | | 5 | The adapter-policy axis | JNI half ready; C half blocked | @@ -75,20 +76,29 @@ until new cells are classified" would be vacuous on its own — a regression tha flips a working cell to `rejected` would be recorded as a successful classification. -### 2. Receipts — the two states that need a toolchain +### 2. Receipts — rustc — landed -`plan` means generation succeeded. Nothing compiles, links or runs the result -yet, so the two strongest states are uncollected. +Every cell that produced Rust is compiled, and the state is a **receipt**: the +cell is written to its own file, the crate is checked in one pass, and each +diagnostic is attributed back by the file rustc names. Nothing maps a cell to a +fixture by hand — that mapping is what let #175's test pass without creating its +own precondition. -The rule from #198 stands: these states are **derived from mechanical receipts, -never declared**. A hand-written *cell → fixture-name* mapping can claim coverage -for a fixture that never touches the cell — the #175 failure exactly. A fixture -emits its cell id only *after* the relevant assertion has executed, and a cell -with no receipt stays `PlanSupported` whatever any table claims. +Compiler messages stay out of the committed report: they vary by toolchain, and +the report has to be identical on every one that builds it. -`examples/emitcheck` is the precedent for the compile half: it exists so rustc -judges emitted Rust. The runtime half rides the JVM covertest and the C smoke -tests. +Turning the compiler on immediately found ten cells whose generated Rust does not +compile, and three defects in this harness — each of which had been reporting a +confident wrong answer. That is the argument for this step in one sentence: +`plan` was worth less than it looked. + +### 2b. The rest of the toolchain + +`cbindgen` does not run, so a C cell that compiles has not been shown to produce +a valid header; the Kotlin compiler does not run either. `RuntimeExercised` needs +the JVM covertest and the C smoke tests, and the same receipt rule applies — a +fixture emits its cell id only *after* the relevant assertion has executed, and a +cell with no receipt keeps the weaker state whatever any table claims. ### 3. The minimum-guarantees table diff --git a/examples/shape-matrix/REPORT.md b/examples/shape-matrix/REPORT.md index cd6e82cb..7f6af078 100644 --- a/examples/shape-matrix/REPORT.md +++ b/examples/shape-matrix/REPORT.md @@ -10,62 +10,69 @@ never by a hand-written list of what is supposed to work. See | Cell | Meaning | |---|---| -| `plan` | generation succeeded. Nothing here compiled, linked or ran the result. | +| `rustc` | the generator produced Rust **and rustc accepted it**. Neither cbindgen nor the Kotlin compiler has seen it. | +| **`bad rust`** | the generator produced Rust that does not compile. Green unit tests can coexist with this — that is why the check exists. | +| `plan` | generation succeeded and the compile check did not run (see the run's stderr). | | `rejected` | the generator refused the shape **and said why**. The intended outcome for anything unsupported. | | **`panic`** | the generator refused it without a diagnosis — the user gets a stack trace instead of a sentence ([#191](https://github.com/milyin/prebindgen/issues/191)). | | `—` | the placement is not legal Rust, so there is nothing to ask. | -`plan` is evidence, not a guarantee: `ToolchainCompiled` and `RuntimeExercised` -are the states that require a compiler and a runtime, and neither is collected -yet. +`rustc` is evidence, not a guarantee. It is the Rust half of +`ToolchainCompiled`: the emitted Rust type-checks against the fixture the way a +binding crate compiles it. The C header, the Kotlin classes and every +`RuntimeExercised` cell still require toolchains this stage does not run. + +Compiler messages are deliberately **not** in this file — they vary by +toolchain, and the report has to be identical on every one that builds it. A +failing cell prints its diagnostics on the run's stderr. ## Summary -| Target | plan | rejected | panic | n/a | -|---|---:|---:|---:|---:| -| C | 50 | 41 | 35 | 18 | -| Kotlin/JNI | 82 | 34 | 10 | 18 | +| Target | rustc | bad rust | plan only | rejected | panic | n/a | +|---|---:|---:|---:|---:|---:|---:| +| C | 45 | 5 | 0 | 41 | 31 | 22 | +| Kotlin/JNI | 75 | 5 | 0 | 34 | 8 | 22 | ## Position: parameter | Shape | Rust | C | Kotlin/JNI | |---|---|---|---| -| `scalar` | `u64` | plan | plan | -| `bool` | `bool` | plan | plan | +| `scalar` | `u64` | rustc | rustc | +| `bool` | `bool` | rustc | rustc | | `unit` | `()` | rejected | rejected | -| `string` | `String` | plan | plan | -| `str_ref` | `&str` | plan | plan | -| `record` | `Rec` | plan | plan | -| `handle` | `Handle` | plan | plan | -| `sum` | `Sum` | plan | plan | -| `unit_enum` | `Mode` | plan | plan | -| `shared_ref` | `&Rec` | rejected | plan | -| `exclusive_ref` | `&mut Rec` | rejected | plan | -| `handle_ref` | `&Handle` | plan | plan | -| `out_param` | `&mut MaybeUninit` | rejected | plan | -| `opt_scalar` | `Option` | plan | plan | +| `string` | `String` | rustc | rustc | +| `str_ref` | `&str` | rustc | rustc | +| `record` | `Rec` | rustc | rustc | +| `handle` | `Handle` | rustc | rustc | +| `sum` | `Sum` | rustc | rustc | +| `unit_enum` | `Mode` | rustc | rustc | +| `shared_ref` | `&Rec` | rejected | rustc | +| `exclusive_ref` | `&mut Rec` | rejected | **bad rust** | +| `handle_ref` | `&Handle` | rustc | rustc | +| `out_param` | `&mut MaybeUninit` | rejected | **bad rust** | +| `opt_scalar` | `Option` | rustc | rustc | | `vec_scalar` | `Vec` | rejected | rejected | -| `slice_scalar` | `&[u64]` | plan | rejected | +| `slice_scalar` | `&[u64]` | rustc | rejected | | `slice_mut_scalar` | `&mut [u64]` | rejected | rejected | -| `array_scalar` | `[u8; 4]` | rejected | plan | +| `array_scalar` | `[u8; 4]` | rejected | rustc | | `boxed_scalar` | `Box` | rejected | rejected | -| `cow_str` | `Cow<'static, str>` | rejected | plan | -| `opt_record` | `Option` | plan | plan | -| `opt_handle` | `Option` | plan | plan | -| `opt_ref` | `Option<&Handle>` | plan | plan | -| `vec_record` | `Vec` | rejected | plan | +| `cow_str` | `Cow<'static, str>` | rejected | **bad rust** | +| `opt_record` | `Option` | **bad rust** | rustc | +| `opt_handle` | `Option` | rustc | rustc | +| `opt_ref` | `Option<&Handle>` | rustc | rustc | +| `vec_record` | `Vec` | rejected | rustc | | `vec_handle` | `Vec` | rejected | **panic** | | `vec_ref` | `Vec<&Handle>` | rejected | **panic** | -| `vec_sum` | `Vec` | rejected | plan | -| `opt_sum` | `Option` | plan | plan | +| `vec_sum` | `Vec` | rejected | rustc | +| `opt_sum` | `Option` | **bad rust** | rustc | | `array_record` | `[Rec; 2]` | rejected | rejected | | `opt_vec` | `Option>` | rejected | rejected | -| `vec_opt` | `Vec>` | rejected | plan | +| `vec_opt` | `Vec>` | rejected | rustc | | `result_scalar` | `Result` | rejected | rejected | | `result_handle` | `Result` | rejected | rejected | | `result_sum_err` | `Result` | rejected | rejected | -| `callback` | `impl Fn(u64) + Send + Sync + 'static` | rejected | plan | -| `callback_handle` | `impl Fn(Handle) + Send + Sync + 'static` | rejected | plan | +| `callback` | `impl Fn(u64) + Send + Sync + 'static` | rejected | rustc | +| `callback_handle` | `impl Fn(Handle) + Send + Sync + 'static` | rejected | rustc |
What the generators said @@ -109,55 +116,55 @@ yet. | Shape | Rust | C | Kotlin/JNI | |---|---|---|---| -| `scalar` | `u64` | plan | plan | -| `bool` | `bool` | plan | plan | -| `unit` | `()` | plan | plan | -| `string` | `String` | plan | plan | -| `str_ref` | `&str` | rejected | plan | -| `record` | `Rec` | plan | plan | -| `handle` | `Handle` | plan | plan | -| `sum` | `Sum` | plan | plan | -| `unit_enum` | `Mode` | plan | plan | +| `scalar` | `u64` | rustc | rustc | +| `bool` | `bool` | rustc | rustc | +| `unit` | `()` | rustc | rustc | +| `string` | `String` | rustc | rustc | +| `str_ref` | `&str` | rejected | rustc | +| `record` | `Rec` | rustc | rustc | +| `handle` | `Handle` | rustc | rustc | +| `sum` | `Sum` | rustc | rustc | +| `unit_enum` | `Mode` | rustc | rustc | | `shared_ref` | `&Rec` | rejected | rejected | | `exclusive_ref` | `&mut Rec` | rejected | rejected | -| `handle_ref` | `&Handle` | plan | plan | +| `handle_ref` | `&Handle` | rustc | rustc | | `out_param` | `&mut MaybeUninit` | rejected | rejected | -| `opt_scalar` | `Option` | plan | plan | -| `vec_scalar` | `Vec` | plan | plan | -| `slice_scalar` | `&[u64]` | plan | rejected | +| `opt_scalar` | `Option` | rustc | rustc | +| `vec_scalar` | `Vec` | rustc | rustc | +| `slice_scalar` | `&[u64]` | **bad rust** | rejected | | `slice_mut_scalar` | `&mut [u64]` | rejected | rejected | -| `array_scalar` | `[u8; 4]` | rejected | plan | +| `array_scalar` | `[u8; 4]` | rejected | rustc | | `boxed_scalar` | `Box` | rejected | rejected | -| `cow_str` | `Cow<'static, str>` | rejected | plan | -| `opt_record` | `Option` | plan | plan | -| `opt_handle` | `Option` | plan | plan | -| `opt_ref` | `Option<&Handle>` | plan | plan | -| `vec_record` | `Vec` | plan | plan | -| `vec_handle` | `Vec` | plan | plan | -| `vec_ref` | `Vec<&Handle>` | plan | plan | -| `vec_sum` | `Vec` | plan | plan | -| `opt_sum` | `Option` | plan | plan | +| `cow_str` | `Cow<'static, str>` | rejected | rustc | +| `opt_record` | `Option` | rustc | rustc | +| `opt_handle` | `Option` | rustc | rustc | +| `opt_ref` | `Option<&Handle>` | rustc | rustc | +| `vec_record` | `Vec` | rustc | rustc | +| `vec_handle` | `Vec` | rustc | rustc | +| `vec_ref` | `Vec<&Handle>` | **bad rust** | rustc | +| `vec_sum` | `Vec` | rustc | rustc | +| `opt_sum` | `Option` | rustc | rustc | | `array_record` | `[Rec; 2]` | rejected | rejected | -| `opt_vec` | `Option>` | plan | plan | -| `vec_opt` | `Vec>` | **panic** | plan | -| `result_scalar` | `Result` | plan | plan | -| `result_handle` | `Result` | plan | plan | +| `opt_vec` | `Option>` | rustc | rustc | +| `vec_opt` | `Vec>` | **panic** | rustc | +| `result_scalar` | `Result` | rustc | rustc | +| `result_handle` | `Result` | rustc | rustc | | `result_sum_err` | `Result` | **panic** | rejected | | `callback` | `impl Fn(u64) + Send + Sync + 'static` | rejected | rejected | | `callback_handle` | `impl Fn(Handle) + Send + Sync + 'static` | rejected | rejected |
What the generators said -- `str_ref` / C: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& str` — error: unresolved prebindgen output type `str` -- `shared_ref` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& Rec` -- `shared_ref` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& Rec` -- `exclusive_ref` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut Rec` -- `exclusive_ref` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut Rec` -- `out_param` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut MaybeUninit < u64 >` -- `out_param` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut MaybeUninit < u64 >` -- `slice_scalar` / Kotlin/JNI: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& [u64]` — error: unresolved prebindgen output type `[u64]` -- `slice_mut_scalar` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut [u64]` -- `slice_mut_scalar` / Kotlin/JNI: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& mut [u64]` — error: unresolved prebindgen output type `[u64]` +- `str_ref` / C: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static str` — error: unresolved prebindgen output type `str` +- `shared_ref` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static Rec` +- `shared_ref` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static Rec` +- `exclusive_ref` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut Rec` +- `exclusive_ref` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut Rec` +- `out_param` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut MaybeUninit < u64 >` +- `out_param` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut MaybeUninit < u64 >` +- `slice_scalar` / Kotlin/JNI: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static [u64]` — error: unresolved prebindgen output type `[u64]` +- `slice_mut_scalar` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut [u64]` +- `slice_mut_scalar` / Kotlin/JNI: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `& 'static mut [u64]` — error: unresolved prebindgen output type `[u64]` - `array_scalar` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `[u8 ; 4]` - `boxed_scalar` / C: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `Box < u64 >` - `boxed_scalar` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `Box < u64 >` @@ -178,34 +185,34 @@ yet. | Shape | Rust | C | Kotlin/JNI | |---|---|---|---| -| `scalar` | `u64` | plan | plan | -| `bool` | `bool` | plan | plan | +| `scalar` | `u64` | rustc | rustc | +| `bool` | `bool` | rustc | rustc | | `unit` | `()` | **panic** | rejected | -| `string` | `String` | plan | plan | +| `string` | `String` | rustc | rustc | | `str_ref` | `&str` | — | — | -| `record` | `Rec` | **panic** | plan | -| `handle` | `Handle` | **panic** | plan | -| `sum` | `Sum` | plan | plan | -| `unit_enum` | `Mode` | **panic** | plan | +| `record` | `Rec` | **panic** | rustc | +| `handle` | `Handle` | **panic** | rustc | +| `sum` | `Sum` | rustc | rustc | +| `unit_enum` | `Mode` | **panic** | rustc | | `shared_ref` | `&Rec` | — | — | | `exclusive_ref` | `&mut Rec` | — | — | | `handle_ref` | `&Handle` | — | — | | `out_param` | `&mut MaybeUninit` | — | — | -| `opt_scalar` | `Option` | **panic** | plan | +| `opt_scalar` | `Option` | **panic** | rustc | | `vec_scalar` | `Vec` | **panic** | rejected | | `slice_scalar` | `&[u64]` | — | — | | `slice_mut_scalar` | `&mut [u64]` | — | — | -| `array_scalar` | `[u8; 4]` | **panic** | plan | +| `array_scalar` | `[u8; 4]` | **panic** | rustc | | `boxed_scalar` | `Box` | **panic** | rejected | -| `cow_str` | `Cow<'static, str>` | **panic** | plan | -| `opt_record` | `Option` | **panic** | plan | -| `opt_handle` | `Option` | **panic** | plan | -| `opt_ref` | `Option<&Handle>` | **panic** | plan | -| `vec_record` | `Vec` | **panic** | plan | +| `cow_str` | `Cow<'static, str>` | **panic** | **bad rust** | +| `opt_record` | `Option` | **panic** | rustc | +| `opt_handle` | `Option` | **panic** | rustc | +| `opt_ref` | `Option<&Handle>` | — | — | +| `vec_record` | `Vec` | **panic** | rustc | | `vec_handle` | `Vec` | **panic** | **panic** | -| `vec_ref` | `Vec<&Handle>` | **panic** | **panic** | +| `vec_ref` | `Vec<&Handle>` | — | — | | `vec_sum` | `Vec` | **panic** | **panic** | -| `opt_sum` | `Option` | **panic** | plan | +| `opt_sum` | `Option` | **panic** | rustc | | `array_record` | `[Rec; 2]` | **panic** | rejected | | `opt_vec` | `Option>` | **panic** | rejected | | `vec_opt` | `Vec>` | **panic** | **panic** | @@ -231,12 +238,9 @@ yet. - `cow_str` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Cow < 'static , str >` - `opt_record` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Option < Rec >` - `opt_handle` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Option < Handle >` -- `opt_ref` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Option < & Handle >` - `vec_record` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Vec < Rec >` - `vec_handle` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Vec < Handle >` - `vec_handle` / Kotlin/JNI: JniGen: `Vec` is unsupported — its elements would be closeable native handles (jlong) the JVM must free individually. Expose a per-element accessor instead of returning a `Vec` of handles. -- `vec_ref` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Vec < & Handle >` -- `vec_ref` / Kotlin/JNI: JniGen: `Vec<& Handle>` is unsupported — its elements would be closeable native handles (jlong) the JVM must free individually. Expose a per-element accessor instead of returning a `Vec` of handles. - `vec_sum` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Vec < Sum >` - `vec_sum` / Kotlin/JNI: fromParts bridge: `Vec` sealed-class field (`Probe.v`) is not supported (variable arity) - `opt_sum` / C: Cbindgen: field `v` of data struct `Probe` has unsupported type `Option < Sum >` @@ -259,40 +263,40 @@ yet. | Shape | Rust | C | Kotlin/JNI | |---|---|---|---| -| `scalar` | `u64` | plan | plan | -| `bool` | `bool` | plan | plan | +| `scalar` | `u64` | rustc | rustc | +| `bool` | `bool` | rustc | rustc | | `unit` | `()` | rejected | **panic** | -| `string` | `String` | plan | plan | +| `string` | `String` | rustc | rustc | | `str_ref` | `&str` | — | — | -| `record` | `Rec` | plan | plan | -| `handle` | `Handle` | plan | plan | -| `sum` | `Sum` | plan | **panic** | -| `unit_enum` | `Mode` | plan | plan | +| `record` | `Rec` | rustc | rustc | +| `handle` | `Handle` | rustc | rustc | +| `sum` | `Sum` | rustc | **panic** | +| `unit_enum` | `Mode` | rustc | rustc | | `shared_ref` | `&Rec` | — | — | | `exclusive_ref` | `&mut Rec` | — | — | | `handle_ref` | `&Handle` | — | — | | `out_param` | `&mut MaybeUninit` | — | — | -| `opt_scalar` | `Option` | **panic** | plan | +| `opt_scalar` | `Option` | **panic** | rustc | | `vec_scalar` | `Vec` | **panic** | rejected | | `slice_scalar` | `&[u64]` | — | — | | `slice_mut_scalar` | `&mut [u64]` | — | — | -| `array_scalar` | `[u8; 4]` | rejected | plan | +| `array_scalar` | `[u8; 4]` | rejected | rustc | | `boxed_scalar` | `Box` | rejected | rejected | -| `cow_str` | `Cow<'static, str>` | rejected | plan | -| `opt_record` | `Option` | **panic** | plan | -| `opt_handle` | `Option` | plan | plan | -| `opt_ref` | `Option<&Handle>` | **panic** | plan | -| `vec_record` | `Vec` | **panic** | plan | +| `cow_str` | `Cow<'static, str>` | rejected | **bad rust** | +| `opt_record` | `Option` | **panic** | rustc | +| `opt_handle` | `Option` | **bad rust** | rustc | +| `opt_ref` | `Option<&Handle>` | — | — | +| `vec_record` | `Vec` | **panic** | rustc | | `vec_handle` | `Vec` | **panic** | **panic** | -| `vec_ref` | `Vec<&Handle>` | **panic** | **panic** | +| `vec_ref` | `Vec<&Handle>` | — | — | | `vec_sum` | `Vec` | **panic** | rejected | | `opt_sum` | `Option` | **panic** | rejected | | `array_record` | `[Rec; 2]` | rejected | rejected | | `opt_vec` | `Option>` | rejected | rejected | -| `vec_opt` | `Vec>` | **panic** | plan | -| `result_scalar` | `Result` | rejected | plan | -| `result_handle` | `Result` | rejected | plan | -| `result_sum_err` | `Result` | rejected | plan | +| `vec_opt` | `Vec>` | **panic** | rustc | +| `result_scalar` | `Result` | rejected | rustc | +| `result_handle` | `Result` | rejected | rustc | +| `result_sum_err` | `Result` | rejected | rustc | | `callback` | `impl Fn(u64) + Send + Sync + 'static` | — | — | | `callback_handle` | `impl Fn(Handle) + Send + Sync + 'static` | — | — | @@ -309,12 +313,9 @@ yet. - `boxed_scalar` / Kotlin/JNI: 1 required type(s) could not be resolved: — error: unresolved prebindgen output type `Box < u64 >` - `cow_str` / C: 4 required type(s) could not be resolved: — error: unresolved prebindgen input type `Probe` — error: unresolved prebindgen output type `Probe` — error: unresolved prebindgen input type `Cow < 'static , str >` — error: unresolved prebindg… - `opt_record` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Option < Rec >` cannot cross: its input and output converters disagree on the wire (`* const rec` in, `()` out) and one union field serves both directions -- `opt_ref` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Option < & Handle >` cannot cross: its input and output converters disagree on the wire (`* const handle` in, `()` out) and one union field serves both directions - `vec_record` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Vec < Rec >` cannot cross: a `Vec` needs TWO C wires (pointer + length) and one union field carries only one, so its length would be silently dropped — hand the sequence over thro… - `vec_handle` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Vec < Handle >` cannot cross: a `Vec` needs TWO C wires (pointer + length) and one union field carries only one, so its length would be silently dropped — hand the sequence over t… - `vec_handle` / Kotlin/JNI: JniGen: `Vec` is unsupported — its elements would be closeable native handles (jlong) the JVM must free individually. Expose a per-element accessor instead of returning a `Vec` of handles. -- `vec_ref` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Vec < & Handle >` cannot cross: a `Vec` needs TWO C wires (pointer + length) and one union field carries only one, so its length would be silently dropped — hand the sequence over… -- `vec_ref` / Kotlin/JNI: JniGen: `Vec<& Handle>` is unsupported — its elements would be closeable native handles (jlong) the JVM must free individually. Expose a per-element accessor instead of returning a `Vec` of handles. - `vec_sum` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Vec < Sum >` cannot cross: a `Vec` needs TWO C wires (pointer + length) and one union field carries only one, so its length would be silently dropped — hand the sequence over thro… - `vec_sum` / Kotlin/JNI: 2 required type(s) could not be resolved: — error: unresolved prebindgen output type `Vec < Sum >` — error: unresolved prebindgen output type `Sum` - `opt_sum` / C: Cbindgen::tagged_union: payload `Probe::Carried` of type `Option < Sum >` cannot cross: its input and output converters disagree on the wire (`* const :: core :: mem :: MaybeUninit < sum >` in, `()` out) and one union field serves both d… diff --git a/examples/shape-matrix/src/check.rs b/examples/shape-matrix/src/check.rs new file mode 100644 index 00000000..82ffd066 --- /dev/null +++ b/examples/shape-matrix/src/check.rs @@ -0,0 +1,265 @@ +//! Does the emitted Rust actually compile? +//! +//! `plan` only says the generator produced a file. An emission can be +//! well-formed, contain every substring a unit test looks for, and still not +//! type-check — [`examples/emitcheck`](../../emitcheck) exists because that +//! happened once with 41 of 41 tests green over it. This stage asks rustc, per +//! cell. +//! +//! # The answer is a receipt, not a claim +//! +//! A cell is recorded as compiling only if **rustc says so about that cell's own +//! file**. Every cell is written to `.rs`, the whole crate is checked in one +//! pass, and each diagnostic is attributed back by the file path the compiler +//! reports. Nothing maps a cell to a fixture by name — a name-keyed mapping can +//! claim coverage for a fixture that never touched the cell, which is the defect +//! #175 was. +//! +//! # What it does not cover +//! +//! rustc, and rustc only. Neither `cbindgen` nor the Kotlin compiler runs here, +//! so a cell that compiles has not been shown to produce a valid C header or a +//! loadable JVM class. Those are the rest of `ToolchainCompiled`, and they are +//! not collected yet. + +use std::{ + collections::{BTreeMap, BTreeSet}, + path::{Path, PathBuf}, + process::Command, +}; + +/// One cell's emitted Rust, ready to be checked. +pub struct Unit { + /// `____`, the receipt key. + pub id: String, + /// The fixture's own items — what the source crate would contain. + pub fixture: String, + /// What the generator emitted for it. + pub emitted: String, +} + +/// Which cells rustc accepted, and what it said about the rest. +#[derive(Default)] +pub struct Checked { + pub compiled: BTreeSet, + /// Cell id → the diagnostics rustc reported for it. **Not** rendered into + /// the committed report: a message is a property of the compiler version, + /// and the report has to be identical on every toolchain that builds it. + pub failed: BTreeMap>, +} + +/// Check every unit in one crate, and attribute the result per cell. +/// +/// `Err` is reserved for the check itself failing to run — a missing cargo, an +/// unwritable directory. That is not a verdict about any cell, and callers must +/// not record it as one. +/// `workspace` names the directory this batch is checked in. Two batches must +/// not share one: the report's run and the self-test run concurrently under +/// `cargo test`, and a shared directory would have them overwriting each +/// other's sources. A *stable* name per batch rather than a unique one per +/// call, so the dependencies stay compiled between runs and the emitted Rust +/// stays on disk to be read after a failure. +pub fn check(workspace: &str, units: &[Unit]) -> Result { + if units.is_empty() { + return Ok(Checked::default()); + } + let root = crate_dir()?.join(workspace); + write_crate(&root, units)?; + + let output = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into())) + .arg("check") + .arg("--quiet") + .arg("--message-format=json") + .arg("--manifest-path") + .arg(root.join("Cargo.toml")) + // Its own target directory: the parent build may still hold the + // workspace one, and a nested cargo blocking on that lock would look + // like a hang rather than a queue. + .arg("--target-dir") + .arg(root.join("target")) + .output() + .map_err(|e| format!("running cargo check: {e}"))?; + + let mut checked = Checked::default(); + for unit in units { + checked.compiled.insert(unit.id.clone()); + } + for line in String::from_utf8_lossy(&output.stdout).lines() { + let Some((file, message)) = diagnostic(line) else { + continue; + }; + let Some(id) = cell_of(&file) else { continue }; + checked.compiled.remove(&id); + checked.failed.entry(id).or_default().push(message); + } + + // A crate that failed to build with no attributable diagnostic means the + // failure was the harness's, not a cell's — a bad `Cargo.toml`, a missing + // dependency. Reporting every cell as compiling would be a lie in the + // direction that hides defects. + if !output.status.success() && checked.failed.is_empty() { + return Err(format!( + "cargo check failed with nothing attributable to a cell:\n{}", + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(checked) +} + +/// The file and message of one rustc error, or `None` for anything else on the +/// JSON stream. +/// +/// Deliberately hand-parsed rather than pulled in with `serde_json`: this reads +/// two fields of a stable format, and the alternative is a dependency in a crate +/// whose whole point is to have no opinions of its own. +fn diagnostic(line: &str) -> Option<(String, String)> { + if !line.contains("\"level\":\"error\"") { + return None; + } + let rendered = field(line, "\"rendered\":\"")?; + let file = rendered + .split(&['\\', '"'][..]) + .find(|part| part.ends_with(".rs") && part.contains(CELL_SEP)) + .or_else(|| { + rendered + .split_whitespace() + .find(|w| w.contains(".rs") && w.contains(CELL_SEP)) + })? + .to_string(); + let message = rendered.lines().next().unwrap_or(&rendered).to_string(); + Some((file, message)) +} + +fn field(line: &str, key: &str) -> Option { + let start = line.find(key)? + key.len(); + let rest = &line[start..]; + let mut out = String::new(); + let mut chars = rest.chars(); + while let Some(c) = chars.next() { + match c { + '"' => break, + '\\' => match chars.next() { + Some('n') => out.push('\n'), + Some('"') => out.push('"'), + Some('\\') => out.push('\\'), + Some(other) => out.push(other), + None => break, + }, + other => out.push(other), + } + } + Some(out) +} + +/// The separator between a cell id's parts, chosen so a file name cannot be +/// mistaken for anything else on a diagnostic line. +const CELL_SEP: &str = "__"; + +fn cell_of(file: &str) -> Option { + let stem = Path::new(file.trim_matches(|c: char| !c.is_ascii_graphic())) + .file_stem()? + .to_str()?; + stem.contains(CELL_SEP).then(|| stem.to_string()) +} + +/// Where the generated crate lives: under the workspace target directory, so it +/// is already ignored by git and cleaned by `cargo clean`. +fn crate_dir() -> Result { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace = manifest + .parent() + .and_then(Path::parent) + .ok_or("locating the workspace root")?; + let target = std::env::var("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace.join("target")); + Ok(target.join("shape-matrix-check")) +} + +fn write_crate(root: &Path, units: &[Unit]) -> Result<(), String> { + let src = root.join("src"); + std::fs::create_dir_all(&src).map_err(|e| format!("creating {}: {e}", src.display()))?; + + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace = manifest.parent().and_then(Path::parent).expect("workspace"); + write( + &root.join("Cargo.toml"), + &manifest_toml(&workspace.display().to_string()), + )?; + + let mut lib = String::from(LIB_HEADER); + for (n, unit) in units.iter().enumerate() { + let file = format!("{}.rs", unit.id); + write(&src.join(&file), &cell_source(unit))?; + lib.push_str(&format!("#[path = \"{file}\"]\npub mod cell_{n};\n")); + } + write(&src.join("lib.rs"), &lib) +} + +fn write(path: &Path, contents: &str) -> Result<(), String> { + std::fs::write(path, contents).map_err(|e| format!("writing {}: {e}", path.display())) +} + +/// One cell as a Rust module: the source crate, then the generated file. +/// +/// This mirrors how a binding crate is actually built — `pub mod myflat;` plus +/// `include!("generated_bindings.rs")` in `emitcheck`, the same two lines every +/// consumer writes. +/// +/// The imports go **inside** the fixture module and nowhere else. A source crate +/// writing `Cow<'static, str>` has imported `Cow`; the generated file is a +/// separate scope, and if it needs an import nobody gave it, that is a finding +/// about the generator rather than something for this harness to paper over. +fn cell_source(unit: &Unit) -> String { + format!( + "// Generated by shape-matrix. Do not edit.\n\ + #![allow(clippy::all, dead_code, unused_imports, unused_variables)]\n\ + \n\ + pub mod {} {{\n\ + use std::borrow::Cow;\n\ + use std::mem::MaybeUninit;\n\ + {}\n\ + }}\n\ + \n\ + {}\n", + crate::run::SOURCE_CRATE, + unit.fixture, + unit.emitted + ) +} + +fn manifest_toml(workspace: &str) -> String { + format!( + r#"# Generated by shape-matrix. Do not edit. +[package] +name = "shape-matrix-check" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +# Exactly what generated code calls into — the dependencies a real binding +# crate declares, and nothing else. +[dependencies] +prebindgen-jni-runtime = {{ path = "{workspace}/prebindgen-jni-runtime" }} +prebindgen-c-runtime = {{ path = "{workspace}/prebindgen-c-runtime" }} +# Pinned exactly, and to what the workspace already resolves: this crate has +# its own lockfile, so a caret range would let a new upstream release change a +# cell's answer with nothing in this repo having changed. +jni = "=0.21.1" +tracing = "=0.1.44" +konst = "=0.3.17" + +[workspace] +"# + ) +} + +const LIB_HEADER: &str = "\ +// Generated by shape-matrix. Do not edit. +// +// One module per cell that produced Rust, each in its own file so a diagnostic +// names the cell it belongs to. +"; diff --git a/examples/shape-matrix/src/corpus.rs b/examples/shape-matrix/src/corpus.rs index 1b6e8bd5..4a82ae8d 100644 --- a/examples/shape-matrix/src/corpus.rs +++ b/examples/shape-matrix/src/corpus.rs @@ -24,18 +24,37 @@ pub enum Need { impl Need { /// The Rust the fixture declares for it. + /// + /// Everything derives `Clone`, as the types in a real source crate do. That + /// is not incidental: several generator paths clone — a borrowed handle + /// crossing out becomes an owned one — so a fixture whose types were not + /// `Clone` would spend its cells measuring that constraint instead of + /// whether the shape crosses. pub fn source(self) -> &'static str { match self { - Need::Record => "pub struct Rec { pub id: u64, pub tag: u32 }", - Need::Handle => "pub struct Handle { pub id: u64 }", - Need::Sum => "pub enum Sum { Num(u64), Nothing }", - Need::UnitEnum => "pub enum Mode { On = 0, Off = 1 }", + Need::Record => "#[derive(Clone)] pub struct Rec { pub id: u64, pub tag: u32 }", + Need::Handle => "#[derive(Clone)] pub struct Handle { pub id: u64 }", + // `Display` too: `result_sum_err` puts this in an error position, + // where both targets render the error as text. + Need::Sum => { + "#[derive(Clone)] pub enum Sum { Num(u64), Nothing }\n\ + impl std::fmt::Display for Sum {\n\ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n\ + write!(f, \"sum\") } }" + } + Need::UnitEnum => "#[derive(Clone, Copy)] pub enum Mode { On = 0, Off = 1 }", // Both targets need a way to render an error, so the accessor is // part of the declaration rather than something a cell goes // without. + // An error type is `Clone` and `Display` because that is what an + // error type is; a fixture without them spends its fallible cells + // measuring that requirement instead of whether the shape crosses. Need::Error => { - "pub struct ZError { pub code: u64 }\n\ - pub fn zerror_message(e: &ZError) -> String { unimplemented!() }" + "#[derive(Clone)] pub struct ZError { pub code: u64 }\n\ + impl std::fmt::Display for ZError {\n\ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n\ + write!(f, \"error {}\", self.code) } }\n\ + pub fn zerror_message(e: &ZError) -> String { unimplemented!() }" } } } @@ -90,6 +109,17 @@ impl Position { Position::Payload => "enum payload", } } + + /// The part of a cell id this position contributes — file-safe, since a + /// cell id names the file rustc reports diagnostics against. + pub fn slug(self) -> &'static str { + match self { + Position::Param => "param", + Position::Return => "ret", + Position::Field => "field", + Position::Payload => "payload", + } + } } /// The shapes. diff --git a/examples/shape-matrix/src/lib.rs b/examples/shape-matrix/src/lib.rs index 920e7564..7dca830c 100644 --- a/examples/shape-matrix/src/lib.rs +++ b/examples/shape-matrix/src/lib.rs @@ -36,8 +36,13 @@ //! legitimately answer differently, and one combined verdict would hide //! exactly the gaps this exists to find. //! +//! Every cell that produces Rust is then handed to rustc ([`check`]), because +//! "the generator produced a file" and "the file compiles" are different claims +//! and only the second is worth much. +//! //! Run it with `cargo run -p shape-matrix`, which rewrites `REPORT.md`. +pub mod check; pub mod corpus; pub mod report; pub mod run; diff --git a/examples/shape-matrix/src/report.rs b/examples/shape-matrix/src/report.rs index 2479d560..20120b2d 100644 --- a/examples/shape-matrix/src/report.rs +++ b/examples/shape-matrix/src/report.rs @@ -7,6 +7,7 @@ use std::fmt::Write as _; use crate::{ + check::{self, Unit}, corpus::{Position, Shape, SHAPES}, run::{declarations, run, ClassKind, State, Target}, tag::TypeTag, @@ -35,6 +36,32 @@ struct Cell { position: Position, target: Target, state: State, + /// Whether rustc accepted the emitted Rust. `None` when there was nothing + /// to check — the generator refused the cell, or the check could not run at + /// all, which is not a verdict about the cell either. + compiled: Option, +} + +impl Cell { + /// `____` — the receipt key, and the name of the + /// file rustc reports against. + fn id(&self) -> String { + format!( + "{}__{}__{}", + self.shape.id, + self.position.slug(), + self.target.slug() + ) + } + + /// What the table prints. + fn text(&self) -> String { + match (&self.state, self.compiled) { + (State::PlanSupported, Some(true)) => "rustc".to_string(), + (State::PlanSupported, Some(false)) => "**bad rust**".to_string(), + (state, _) => state.cell(), + } + } } fn run_all() -> Vec { @@ -44,42 +71,76 @@ fn run_all() -> Vec { std::panic::set_hook(Box::new(|_| {})); let mut cells = Vec::new(); + let mut units = Vec::new(); for shape in SHAPES { for position in Position::ALL { for target in Target::ALL { - cells.push(Cell { + let outcome = run(shape, *position, *target); + let cell = Cell { shape, position: *position, target: *target, - state: run(shape, *position, *target), - }); + state: outcome.state, + compiled: None, + }; + if let Some(emitted) = outcome.emitted { + units.push(Unit { + id: cell.id(), + fixture: crate::run::fixture_source(shape, *position), + emitted, + }); + } + cells.push(cell); } } } std::panic::set_hook(previous); + + // A check that could not run is reported as such and leaves every cell + // uncompiled, rather than being folded into the cells as a verdict they did + // not earn. + match check::check("cells", &units) { + Ok(checked) => { + for cell in &mut cells { + if matches!(cell.state, State::PlanSupported) { + cell.compiled = Some(checked.compiled.contains(&cell.id())); + } + } + for (id, messages) in &checked.failed { + eprintln!("shape-matrix: {id} emitted Rust that does not compile:"); + for message in messages { + eprintln!(" {message}"); + } + } + } + Err(err) => eprintln!("shape-matrix: the compile check did not run: {err}"), + } + cells } fn render_summary(out: &mut String, results: &[Cell]) { out.push_str("## Summary\n\n"); - out.push_str("| Target | plan | rejected | panic | n/a |\n"); - out.push_str("|---|---:|---:|---:|---:|\n"); + out.push_str("| Target | rustc | bad rust | plan only | rejected | panic | n/a |\n"); + out.push_str("|---|---:|---:|---:|---:|---:|---:|\n"); for target in Target::ALL { - let of = |f: fn(&State) -> bool| { + let of = |f: fn(&Cell) -> bool| { results .iter() - .filter(|c| c.target == *target && f(&c.state)) + .filter(|c| c.target == *target && f(c)) .count() }; let _ = writeln!( out, - "| {} | {} | {} | {} | {} |", + "| {} | {} | {} | {} | {} | {} | {} |", target.as_str(), - of(|s| matches!(s, State::PlanSupported)), - of(|s| matches!(s, State::Rejected(_))), - of(|s| matches!(s, State::Panicked(_))), - of(|s| matches!(s, State::NotApplicable(_))), + of(|c| c.compiled == Some(true)), + of(|c| c.compiled == Some(false)), + of(|c| matches!(c.state, State::PlanSupported) && c.compiled.is_none()), + of(|c| matches!(c.state, State::Rejected(_))), + of(|c| matches!(c.state, State::Panicked(_))), + of(|c| matches!(c.state, State::NotApplicable(_))), ); } out.push('\n'); @@ -96,7 +157,7 @@ fn render_position(out: &mut String, results: &[Cell], position: Position) { .find(|c| { std::ptr::eq(c.shape, shape) && c.position == position && c.target == target }) - .map(|c| c.state.cell()) + .map(|c| c.text()) .unwrap_or_default() }; let _ = writeln!( @@ -211,13 +272,20 @@ never by a hand-written list of what is supposed to work. See | Cell | Meaning | |---|---| -| `plan` | generation succeeded. Nothing here compiled, linked or ran the result. | +| `rustc` | the generator produced Rust **and rustc accepted it**. Neither cbindgen nor the Kotlin compiler has seen it. | +| **`bad rust`** | the generator produced Rust that does not compile. Green unit tests can coexist with this — that is why the check exists. | +| `plan` | generation succeeded and the compile check did not run (see the run\'s stderr). | | `rejected` | the generator refused the shape **and said why**. The intended outcome for anything unsupported. | | **`panic`** | the generator refused it without a diagnosis — the user gets a stack trace instead of a sentence ([#191](https://github.com/milyin/prebindgen/issues/191)). | | `—` | the placement is not legal Rust, so there is nothing to ask. | -`plan` is evidence, not a guarantee: `ToolchainCompiled` and `RuntimeExercised` -are the states that require a compiler and a runtime, and neither is collected -yet. +`rustc` is evidence, not a guarantee. It is the Rust half of +`ToolchainCompiled`: the emitted Rust type-checks against the fixture the way a +binding crate compiles it. The C header, the Kotlin classes and every +`RuntimeExercised` cell still require toolchains this stage does not run. + +Compiler messages are deliberately **not** in this file — they vary by +toolchain, and the report has to be identical on every one that builds it. A +failing cell prints its diagnostics on the run\'s stderr. "; diff --git a/examples/shape-matrix/src/run.rs b/examples/shape-matrix/src/run.rs index 4c251667..6bce62ee 100644 --- a/examples/shape-matrix/src/run.rs +++ b/examples/shape-matrix/src/run.rs @@ -16,8 +16,14 @@ use prebindgen_registry::{ExpandReturnDecl, FunctionDecl}; use crate::corpus::{Need, Position, Shape}; -/// The crate name every fixture item is stamped with. -const SOURCE_CRATE: &str = "probe"; +/// The crate name every fixture item is stamped with, and so the module the +/// generated code qualifies its calls through. +/// +/// Not `probe`: the fixture's function is called that, and a consumer mounting +/// the source crate as `mod probe` next to a generated `pub fn probe` wrapper +/// does not compile. The two names are different things and this crate found +/// out by conflating them. +pub const SOURCE_CRATE: &str = "flat"; /// The function every fixture declares to the target. const PROBE_FN: &str = "probe"; @@ -102,6 +108,15 @@ impl Target { Target::Jni => "Kotlin/JNI", } } + + /// The part of a cell id this target contributes. See + /// [`Position::slug`](crate::corpus::Position::slug). + pub fn slug(self) -> &'static str { + match self { + Target::C => "c", + Target::Jni => "jni", + } + } } /// Why a placement is not legal Rust to begin with. @@ -118,12 +133,38 @@ pub fn not_applicable(shape: &Shape, position: Position) -> Option<&'static str> if shape.spelling.contains("impl Fn") { return Some("`impl Trait` is not a field type"); } - if shape.spelling.starts_with('&') { + if shape.spelling.contains('&') { return Some("a borrowed field needs a lifetime parameter on its declaration"); } None } +/// The same spelling, with every borrow given an explicit `'static`. +/// +/// A returned borrow needs a lifetime and the probe function has no parameter +/// to elide one from, so `-> &Handle` is not Rust at all. Writing `'static` +/// keeps the shape and makes the fixture legal; the alternative — adding an +/// anchor parameter — would change what the cell measures. +/// +/// This was found the hard way: before it, every borrow-returning cell reported +/// `plan` for a fixture that could not have compiled. +fn anchored(spelling: &str) -> String { + let mut out = String::new(); + let mut chars = spelling.chars().peekable(); + while let Some(c) = chars.next() { + out.push(c); + if c != '&' { + continue; + } + // Already written with a lifetime — `Cow<'static, str>` and friends. + if chars.peek() == Some(&'\'') { + continue; + } + out.push_str("'static "); + } + out +} + /// The fixture's Rust source: the shape's supporting declarations, the wrapper /// declaration the position needs, and the function that makes it cross. pub fn fixture_source(shape: &Shape, position: Position) -> String { @@ -135,18 +176,23 @@ pub fn fixture_source(shape: &Shape, position: Position) -> String { items.push(format!("pub fn {PROBE_FN}(v: {ty}) {{ let _ = v; }}")); } Position::Return => { + let ty = anchored(ty); items.push(format!( "pub fn {PROBE_FN}() -> {ty} {{ unimplemented!() }}" )); } Position::Field => { - items.push(format!("pub struct {PROBE_TY} {{ pub v: {ty} }}")); + items.push(format!( + "#[derive(Clone)] pub struct {PROBE_TY} {{ pub v: {ty} }}" + )); items.push(format!( "pub fn {PROBE_FN}() -> {PROBE_TY} {{ unimplemented!() }}" )); } Position::Payload => { - items.push(format!("pub enum {PROBE_TY} {{ Carried({ty}), Empty }}")); + items.push(format!( + "#[derive(Clone)] pub enum {PROBE_TY} {{ Carried({ty}), Empty }}" + )); items.push(format!( "pub fn {PROBE_FN}() -> {PROBE_TY} {{ unimplemented!() }}" )); @@ -273,6 +319,18 @@ pub fn declarations(shape: &Shape, position: Position) -> Vec { decls } +/// The fixture's items, as the **model** sees them. +/// +/// Filtered to the four item kinds a `#[prebindgen]` surface declares, the same +/// filter `examples/emitcheck` applies to its own source file. A fixture is a +/// real Rust file and so carries things a flat API does not declare — an +/// `impl Display for ZError` is part of what makes the crate compile and is not +/// part of its boundary. +/// +/// Found by feeding them: an `impl` block reaches the frontend as *"is an item +/// kind the prebindgen source language does not model"*, which failed the whole +/// binding and turned 32 cells into rejections that had nothing to do with +/// their shape. fn items(source: &str) -> Vec<(syn::Item, SourceLocation)> { let loc = SourceLocation { crate_name: Some(SOURCE_CRATE.to_string()), @@ -282,6 +340,12 @@ fn items(source: &str) -> Vec<(syn::Item, SourceLocation)> { .expect("fixture parses") .items .into_iter() + .filter(|item| { + matches!( + item, + syn::Item::Fn(_) | syn::Item::Struct(_) | syn::Item::Enum(_) | syn::Item::Const(_) + ) + }) .map(|item| (item, loc.clone())) .collect() } @@ -294,12 +358,23 @@ fn ident(name: &str) -> syn::Ident { syn::parse_str(name).expect("ident parses") } +/// What one cell produced: the generator's answer, and — when it answered at +/// all — the Rust it emitted, which is what the next stage type-checks. +pub struct Outcome { + pub state: State, + /// The generated Rust. `Some` exactly when `state` is `PlanSupported`. + pub emitted: Option, +} + /// Run one cell, catching a panic as an outcome rather than letting it end the /// run. A generator that panics on an unsupported shape is reporting something /// — badly — and the table says so. -pub fn run(shape: &Shape, position: Position, target: Target) -> State { +pub fn run(shape: &Shape, position: Position, target: Target) -> Outcome { if let Some(reason) = not_applicable(shape, position) { - return State::NotApplicable(reason); + return Outcome { + state: State::NotApplicable(reason), + emitted: None, + }; } let source = fixture_source(shape, position); let decls = declarations(shape, position); @@ -310,9 +385,18 @@ pub fn run(shape: &Shape, position: Position, target: Target) -> State { })); match outcome { - Ok(Ok(())) => State::PlanSupported, - Ok(Err(msg)) => State::Rejected(msg), - Err(payload) => State::Panicked(panic_message(payload)), + Ok(Ok(emitted)) => Outcome { + state: State::PlanSupported, + emitted: Some(emitted), + }, + Ok(Err(msg)) => Outcome { + state: State::Rejected(msg), + emitted: None, + }, + Err(payload) => Outcome { + state: State::Panicked(panic_message(payload)), + emitted: None, + }, } } @@ -326,7 +410,20 @@ fn panic_message(payload: Box) -> String { } } -fn run_jni(source: &str, decls: &[Decl]) -> Result<(), String> { +/// The generated Rust, as the target wrote it. +/// +/// Through a file rather than a string because that is the only surface either +/// generator offers — a build script writes `generated_bindings.rs` and the +/// consumer `include!`s it, so this is also exactly what a consumer compiles. +fn read_back( + write: impl FnOnce(&std::path::Path) -> Result, +) -> Result { + let dir = tempfile::tempdir().map_err(|e| e.to_string())?; + let written = write(&dir.path().join("generated.rs"))?; + std::fs::read_to_string(written).map_err(|e| e.to_string()) +} + +fn run_jni(source: &str, decls: &[Decl]) -> Result { let mut pkg = prebindgen_jni::package!().fun(FunctionDecl::new(ident(PROBE_FN))); let mut error_decls: Vec = Vec::new(); for decl in decls { @@ -352,11 +449,7 @@ fn run_jni(source: &str, decls: &[Decl]) -> Result<(), String> { } let generation = builder.build().map_err(|e| e.to_string())?; - let dir = tempfile::tempdir().map_err(|e| e.to_string())?; - generation - .write_rust(dir.path().join("generated.rs")) - .map_err(|e| e.to_string())?; - Ok(()) + read_back(|path| generation.write_rust(path).map_err(|e| e.to_string())) } /// The declaration axis, spoken to the C builder. @@ -381,7 +474,7 @@ pub fn to_c(cbindgen: prebindgen_c::CbindgenBuilder, decl: &Decl) -> prebindgen_ } } -fn run_c(source: &str, decls: &[Decl]) -> Result<(), String> { +fn run_c(source: &str, decls: &[Decl]) -> Result { let mut cbindgen = Cbindgen::builder() .items(items(source)) .source_module(syn::parse_str(SOURCE_CRATE).expect("crate name is a path")) @@ -397,9 +490,5 @@ fn run_c(source: &str, decls: &[Decl]) -> Result<(), String> { } let generation = cbindgen.build().map_err(|e| e.to_string())?; - let dir = tempfile::tempdir().map_err(|e| e.to_string())?; - generation - .write_rust(dir.path().join("generated.rs")) - .map_err(|e| e.to_string())?; - Ok(()) + read_back(|path| generation.write_rust(path).map_err(|e| e.to_string())) } diff --git a/examples/shape-matrix/src/tests.rs b/examples/shape-matrix/src/tests.rs index 3043637f..a2c16382 100644 --- a/examples/shape-matrix/src/tests.rs +++ b/examples/shape-matrix/src/tests.rs @@ -127,15 +127,65 @@ fn a_cell_always_answers() { .find(|s| s.id == "scalar") .expect("scalar shape"); for target in Target::ALL { - let state = crate::run::run(shape, Position::Param, *target); + let outcome = crate::run::run(shape, Position::Param, *target); assert!( - matches!(state, crate::run::State::PlanSupported), - "a scalar parameter should cross for {}, got {state:?}", - target.as_str() + matches!(outcome.state, crate::run::State::PlanSupported), + "a scalar parameter should cross for {}, got {:?}", + target.as_str(), + outcome.state + ); + assert!( + outcome.emitted.is_some(), + "a cell that generated produced no Rust to check" ); } } +/// The compile check must attribute per cell, and must actually discriminate. +/// +/// It would be easy to write a check that marks everything as compiling: no +/// diagnostic ever matches, every cell passes, and the column becomes +/// decoration. So this feeds it one unit that compiles and one that cannot, in +/// the same crate, and requires it to separate them — which also pins the +/// attribution path, since the only thing linking a diagnostic to a cell is the +/// file rustc names. +#[test] +fn the_compile_check_separates_good_from_bad() { + let checked = check::check( + "selftest", + &[ + check::Unit { + id: "selftest_good__param__jni".to_string(), + fixture: "pub fn probe(v: u64) -> u64 { v }".to_string(), + emitted: "pub fn wrapper(v: u64) -> u64 { flat::probe(v) }".to_string(), + }, + check::Unit { + id: "selftest_bad__param__jni".to_string(), + fixture: "pub fn probe(v: u64) -> u64 { v }".to_string(), + // A type error in emitted code, of the kind a generator makes: the + // wrapper hands a string to a function taking an integer. + emitted: "pub fn wrapper() -> u64 { flat::probe(\"not a u64\") }".to_string(), + }, + ], + ) + .expect("the check runs"); + + assert!( + checked.compiled.contains("selftest_good__param__jni"), + "a unit that compiles was not recorded as compiling" + ); + assert!( + !checked.compiled.contains("selftest_bad__param__jni"), + "a unit that does not compile was recorded as compiling — the check is \ + reporting a state it did not establish" + ); + assert!( + checked.failed.contains_key("selftest_bad__param__jni"), + "the failing unit produced no attributed diagnostic, so nothing links a \ + compiler error to the cell it belongs to" + ); +} + /// The committed report is the regression gate; a stale one gates nothing. #[test] fn report_is_current() {