Skip to content

Rust::com Derive macro for CommData#58

Open
saksham-chawla wants to merge 2 commits intocodex/replay-20260319023913-pr-156-basefrom
codex/replay-20260319023913-pr-156-head
Open

Rust::com Derive macro for CommData#58
saksham-chawla wants to merge 2 commits intocodex/replay-20260319023913-pr-156-basefrom
codex/replay-20260319023913-pr-156-head

Conversation

@saksham-chawla
Copy link
Collaborator

@saksham-chawla saksham-chawla commented Mar 18, 2026

  • Implemented derive macro for CommData trait impl

eclipse-score#155

* Implemented derive macro for CommData trait impl
* CommData should explicitly derive Reloc. This makes the dependency clear in the code.
Copy link

@propel-code-bot propel-code-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several important macro logic issues and minor docs/UX fixes need addressing before merge.

Status: Changes Suggested | Risk: Medium | Readiness: 5/10 (Needs Polish)

Issues Identified & Suggestions
  • Preserve stable IDs; avoid breaking changes to generated identifiers: com_api_gen.rs
  • Handle generics and add required bounds in CommData derive: lib.rs
  • Fix stray doc fence and improve attribute error messages: lib.rs
Review Details

📁 3 files reviewed | 💬 5 comments

👍 / 👎 individual comments to help improve reviews for you

Comment on lines +23 to 26
#[derive(Debug, Reloc, CommData)]
#[repr(C)]
// No explicit ID provided, so it will be auto-generated as "com_api_gen::Exhaust"
pub struct Exhaust {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

[Logic] Backward-incompatible ID change: Exhaust previously had const ID: &'static str = "Exhaust" and will now auto-generate as "com_api_gen::Exhaust" (or whatever module_path!() resolves to). Since these IDs are used by the framework to match types across process boundaries (as stated in the CommData docs), this silently breaks any existing consumer/producer that was matching on "Exhaust". Either preserve the old ID with #[comm_data(id = "Exhaust")] or document this as an intentional breaking change.

Suggested change
#[derive(Debug, Reloc, CommData)]
#[repr(C)]
// No explicit ID provided, so it will be auto-generated as "com_api_gen::Exhaust"
pub struct Exhaust {}
#[derive(Debug, Reloc, CommData)]
#[repr(C)]
#[comm_data(id = "Exhaust")]
pub struct Exhaust {}
Context for Agents
Backward-incompatible ID change: `Exhaust` previously had `const ID: &'static str = "Exhaust"` and will now auto-generate as `"com_api_gen::Exhaust"` (or whatever `module_path!()` resolves to). Since these IDs are used by the framework to match types across process boundaries (as stated in the `CommData` docs), this silently breaks any existing consumer/producer that was matching on `"Exhaust"`. Either preserve the old ID with `#[comm_data(id = "Exhaust")]` or document this as an intentional breaking change.

```suggestion
#[derive(Debug, Reloc, CommData)]
#[repr(C)]
#[comm_data(id = "Exhaust")]
pub struct Exhaust {}
```

File: score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs
Line: 26
Chain of Thought
  • Validation Decision: accepted
  • Confidence: 0.70
  • Likelihood Score: 3/5
  • Likelihood Reason: The ID does change from "Exhaust" to "com_api_gen::Exhaust", which would break any consumer/producer matching on the old ID. However, this is example code explicitly documenting the auto-generation feature, so the change appears intentional. The likelihood of real breakage depends on whether any production code depends on this example's ID.
  • Suggested Action: modify
  • Tool Calls Used: 5

Reasoning

The comment is technically correct about the mechanics:

  1. In BASE, Exhaust had const ID: &'static str = "Exhaust" (manual impl).
  2. In HEAD, the #[derive(CommData)] without #[comm_data(id = "...")] will auto-generate the ID as concat!(module_path!(), "::", stringify!(Exhaust)), confirmed by macro code at lib.rs lines 80-83.
  3. This changes the ID from "Exhaust" to something like "com_api_gen::Exhaust" — a backward-incompatible change.
  4. The Tire struct does use #[comm_data(id = "Tire")] to preserve its old ID, showing the reviewer's suggestion is valid.

However, there are mitigating factors:

  1. This is example code in score/mw/com/example/..., designed to demonstrate both usage patterns (explicit ID and auto-generated ID).
  2. The code already has an explicit inline comment: "No explicit ID provided, so it will be auto-generated as 'com_api_gen::Exhaust'" — this documents the intentional nature of the change.
  3. The reviewer's own suggestion says "or document this as an intentional breaking change" — and the inline comment arguably does this.

The comment is valid in identifying the breaking change, but the suggestion to add #[comm_data(id = "Exhaust")] would defeat the purpose of demonstrating the auto-generation feature. The alternative suggestion ("document this as an intentional breaking change") is already partially addressed by the inline comment. Still, the reviewer raises a valid concern worth acknowledging.

Evidence Summary

1. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

2. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept/com_api_concept.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

3. File Snapshot (BASE)

  • File: score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

4. File Snapshot (HEAD)

  • File: score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

Fix in Cursor

}
Ok(None) => {
// Auto-generated: use fully qualified type name with module_path!()
quote! { concat!(module_path!(), "::", stringify!(#ident_name)) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

[Logic] Auto-generated ID ignores generic parameters. If a generic type like Wrapper<T> derives CommData, <Wrapper<u32> as CommData>::ID and <Wrapper<u64> as CommData>::ID both become module::Wrapper, which can cause type collisions across IPC. If generics aren't supported, reject them explicitly:

if !input_args.generics.params.is_empty() {
    return syn::Error::new_spanned(
        &ident_name,
        "CommData cannot be derived for generic types; use a concrete wrapper or an explicit ID",
    )
    .to_compile_error()
    .into();
}
Context for Agents
Auto-generated ID ignores generic parameters. If a generic type like `Wrapper<T>` derives `CommData`, `<Wrapper<u32> as CommData>::ID` and `<Wrapper<u64> as CommData>::ID` both become `module::Wrapper`, which can cause type collisions across IPC. If generics aren't supported, reject them explicitly:

```rust
if !input_args.generics.params.is_empty() {
    return syn::Error::new_spanned(
        &ident_name,
        "CommData cannot be derived for generic types; use a concrete wrapper or an explicit ID",
    )
    .to_compile_error()
    .into();
}
```

File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
Line: 82
Chain of Thought
  • Validation Decision: accepted
  • Confidence: 0.95
  • Likelihood Score: 4/5
  • Likelihood Reason: The macro explicitly handles generics in the impl block (lines 70-72, 91) — it generates valid impl&lt;T&gt; CommData for Wrapper&lt;T&gt; code — but the auto-generated ID at line 82 uses only stringify!(#ident_name) which strips type parameters. Since the Reloc derive (which must precede CommData) fully supports generics, a developer will naturally try to derive both on a generic type, leading to silent ID collisions across IPC.
  • Suggested Action: keep
  • Tool Calls Used: 2

Reasoning

The comment identifies a genuine bug in newly introduced code (this PR adds the entire derive_comm_data function).

The issue is real:

  • Line 68: let ident_name = &amp;input_args.ident;DeriveInput::ident is just the type name without generic parameters (e.g., Wrapper, not Wrapper&lt;T&gt;).
  • Line 82: quote! { concat!(module_path!(), &#34;::&#34;, stringify!(#ident_name)) }stringify!(Wrapper) produces &#34;Wrapper&#34; regardless of the concrete type parameter.
  • Therefore, &lt;Wrapper&lt;u32&gt; as CommData&gt;::ID and &lt;Wrapper&lt;u64&gt; as CommData&gt;::ID would both be &#34;some_module::Wrapper&#34;.

The macro explicitly supports generics but gets the ID wrong:

  • Lines 70-72 handle generics: let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
  • Line 91 generates: impl #impl_generics CommData for #ident_name #type_generics #where_clause
  • The companion Reloc derive also supports generics (line 182, 213-214).
  • So the code compiles fine for generic types but silently produces identical IDs.

This is introduced by this PR — the entire derive_comm_data function is new.

The comment is actionable — it provides a concrete code snippet to reject generics at the macro level, which is a clean and safe fix. The ID is described as being used "to match types across process boundaries" (line 21 of the doc), so collisions could cause type confusion in IPC, which is a serious correctness issue.

No existing guards prevent this: There is no check on input_args.generics.params in the derive_comm_data function.

Evidence Summary

1. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
  • Lines: 65-98
65: #[proc_macro_derive(CommData, attributes(comm_data))]
66: pub fn derive_comm_data(input: TokenStream) -> TokenStream {
67:     let input_args = parse_macro_input!(input as DeriveInput);
68:     let ident_name = &input_args.ident;
69: 
70:     //Add where clause if there are generics
71:     let generics = input_args.generics.clone();
72:     let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
73: 
74:     // Generate ID string: use user-provided ID if available, otherwise use fully qualified type name
...

2. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
  • Lines: 167-260
167: #[proc_macro_derive(Reloc)]
168: pub fn derive_reloc(input: TokenStream) -> TokenStream {
169:     let input_args = parse_macro_input!(input as DeriveInput);
170:     let ident_name = &input_args.ident;
171: 
172:     // Ensure #[repr(C)] on the struct itself
173:     if !has_repr_c(&input_args.attrs) {
174:         return syn::Error::new_spanned(
175:             &ident_name,
176:             "The #[derive(Reloc)] macro requires #[repr(C)] on the type",
...

Fix in Cursor

Comment on lines +90 to +94
let comm_data_impl = quote! {
impl #impl_generics CommData for #ident_name #type_generics #where_clause {
const ID: &'static str = #id_str;
}
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

[Logic] The generated impl CommData never carries over the bounds required by the CommData supertraits. For a generic type

#[derive(Reloc, CommData)]
#[repr(C)]
pub struct Wrapper<T> {
    inner: T,
}

Reloc derive expands to unsafe impl<T> Reloc for Wrapper<T> where T: Reloc {}, but the code here emits impl<T> CommData for Wrapper<T> { … } with an empty where clause. The compiler therefore has to prove Wrapper<T>: Reloc + Send + Debug + 'static for all T, which is impossible and currently makes the derive fail for every generic struct/enum (the trait bound Wrapper<T>: Reloc is not satisfied).

Please push the required predicate onto the generated where clause before split_for_impl(), e.g.

let mut generics = input_args.generics.clone();
{
    let where_clause = generics.make_where_clause();
    where_clause.predicates.push(parse_quote!(
        #ident_name #type_generics: Reloc + ::core::fmt::Debug + ::core::marker::Send + 'static
    ));
}
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();

(Using #ident_name #type_generics or Self for the predicate is fine.) This restricts the CommData impl to exactly the cases where the supertraits are satisfied, allowing generic types to derive successfully.

Context for Agents
The generated `impl CommData` never carries over the bounds required by the `CommData` supertraits. For a generic type

```rust
#[derive(Reloc, CommData)]
#[repr(C)]
pub struct Wrapper<T> {
    inner: T,
}
```

`Reloc` derive expands to `unsafe impl<T> Reloc for Wrapper<T> where T: Reloc {}`, but the code here emits `impl<T> CommData for Wrapper<T> { … }` with an empty `where` clause. The compiler therefore has to prove `Wrapper<T>: Reloc + Send + Debug + 'static` for *all* `T`, which is impossible and currently makes the derive fail for every generic struct/enum (`the trait bound Wrapper<T>: Reloc is not satisfied`).

Please push the required predicate onto the generated `where` clause before `split_for_impl()`, e.g.

```rust
let mut generics = input_args.generics.clone();
{
    let where_clause = generics.make_where_clause();
    where_clause.predicates.push(parse_quote!(
        #ident_name #type_generics: Reloc + ::core::fmt::Debug + ::core::marker::Send + 'static
    ));
}
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
```

(Using `#ident_name #type_generics` or `Self` for the predicate is fine.) This restricts the `CommData` impl to exactly the cases where the supertraits are satisfied, allowing generic types to derive successfully.

File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
Line: 94
Chain of Thought
  • Validation Decision: accepted
  • Confidence: 0.95
  • Likelihood Score: 5/5
  • Likelihood Reason: Any use of #[derive(CommData)] on a generic struct/enum will fail to compile. The code even has a comment saying "Add where clause if there are generics" but doesn't actually add any bounds. This is inevitable for any generic type usage.
  • Suggested Action: keep
  • Tool Calls Used: 2

Reasoning

The comment is accurate, well-supported by the code, and actionable.

  1. Accuracy: The CommData trait requires supertraits Reloc + Send + Debug + &#39;static (line 211 of com_api_concept.rs). The derive_comm_data macro (lines 70-72) clones the input generics and calls split_for_impl() without pushing any supertrait bounds onto the where clause. For a generic type like Wrapper&lt;T&gt;, this produces impl&lt;T&gt; CommData for Wrapper&lt;T&gt; {} with no bounds, making it impossible for the compiler to verify that Wrapper&lt;T&gt; satisfies the CommData supertraits for all T.

  2. Contrast with derive_reloc: The derive_reloc macro correctly handles generics by calling create_bounds_with_reloc() (which adds T: Reloc to type params) and then pushing field-type bounds. The derive_comm_data macro does neither.

  3. Introduced by this PR: The entire derive_comm_data function is new code introduced in this PR.

  4. Actionable: The comment provides a specific, correct fix — push #ident_name #type_generics: Reloc + Debug + Send + &#39;static onto the where clause before calling split_for_impl(). The code pattern is consistent with how derive_reloc handles similar issues.

  5. Ironic evidence: Line 70 has the comment "Add where clause if there are generics" but the subsequent code doesn't actually add any bounds — it just clones and splits.

Evidence Summary

1. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

2. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept/com_api_concept.rs
1: /********************************************************************************
2:  * Copyright (c) 2025 Contributors to the Eclipse Foundation
3:  *
4:  * See the NOTICE file(s) distributed with this work for additional
5:  * information regarding copyright ownership.
6:  *
7:  * This program and the accompanying materials are made available under the
8:  * terms of the Apache License Version 2.0 which is available at
9:  * https://www.apache.org/licenses/LICENSE-2.0
10:  *
...

Fix in Cursor

/// value: u32,
/// }
/// ```
/// ```

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended

[Documentation] Stray doc fence: line 64 opens a new ``` code block but the doc comment ends immediately after, leaving it unterminated. This will cause a rustdoc warning or error. Remove the orphaned fence.

Suggested change
/// ```
/// ```
#[proc_macro_derive(CommData, attributes(comm_data))]
Context for Agents
Stray doc fence: line 64 opens a new ` ``` ` code block but the doc comment ends immediately after, leaving it unterminated. This will cause a rustdoc warning or error. Remove the orphaned fence.

```suggestion
/// ```
#[proc_macro_derive(CommData, attributes(comm_data))]
```

File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
Line: 64
Chain of Thought
  • Validation Decision: accepted
  • Confidence: 0.97
  • Likelihood Score: 5/5
  • Likelihood Reason: The unterminated code fence is clearly present at line 64. Rustdoc will inevitably produce a warning or error about the unclosed code block when documentation is generated.
  • Suggested Action: keep
  • Tool Calls Used: 1

Reasoning

Tracing the code fence pairs in the doc comment for derive_comm_data (lines 18-64):

  1. Line 33 opens, line 35 closes → code block 1 (attribute example)
  2. Line 38 ignore opens, line 49 closes → code block 2 (usage example)
  3. Line 51 ignore opens, line 63 closes → code block 3 (custom ID example)
  4. Line 64 ``` opens → code block 4... NEVER CLOSED

Line 65 is #[proc_macro_derive(CommData, attributes(comm_data))], which is not a doc comment, so the doc comment ends at line 64. The fence at line 64 opens a code block with no closing fence, making it unterminated.

This is new code introduced by this PR (line 64 is marked as "added"). The fix is straightforward: remove line 64. The comment correctly identifies the issue and provides an actionable suggestion.

Evidence Summary

1. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
  • Lines: 32-66
32: /// It also supports an optional attribute to specify a custom ID string:
33: /// ```
34: /// #[comm_data(id = "CustomID_String")]
35: /// ```
36: /// If no custom ID is provided, a fully qualified type name is used as the ID.
37: /// Example usage:
38: /// ```ignore
39: /// pub unsafe trait Reloc {}
40: /// pub trait CommData: Reloc + Send + 'static {
41: ///    const ID: &'static str;
...

Fix in Cursor

Comment on lines +121 to +124
if !nv.path.is_ident("id") {
return Err(syn::Error::new_spanned(
&nv.path,
"Expected #[comm_data(id = \"...\")]",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory

[Maintainability] The error message here is generic. Since you've already confirmed the attribute is comm_data and are now checking the key inside it, you can provide a more specific error message to the user.

Suggested change
if !nv.path.is_ident("id") {
return Err(syn::Error::new_spanned(
&nv.path,
"Expected #[comm_data(id = \"...\")]",
"unexpected attribute key, expected `id`",

This principle applies to other error cases in this function as well (e.g., for non-string literal values). More specific error messages greatly improve the usability of procedural macros.

Context for Agents
The error message here is generic. Since you've already confirmed the attribute is `comm_data` and are now checking the key inside it, you can provide a more specific error message to the user.

```suggestion
                "unexpected attribute key, expected `id`",
```

This principle applies to other error cases in this function as well (e.g., for non-string literal values). More specific error messages greatly improve the usability of procedural macros.

File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
Line: 124
Chain of Thought
  • Validation Decision: accepted
  • Confidence: 0.82
  • Likelihood Score: 3/5
  • Likelihood Reason: Users who mistype the attribute key will see a generic "Expected #[comm_data(id = ...)]" message rather than one that specifically says their key name is wrong. While the generic message still shows the correct format, a more targeted message improves usability. This is a plausible usability issue for proc macro users.
  • Suggested Action: keep
  • Tool Calls Used: 1

Reasoning

The comment targets line 124 in the newly added extract_id_from_attribute function. At this point in the code, the function has already confirmed:

  1. The attribute is comm_data (line 103)
  2. The meta is a Meta::List (line 107)
  3. The parsed args are a Meta::NameValue (line 114)
  4. But the key is NOT id (line 121)

The error at line 124 currently says &#34;Expected #[comm_data(id = \&#34;...\&#34;)]&#34; — the same generic message used for multiple different validation failures (lines 110, 117, 124). The reviewer correctly identifies that since we know the specific failure mode (wrong key name), a more targeted message like &#34;unexpected attribute key, expected \id`"` would help users diagnose their mistake faster.

The comment is:

  • Accurate: It correctly identifies that the same error message is reused across different failure modes, reducing diagnostic precision.
  • Actionable: It provides a concrete replacement string.
  • Introduced by this PR: The entire function is new code added in this PR.
  • Valid suggestion: More specific proc macro error messages are a well-established best practice, and the suggestion is reasonable.

The existing generic message is not terrible (it does show the expected syntax), so this is more of a quality improvement than a critical bug, but it's still a valid and actionable review comment.

Evidence Summary

1. File Snapshot (HEAD)

  • File: score/mw/com/impl/rust/com-api/com-api-concept-macros/lib.rs
  • Lines: 100-153
100: /// Extract custom ID from #[comm_data(id = "...")] attribute
101: fn extract_id_from_attribute(attrs: &[syn::Attribute]) -> Result<Option<String>, syn::Error> {
102:     for attr in attrs {
103:         if !attr.path().is_ident("comm_data") {
104:             continue;
105:         }
106: 
107:         let Meta::List(list) = &attr.meta else {
108:             return Err(syn::Error::new_spanned(
109:                 &attr,
...

Fix in Cursor

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.

2 participants