gen-rust: make the Copy derive transitive over declared types - #3437
Merged
Conversation
Closes #3436 A struct parameter is passed by value, so a struct that is not `Copy` is MOVED into the call and a caller reading the same value twice does not compile. The derive asked `is_copy_rust_type`, which knows the scalars and `[T; N]` and nothing about declared types: struct Inner { a : i32 } // all scalars -> Copy struct Outer { i : Inner } // a struct field -> NOT Copy (first(o), second(o)) // error[E0382]: use of moved value: `o` The generated module compiles by itself; the error appears only at the caller. `specs/fpga/memory.t27` has that shape -- its own `is_rom` test reads `m` twice. Enums were the second half of the same blind spot: every enum is emitted `#[derive(Debug, Clone, Copy, PartialEq, Eq)]` unconditionally, so an enum-typed field cannot be what disqualifies its owner, but it was. A pre-pass collects every declared struct with its field types and every enum name; `copy_qualified_types` takes the least fixed point over that; the derive asks the resulting set as well as the static helper. A struct with no fields is deliberately NOT admitted -- the existing rule is `all_fields_copy` over a non-empty list, and widening it is a separate question. Measured over 582 translation units, old binary against new: error[E0382] 125 -> 106 files with E0382 27 -> 21 total errors 3251 -> 3223 files that compile 370 -> 372 Population: 1034 structs in generated Rust, 579 without `Copy`, of which 24 in 18 files qualify once transitivity is applied. Exactly one class grew, `E0119` 6 -> 7, and it is not this change. `specs/ml/optimizer/adamw.t27` emits `AdamWConfig` twice -- the struct is not declared in that spec at all, it arrives through `use` -- so a third derived trait yields a third conflicting implementation. That file fails to compile either way. Filed separately. The negative test is the part worth reading. Its first version asserted only that a struct holding a growable field is not `Copy`, and a mutant admitting EVERY struct to the fixpoint passed it -- the derive asks about a FIELD's type (`Vec<u8>`) and never about that struct's own name, so the mutant stayed invisible until a struct was given it as a field. The test now nests the un-copyable struct inside another and hands the result to rustc, because `#[derive(Copy)]` on a struct reaching a `Vec` is itself an error. Three mutants, three dead. `check_ring_spec_drift.py` loses a silent `diff[:6]` that printed six names under the line reading "differing 7". The seventh, `verify_trace`, is how this investigation started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
gHashTag
enabled auto-merge (squash)
September 8, 2026 00:05
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
This was referenced Sep 8, 2026
gHashTag
added a commit
that referenced
this pull request
Sep 8, 2026
Refs #3440 `Spec Guards` has been red on master since #3435 and red again after #3437. Both merged, because it is not a required context -- so `--auto` never looked at it, and neither did I. It was wired one pass earlier precisely because three checks only ran when someone typed them. The guard is right. #3435 changed C parameter emission and #3437 changed the Rust `Copy` derive, so the seals recording what those backends emit no longer described the output. Measured on b519f90: seals scanned 1318 current 953 STALE generated-code hash 102 across 57 specs sealed with gen_hash=none 169 spec file no longer present 94 By field: 88 gen_hash_rust, 19 gen_hash_c. `spec_hash` stale for ZERO, which is why every coverage and freshness gate stayed green -- the same shape as #3415, one merge later. This is a cycle rather than rot: the previous `sealed_at` on the affected files was 2026-09-07T21:07:04Z, hours old. Every backend change invalidates seals and nothing in the merge path refreshes them or blocks on them. The diff is 102 files and exactly three keys move: `sealed_at` (102), `gen_hash_rust` (88), `gen_hash_c` (19). `spec_hash`, `gen_hash_verilog` and `gen_hash_zig` are untouched. After: 0 stale, the gate exits 0. Two neighbouring populations are deliberately left alone and named in #3440 rather than folded in: 94 seals whose spec file no longer exists, and 169 recording `gen_hash=none`. Conflating them is how the last batch stayed invisible. The cure for the cycle is the owner's call and is not in this commit. Co-authored-by: lab <lab@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3436
A struct parameter is passed by value, so a non-
Copystruct is MOVED into thecall and a caller reading the same value twice does not compile. The derive
asked
is_copy_rust_type, which knows the scalars and[T; N]and nothingabout declared types.
The generated module compiles by itself; the error appears only at the caller.
specs/fpga/memory.t27has exactly that shape -- its ownis_romtest readsmtwice. Enums were the second half of the same blind spot: every enum isemitted
Copyunconditionally, so an enum-typed field cannot be whatdisqualifies its owner, but it was.
Measured -- 582 translation units, old binary against new
error[E0382]Population: 1034 structs in generated Rust, 579 without
Copy, ofwhich 24 in 18 files qualify once transitivity is applied.
One class grew, and it is not this change.
E0119went 6 -> 7 becausespecs/ml/optimizer/adamw.t27emitsAdamWConfigtwice -- the struct is notdeclared in that spec at all, it arrives through
use-- so a third derivedtrait yields a third conflicting implementation. That file fails to compile
either way, and the duplicate emission is filed separately.
The negative test could not fail
Worth reading, because it nearly shipped. The first version asserted only that
a struct holding a growable field is not
Copy. A mutant admitting everystruct to the fixpoint passed all five tests: the derive asks about a FIELD's
type (
Vec<u8>) and never about that struct's own name, so the mutant stayedinvisible until a struct was given it as a field.
The test now nests the un-copyable struct inside another and hands the result
to rustc, since
#[derive(Copy)]on a struct reaching aVecis itself anerror.
Full suite: 2593 passed, 0 failed.
Also
check_ring_spec_drift.pyloses a silentdiff[:6]that printed six namesunder a line reading "differing 7". The seventh,
verify_trace, is how thisinvestigation started -- an unmarked cap reads as coverage.