Skip to content

refactor: compare foreign function declarations by resolved type - #1936

Open
ahomescu wants to merge 3 commits into
masterfrom
ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs
Open

refactor: compare foreign function declarations by resolved type#1936
ahomescu wants to merge 3 commits into
masterfrom
ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs

Conversation

@ahomescu

@ahomescu ahomescu commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stack created with GitHub Stacks CLIGive Feedback 💬

Some relevant output from Claude:

    Regression test: `test_reorganize_foreign_fn_type_identity`
    (`tests/snapshots/reorganize_foreign_fn_type_identity.rs`).
    Each translation unit has its own `buf`, and the two differ in field visibility,
    so the transform correctly keeps them apart and renames the second to `buf_1`.
    Both units then declare `fill(b: *mut buf)` — spelled identically, but naming a
    different type in each unit.
    
    `find_foreign_item` compares the two with `compatible_fn_prototypes`.
    A foreign item has no body and so no typeck results to resolve against,
    and the comparison falls through to a purely syntactic check that sees `*mut buf`
    against `*mut buf` and reports a match. The second declaration is dropped
    and both call sites are pointed at the first, so the caller written against
    `buf_1` now passes it to a function taking `*mut buf`:
    
        error[E0308]: mismatched types

    Comparing foreign functions by resolved type routed the comparison through
    `structural_eq_tys_impl`, whose array arm lets a zero-length array match an
    array of any length. That leniency is there for an `extern` array
    declaration, which C lets omit the length that the definition gives, and
    which we translate to a zero length; it does not hold inside a function
    signature. `*mut [c_int; 0]` and `*mut [c_int; 4]` are distinct Rust types
    with no coercion between them, so two declarations that differ only there
    are not interchangeable at a call site.
    
    `test_reorganize_foreign_fn_rename` regressed on exactly that: its two
    `compute` declarations were collapsed into one and `b_call` was rewritten
    to call a function it cannot pass its argument to.
    
    `TypeCompare` now carries an `exact_array_lens` flag, off by default so
    that the `extern` array declaration it exists for keeps matching its
    definition, and on for `compatible_fn_sigs` and `compatible_fn_prototypes`.
    An array length reached through a signature, including one nested in a
    pointed-to struct, has to match exactly.

    `find_foreign_item` decided whether two foreign functions were the same by
    handing their `FnDecl`s to `compatible_fn_prototypes`. That resolves each
    parameter through `opt_node_type`, but a foreign item has no body and so no
    typeck results. Two declarations from different modules whose parameters were
    merely spelled alike were reported identical, one was dropped, and its
    callers were rewritten to a function taking a different type.
    
    `compatible_foreign_fns` now compares function signatures with `compatible_fn_sigs`.
    `match_exports` already compared foreign functions against external ones
    this way; this brings the crate-local comparison in line with it. The
    syntactic path is kept as a fallback for a signature that will not resolve,
    which is also why the arity check added earlier still matters.
    
    Structurally identical types from different modules still compare equal, so
    this only separates declarations that genuinely differ.

@ahomescu
ahomescu changed the base branch from ahomescu/fix_reorganize_definitions/preserve_extern_link_name to ahomescu/fix_reorganize_definitions/mismatched_extern_arity July 25, 2026 00:57
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 3e52313 to af3c69c Compare July 25, 2026 01:40
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from af3c69c to fedfec1 Compare July 25, 2026 01:57
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from fedfec1 to f4c8e2f Compare July 25, 2026 02:08
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from f4c8e2f to 47b3cf0 Compare July 25, 2026 02:11
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch 2 times, most recently from 732a60c to 10990b4 Compare July 25, 2026 02:35
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 10990b4 to 60a774f Compare July 25, 2026 05:09
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 60a774f to 2bff68f Compare July 25, 2026 05:33
@ahomescu
ahomescu changed the base branch from ahomescu/fix_reorganize_definitions/mismatched_extern_arity to ahomescu/fix_reorganize_definitions/deterministic_externs July 25, 2026 05:41
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 2bff68f to 2d099d0 Compare July 25, 2026 05:41
@ahomescu
ahomescu changed the base branch from ahomescu/fix_reorganize_definitions/deterministic_externs to ahomescu/fix_reorganize_definitions/mismatched_extern_arity July 25, 2026 05:42
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 2d099d0 to 0380037 Compare July 25, 2026 05:42
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch 3 times, most recently from f423c43 to 8e87153 Compare July 25, 2026 06:16
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch 2 times, most recently from 15e6ee0 to 7bb56b6 Compare July 25, 2026 06:43
@ahomescu
ahomescu requested a review from thedataking July 25, 2026 06:43
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch 2 times, most recently from 1499b5b to 25aef51 Compare July 30, 2026 00:19
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 25aef51 to 82e9b1b Compare July 30, 2026 00:31
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 82e9b1b to 00ecfb8 Compare July 30, 2026 22:54
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 00ecfb8 to 43f8815 Compare July 30, 2026 22:56
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 43f8815 to a7399e5 Compare July 30, 2026 22:58
@thedataking
thedataking requested a review from fw-immunant July 31, 2026 22:28
@fw-immunant

Copy link
Copy Markdown
Contributor

The commit message for compare foreign function declarations by resolved type is exhaustingly long, but doesn't answer questions like:

  • when will the signature fail to resolve, resulting in syntactic comparison?
  • what are the specific E0308/E0603 errors referenced?

@fw-immunant fw-immunant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is functionally reasonable, modulo various comments. That said, the newly-added test does appear to be failing. I'd also prefer if commit messages generated by an LLM were demarcated as quotation (if included at all) rather than appearing like a message from the committer.

Comment thread c2rust-refactor/src/context.rs Outdated
type DefMapping = HashMap<DefId, DefId>;

#[derive(Clone, Copy)]
pub struct TypeCompare<'a, 'tcx: 'a, 'b> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A doc comment explaining the purpose of this type would be good to have, as the name TypeCompare is not entirely clear about how the data stored here relates to comparing types--is it a pass over source code which compares types, guidance for which types to compare, a specification of what to compare about types, the contextual information required to compare types, etc.

In practice it seems like it's the latter, and now being augmented with the penultimate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That seems independent of the current change, TypeCompare is a really old type. I'll make a separate PR for that.

@ahomescu ahomescu Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re this line: I'm not sure why we needed the new derive, so I deleted it.

Update: I found out why we needed it, it's because with_exact_array_lens takes a reference but produces a new object.

Comment thread c2rust-refactor/src/context.rs
Comment on lines 1587 to +1593
// We allow 0 length arrays to match any length arrays. This
// isn't exactly the C definition of compatible extern global
// array types with global array definitions, but it should be
// apply in practice as we translate empty extern array lengths
// into 0 length extern arrays.
if len1 != len2 && len1 != Some(0) && len2 != Some(0) {
let lenient = !self.exact_array_lens && (len1 == Some(0) || len2 == Some(0));
if len1 != len2 && !lenient {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any way we could avoid translating incomplete array types to zero-length arrays, e.g. by translating to a pointer to a DST or such? Our test coverage of what we currently do here seems pretty minimal (e.g. our c2rust-transpile/tests/snapshots/incomplete_arrays.c snapshot generates no code), but in practice I don't see how Rust code using elements from a zero-length array (even if its actual definition is foreign) can be sound.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We might need to do different things for each of the following cases:

  • extern array, e.g. extern int foo[];. I'm not sure we can use a DST here, since a pointer or reference to it becomes a fat pointer.
  • Function argument: void foo(int arg[]);. This is an array-to-pointer decay, so replacing it with int *arg might be fine.
  • Flexible array member at the end of a structure. I think this is the only one that could be a DST, but we would have to detect it.

I'm not sure the transpiler handles these cases differently, I'll have to go check.

Comment thread c2rust-refactor/src/transform/reorganize_definitions.rs Outdated
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from a7399e5 to a7355b9 Compare August 4, 2026 02:41
Base automatically changed from ahomescu/fix_reorganize_definitions/mismatched_extern_arity to master August 4, 2026 02:41
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch 3 times, most recently from e00fe34 to 7899fc2 Compare August 5, 2026 00:22
@ahomescu

ahomescu commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

I need to investigate what happened here with all the rebasing, because the test and the fix don't seem to match anymore.

Add a regression test for a bug where foreign function declarations get
merged based purely on how their types are spelled.

Two translation units each define their own struct named `buf`, and since
the structs differ, the transform correctly renames the second one to
`buf_1`. But both units also declare `fill(b: *mut buf)`. When
`find_foreign_item` compares these declarations via
`compatible_fn_prototypes`, it cannot resolve the types semantically
(foreign items have no body, hence no typeck results), so it falls back to
a syntactic comparison: `*mut buf` vs `*mut buf` looks identical, even
though each refers to a different struct. The duplicate declaration is
dropped and all callers are pointed at the first one, so code written
against `buf_1` ends up passing it to a function expecting `*mut buf`:

    error[E0308]: mismatched types
Type comparison lets a zero-length array match an array of any length.
That leniency exists so an `extern` array declaration, whose omitted C
length we translate as 0, can match its definition — but it also leaked
into function signature comparison, where `*mut [c_int; 0]` and
`*mut [c_int; 4]` are distinct, non-coercible types. As a result,
`test_reorganize_foreign_fn_rename` merged two different `compute`
declarations and rewrote a call to pass an argument of the wrong type.

Add an `exact_array_lens` flag to `TypeCompare`: off by default so
extern arrays still match their definitions, and turned on by
`compatible_fn_sigs` and `compatible_fn_prototypes` so array lengths
anywhere in a signature must match exactly.
`find_foreign_item` compared foreign functions via `compatible_fn_prototypes`,
which resolves parameters through `opt_node_type` — but foreign items have no
typeck results, so declarations from different modules that were merely
spelled alike merged, and callers of the dropped one were rewritten to a
function taking a different type.

Compare with `compatible_fn_sigs` instead, matching what `match_exports`
already does for external functions. The syntactic path remains as a fallback
for signatures that will not resolve, which is why the earlier arity check
still matters.
@ahomescu
ahomescu force-pushed the ahomescu/fix_reorganize_definitions/compare_extern_fn_sigs branch from 7899fc2 to b19824c Compare August 5, 2026 00:43
@ahomescu

ahomescu commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

when will the signature fail to resolve, resulting in syntactic comparison?

There are now more details in the comment that cover this case.

what are the specific E0308/E0603 errors referenced?

Is that relevant? Anyway, E0308 is Expected type did not match the received type. and E0603 is A private item was used outside its scope. but I think the latter is gone now.

/// after types were resolved.
def_mapping: Option<&'b DefMapping>,

/// Require array lengths to match exactly, instead of letting a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a fix for a pre-existing array issue that is now exposed by the more accurate signature comparison. I'm starting to think it should be its own separate PR, but in either case it needs to lands first. Thoughts?

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