Skip to content

gen-rust: make the Copy derive transitive over declared types - #3437

Merged
gHashTag merged 1 commit into
masterfrom
rust/transitive-copy
Sep 8, 2026
Merged

gen-rust: make the Copy derive transitive over declared types#3437
gHashTag merged 1 commit into
masterfrom
rust/transitive-copy

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #3436

A struct parameter is passed by value, so a non-Copy struct 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
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 exactly that shape -- its own is_rom test reads
m twice. Enums were the second half of the same blind spot: every enum is
emitted Copy unconditionally, so an enum-typed field cannot be what
disqualifies its owner, but it was.

Measured -- 582 translation units, old binary against new

before after
error[E0382] 125 106
files with any 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.

One class grew, and it is not this change. E0119 went 6 -> 7 because
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, 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 every
struct 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 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, since #[derive(Copy)] on a struct reaching a Vec is itself an
error.

mutant dead tests
derive back to the static helper 4
fixpoint admits everything 1 (was 0 before the test was fixed)
enums dropped from the seed 1

Full suite: 2593 passed, 0 failed.

Also

check_ring_spec_drift.py loses a silent diff[:6] that printed six names
under a line reading "differing 7". The seventh, verify_trace, is how this
investigation started -- an unmarked cap reads as coverage.

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>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-08 00:05:37 UTC

Summary

Status Count
Total Open PRs 16
PRs with Failing Checks 13
PRs with All Checks Green 3
READY 2
FAILING 13
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 13 + 0 + 0 = 15, and there are 16 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad79c813365 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag enabled auto-merge (squash) September 8, 2026 00:05
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@gHashTag
gHashTag merged commit ee48c40 into master Sep 8, 2026
26 of 32 checks passed
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>
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.

gen-rust: a struct of Copy structs is not Copy, so its generated API consumes its argument

1 participant