N3332
_Record types

Published Proposal,

Previous Revisions:
None
Authors:
Paper Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC 9899 Programming Languages — C, ISO/IEC JTC1/SC22/WG14
Proposal Category:
Change Request, Feature Request
Target:
C2y

Abstract

User-controlled ways of handling compatibility for types in C, as a means to strengthen Type-Based Alias Analysis but also give users an explicit handle on increasing type diversity in C.

1. Changelog

1.1. Revision 0

2. Introduction and Motivation

The need for greater and more expansive compatibility due to various aspects of C programming — including macro-based generic programming for anonymous, in-line defined structures — has been increasingly high in C software. Furthermore, software which has been unable to get compatibility rules and aliasing rules to accommodate their code have worked largely by simply turning off strict type-based alias analysis with flags such as -fno-strict-aliasing. (One major compiler vendor has simply decided to not implement any serious type-based aliasing analysis and forego all of it.) This has put C in a tenuous situation, where its potentially rich type system is deeply at odds with some of its more serious and prominent users.

2.1. Type Compatibility Issues with Anonymous Types

During the discussion of improving tag compatibility from Martin Uecker’s [N3037], it was shown in a previous version of the paper that making all unnamed types compatible within the same translation unit would create a serious problem for type safety in C. Consider the following snippet of code:

typedef struct { int value; } fahrenheit;
typedef struct { int value; } celsius;

Under previous iterations of [N3037], these two would be considered compatible. This was seen as overreaching, as despite current C rules saying these two types are technically compatible if they are somehow accessed outside of the translation unit they are defined in, it violated safety within the translation unit itself. That is neither intended nor helpful, and could very well violate fundamental safety guarantees in a large body of software, particularly simulation and other units-heavy software. Therefore, this provision was removed from in [N3037] before the paper was approved for C23.

Speculation as to other ways of solving this problem were brought forward, such as making all anonymous structures identical except in the case where they are nested within a typedef declaration. None have been formally proposed yet, but the authors believe this subtle interaction would result in a greater complexity burden than is reasonably advisable. It is also unintuitive to go about things in this manner, as it would result in different behavior if a structure is named or not and specifically only in cases where a typedef is present or not.

2.2. Lack of Structural Typing

Additionally, other kinds of code contain repetitive definitions of the same structure which logically, spiritually, and for all intents and purposes are exactly identical. Take these various range definitions in the headers of Andre Weissflog’s sokol, a library that allows various different programming languages to interoperate with graphics in WASM, Nim, Zig, and others through C:

Due to the current compatibility rules, these types are not compatible. And yet, the author of sokol has stated the only reason they are different types is for compilation time optimization:

They’re all meant to be interchangeable. The reason they are separate is because I want the various sokol headers to be as standalone as possible (e.g. not require a shared "sokol_common.h" header).

September 2nd, 2024

Before this, Weissflog has also gone on to state:

I sometimes wish C had optional structurally typed structs (so that two differently named structs with the same interior are assignable to each other, would help to send data from one library to another without ‘entangling’ them via shared types.

August 23rd, 2024

This has also been a routine problem for developers who end up being the user of larger libraries or coordinating bigger projects, where disparate mathematics libraries and the like can be common. An example includes a frustration from a Doctor of Computer Science and Autodesk Meshmixer Creator Ryan Schmidt, offhandedly remarking on the current state of programming languages:

an utterly insane thing in (most? all?) programming languages is that you can have two separate math libraries, that each define their own vector-math types in exactly the same way, and there is no way to make MyVector3f = YourVector3f work transparently

July 30th, 2024

Indeed, given the different definitions of even a 2 or 3 dimensional vector in Datenwolf’s linmath.h versus recp’s cglm versus older libraries like feselva’s mathc, it can be frustrating coordinating these structures and types to work together with one another.

Certain languages like OCaml have types that use what is known as "structural typing", as is alluded to by Weissflog in his yearning for a better type system for C. These types only consider their members/fields/properties in order to determine type compatibility and identity. There are various "sub-flavors" of structural typing, but it effectively behaves exactly like C type compatibility with the caveat that the top-level structure or union name is not considered relevant when performing compatibility checks.

2.3. Macro-generic Data Structure Issues

Martin Uecker’s [N3037] enabled type-generic datastructures and macros with identical names at file and function scope with the same inner contents to be considered compatible types in C23. This meant that defining an e.g. dynamic array macro as:

#define array(T) struct array_##T##_t { size_t size; T* values; }

worked very well and no longer required a single "stable" pre-declaration of a given use of this type before using e.g. array(int) at function scope. However, this became slightly problematic with types that had spaces in them or were otherwise weird, such as with array(unsigned char). The workaround was to use typedefs, but it still left a problem: how come it was not possible to make an unnamed type that, within a single translation unit, compatible with other types like it? While the "strong typing" case is indeed important, it seemed that we had to sacrifice one use case for another: this is, ultimately, not a good place to be in the ecosystem.

2.4. A Solution

We are proposing a new keyword to allow users to explicitly annotate structures and unions which may have their top-level name ignored for compatibility purposes, as well as additional opt-in changes that are specified by the standard, and then further by implementations. The spelling of the keyword is _Record and _Record( record-attributes), and it creates new record modifiers which changes how types are considered compatible. Changing this allows explicit opt-in support for:

This alleviates the pressure from having to find a precise formulation of typedef or empty strucutres which could disrupt and negatively impact existing code pointed out in previous minuted discussion of [N3037], while also giving users better explicit control of compatibility and sharing between different disparate and unconnected libraries. The design for _Record types is as below.

3. Design

The design of _Record and it’s parenthesized counterpart is meant to accomplish a few critical goals that benefit both end-users and implementation vendors alike:

Note: This does NOT provide standards-blessed semantics for aliasing or assigning disparate types of different field types. That is covered under the vendor-provided record modifiers portion, and we hope that in providing that level of space we can further strengthen the case for type-based alias analysis by giving users and vendors more controls over layout-based compatibility. However, this proposal at this time does not have any mechanisms for allowing the punning of e.g. a structure with a single int32_t member and a structure with a single float member.

A quick example looks as follows:

typedef struct _Record range {
  void* ptr;
  size_t size;
} range;

typedef struct _Record slice {
  void* ptr;
  size_t size;
} slice;

void slice_func(slice value);

int main () {
  unsigned char data[1];
  struct range r = { .ptr = data, .size = sizeof(data) };
  struct slice s = { .ptr = nullptr, .size = 0 };

  slice_func(r); // ok
  struct range* slice_ptr_thru_range = &s; // ok
  r = s; // ok
	
  return 0;
}

This design achieves those goals, in various ways. First, let us review the syntax.

3.1. Syntax

Syntactically, _Record is a new keyword that is part of the declarator for a type definition. It goes between the tag type of struct or union and the identifier, and before the attribute specifier sequence:

struct _Record meow { char __padding; }; // ok
struct _Record [[some, attribs]] bark { char __padding; }; // ok
struct _Record { char __padding; }; // ok
struct _Record [[other_attrib]] { char __padding; }; // ok

union _Record purr { char __padding; }; // ok
union _Record [[some, attribs]] woof { char __padding; }; // ok
union _Record { char __padding; }; // ok
union _Record [[other_attrib]] { char __padding; }; // ok

Every definition of a type must agree and either have _Record on it or not have _Record on it. _Record does not need to be placed on the type when forwarding declaring or referencing the type anymore: it will always be considered a _Record type. A type that was previously defined without _Record cannot be redefined with the _Record keyword on it. A forward declaration also cannot contain _Record, because it only carries meaning on the defining declaration:

struct _Record meow; // constraint violation

struct _Record bark { char __padding; }; // ok

int main () {
  struct bark b = {}; // ok
  struct _Record bark b2 = {}; // constraint violation;
  struct _Record woof { char __padding; } w0 = {}; // ok
  struct _Record purr* p0; // constraint violation
  return 0;
}

3.2. _Record for Macro-Generic Datastructures

Given the example in § 2.3 Macro-generic Data Structure Issues, we can now side step any issues of non-identifier type names such as int* or unsigned char or similar by simply defining the structure to be an empty struct that is marked with _Record:

#define array(T) struct _Record { size_t size; T* values; }

The structure remains unnamed, which means no extra effort needs to be taken to avoid colliding with user namespaced entities either. It works at file and function scope without issue. And there’s no compatibility problems either, which preserves all of the intended effects of [N3037].

3.3. Shared Space vs. Fully Closed

The proposed semantics for _Record types is meant to be lenient and shared (also sometimes known as "viral"); that is, rather than needing both structures on both sides of a comparison, argument pass, or assignment to be annotated with _Record, only one of the structures or unions must be. This is very imporant because of preexisting code.

Requiring that both sides of an assignment or argument pass requires the arduous task of modifying every existing library to have better semantics. It is against the charter and general nature of C over the last 40 years to require sweeping changes or steep investment in existing code to make these things work. Many fundamental libraries can be perfectly valid and usable, even if not well-maintained or locked into a specific era VIA contractual obligation. To bring more immediate usability outside of closely-knit ecosystems, the design of this system is so that only one of the two types for an assignment is record modified. Similarly, if there are two different kinds of record modified types, the standard defines the "order" in which record modifications take priority. In general, this priority can be considered as simply being from "most lenient" to "least lenient".

Note that this does not subject any piece of code using known and well-understood mechanisms such as incomplete types / private source file definitions to suddenly be more or less compatible than they used to be. Record types are a property only of types with source-available definitions marked with _Record in a translation unit. This does, however, add a new way to access an old problem in C.

3.3.1. Type-Based Alias Analysis under "Viral" Compatibility

Consider the following (probably illegal, but nonetheless compiling and running) code:

// alice.h
typedef struct alice_vec { int x; int y; } alice_vec;
// bob.h
typedef struct bob_vec { int x; int y; } bob_vec;
// TU #1
#include <alice.h>
#include <bob.h>

void foo(alice_vec *a, const bob_vec *b) {
  a.x = b.y;
  a.y = b.x; // TBAA says this is independent of the first read/write
}
// TU #2
#include <alice.h>
#include <bob.h>
#include <foo.h>

void bar(alice_vec a) {
  union my_vec { alic_vec a; bob_vec b; } u = {a};
  foo(&u.a, &u.b); // erm......
}

A similar issue can arise from the use of _Record, and the fact that an alias happened will escape the this translation unit without any indication:

// TU #3
#include <alice.h>
#include <bob.h>
#include <foo.h>

// my_vec compatible with alice_vec and bob_vec
typedef struct _Record my_vec { int x; int y; } my_vec;

void baz(alice_vec a) {
  const bob_vec* b = (my_vec*)&a;
  foo(&u.a, &u.b); // ... ah.
}

In both cases, the first translation unit has no idea about the aliasing of the second and third translation units used here. One example uses a union to do it, the other uses a record type that is compatible with both types. This is one of the weaknesses of _Record as a whole with a shared space / "viral" compatibility. A similar problem happens using void casts, without any intermediate _Record behavior at all:

// TU #4
#include <alice.h>
#include <bob.h>
#include <foo.h>

void quux(alice_vec a) {
  const bob_vec* b = (void*)&a;
  foo(&u.a, &u.b); // ... hrm.
}

3.3.2. Should This Be Closed?

As stated in § 3.3 Shared Space vs. Fully Closed, we believe the shared nature of _Record provides far more benefit to the ecosystem. Requiring everyone to annotate their code to turn on the benfits is a much less compelling design. This is also not a problem exclusive to this design: as demonstrated, this can happen with unions or void* casts. It does not make the situation better, but it also does not make the situation any worse than it is.

NOTE: C++ technically is defeated by the void* situation, but not the union situation because it has explicit wording about which member of a union is an active union, and it is illegal to access a union through its non-active member.

There are some potential directions that could fix this.

The first one is disallowing pointer compatibility if both types aren’t _Record types, but to allow things like argument passing and assignment so long as one is marked compatible through _Record. Having both types be record types means that alice_vec and bob_vec would both need to be marked as _Record, and that would clue a potential optimizer in to know that they might be potentially aliasing unless someone explicitly used the restrict keyword. This is somewhat of a compromise between many positions, but drastically decreases the usability of _Record for a wide variety of use cases. Macro generic programming is not affected.

The second course of action is to require that _Record is placed on all types in order for any of its features to apply, meaning all of pointer compatibility, assignment, and argument passing are all disallowed unless both types are annotated. We think this is a poor idea and will create greater friction in adopting this feature to solve the present problems of the ecosystem, although it will have no effect on macro generic programming.

The third course of action is to simply wait for whatever fix for the void* and union situations the Memory Model TS produces. This is a more general solution, and could solve it for everyone while not impeding this design. Both pointer casting and macro generic programming are not affected in this case.

Our recommendation is for the third course of action, as this does not introduce a new footgun that has not been present since the dawn of C code.

3.4. Generic Selection

The use of generic selection in codebases will be impacted by the changes here due to using type compatibility as a mechanism for improvement. However, at this time, generic selection has its issues due to not being based on better, stricter type matching semantics. While Aaron Ballman’s [N3260] provided some of that through its own opt-in mechanism by providing a type rather than an expression, it still leaves the matching semantics dubious in cases with expressions. This includes for expressions which result in record types; if someone provides what are potentially 2 different types that match due to _Record, unintentional matches or sudden compilation errors will make things worse.

There is no impact on existing code due to this feature being opt-in from users, but it can affect code written in the future with this feature. A follow-on paper will be written to address generic selection as a whole rather than attempting to do a piecemeal modification of the fundamentally incomplete generic selection feature at this time.

3.5. Vendor Implementation Space

When discussing this feature with users, they made it clear they had many other ideals for this sort of compatibility fixup. Some voiced a need to make structures with the same field order and types but different top-level AND field names should be compatible, such as:

typedef struct _Record liba_vec2 {
  float x;
  float y;
} liba_vec2;

typedef struct _Record libd_vec2 {
  float mx;
  float my;
} libd_vec2; // names different, still not compatible even with _Record

void f(liba_vec2 v);

int main () {
  liba_vec2 vec_a = {};
  libd_vec2 vec_d = { 1.0f, 1.0f };
  libd_vec2* d_thru_a = &vec_a; // constraint violation
  vec_a = vec_d; // constraint violation
  f(vec_d); // constraint violation
  return 0;
}

Others voiced that they would like to have more custom logic to make the sequence of sockaddr structures found in <sys/socket.h> (which may necessitate encoding additional runtime checks and similar into the _Record type that would be triggered upon casting from an e.g. struct sockaddr to <netinet/in.h>’s struct sockaddr_in6). It is clear that the ground for extensions to the idea of "custom compatibility" is fertile, and that there are many cases that cannot be reasonably covered by this proposal nor its inclusion into the C standard in-general. Therefore, the best way to allow for extension, experimentation, and customization is through the _Record() syntax carried forward as an "extension place" to this proposal.

The syntax follows that of attributes, providing _Record(foo) and _Record(foo(maybe_args)) as a reserved syntax for the C standard. Implementations can inject their own vendor-specific semantics with _Record(vendor::bark) and _Record(vendor::bark(maybe_args)). A caveat for flexible _Record() syntax is that any misunderstood or unknown _Record() declarations causes immediate compilation failure (i.e., is a constraint violation). This can be mitigated with the usual strategies (macros and feature detection); but, it is notable since this is different from attributes which are ignorable by the implementation.

3.6. _Record(types) for even MORE Compatibility Adjustment

The prior example in § 3.5 Vendor Implementation Space showing identical type layout but differing names was a common ask for the addition of _Record by itself. While _Record solved the original problem of 2 anonymous structure definitions put behind typedefs for the purpose of strong typing an API to prevent accidental assignments and unintional casts, it did not satisfy the full gamut of issues people have had in the wild. Threfore, as an addendum to just plain _Record, we utilize the syntax from § 3.5 Vendor Implementation Space to add a standard-recognized _Record(types). The types is a standard-specified identifier that can go in parentheses for _Record. This syntax produces standard-defined record modifiers for making liba_vec2 and libd_vec2 in § 3.5 Vendor Implementation Space be considered compatible types:

typedef struct _Record(types) liba_vec2 {
  float x;
  float y;
} liba_vec2;

typedef struct _Record(types) libd_vec2 {
  float mx;
  float my;
} libd_vec2; // compatible with libd_vec2

void f(liba_vec2 v);

int main () {
  liba_vec2 vec_a = {};
  libd_vec2 vec_d = { 1.0f, 1.0f };
  libd_vec2* d_thru_a = &vec_a; // ok
  vec_a = vec_d; // ok
  f(vec_d); // ok
  return 0;
}

All of the properties associated with properly passing two compatible types work just fine in this case, since it has been marked as only requiring that the types and their ordering must be identical and the names are ignored.

At the moment, we we have only provided _Record(types) or naked _Record/_Record(), we do not have to concern ourselves with "what if there are multiple attributes in the record modifier" for this proposal. But, to make clear the intention: for standard record modifiers, the standard manages how/if they can mix, and what is the effect of any allowed combinations. For implementation ones, it is on the implementation to define that behavior.

3.7. (NOT PROPOSED) Future Direction: _Record(const) for (nested) qualifier Compatibility Adjustment

Another asked for feature is potentially marking types as being compatible if their types only differ by certain qualifiers: for example, an "array of spans" is a common data structure for asynchronous workloads, and employing const correctness in certain structures means that two types that are morally and spiritually compatible end up not being compatible:

typedef struct span {
  void* data;
  size_t size;
} span;

typedef struct c_span {
  const void* ptr;
  size_t len;
} c_span; // not compatible with the above

int submit_async_work(c_span* workoads, size_t workloads_size);

int main () {
  span lots_of_work[10'000]; = {};
  /* lots of assignment/work here... */

  submit_async_work(lots_of_work, 10'000); // constraint violation;
                                           // must copy to c_span array
}

This can lead to excessive copying of the data structure if only to get it into the right "form", as neither C nor C++ can handle levels of nested const in this manner for compatibility purposes. Even if _Record(types) ignores the names, the corresponding members are still not compatible. A new record modifier can, potentially, solve that problem by marking these two structures compatible. Through a new _Record(const) modifier, such structures in asynchronous work could be considered both compatible and aliasable, saving both time and energy.

In general, we support such a sentiment. The reason we propose types in this proposal but not const is that the effect of the name on the actual layout and access of the type is negligible: however, const on the members of a structure or union may have potential unforseen consequences when the power of aliasing compatible types is brought up. Therefore, it is not included in this proposal. We encourage implementations to implement _Record(vendor::const) and report back if this poses any issues that should be known over the next few years. In general, we expect there not to be any show-stopping issues, but we would like to be sure. Usage could be like so:

typedef struct _Record(types, vendor::const) span {
  void* data;
  size_t size;
} span;

typedef struct _Record(types, vendor::const) c_span {
  const void* ptr;
  size_t len;
} c_span; // nnow it's compatible

int submit_async_work(c_span* workoads, size_t workloads_size);

int main () {
  span lots_of_work[10'000]; = {};
  /* lots of assignment/work here... */

  submit_async_work(lots_of_work, 10'000); // constraint violation;
                                           // must copy to c_span array
}

4. Prior Art & Implementation Experience

There is no prior art for this. We would like to get a "temperature" on this proposal, so we can go spend 2 or so years talking to Clang and GCC to get implementations going and off the ground. Having a proposal and talking to WG14 is the first step to proper standardization, as a hope to ensure the feature has a united featureset that all vendors can use.

5. Specification

The following wording is against the latest draft of the C standard.

NOTE: This proposal does not modify generic selection; this is intentional. Generic selection needs a better mechanism to match types than "compatibility"; right now, changes to compatibility can deeply affect generic selection in a way that is not good. A follow-on paper will address and resolve these issues separately.

5.1. Modify Section §6.2.7 Tags

6.2.7 Compatible type and composite type

Two types are compatible types if they are the same. Additional rules for determining whether two types are compatible are described in 6.7.3 for type specifiers, in 6.7.4 for type qualifiers, and in 6.7.7 for declarators.45) Moreover, two complete structure, union, or enumerated types declared with the same tag are compatible if members satisfy the following requirements:

— there shall be a one-to-one correspondence between their members such that each pair of corresponding members are declared with compatible types;

— if one member of the pair is declared with an alignment specifier, the other is declared with an equivalent alignment specifier;

— and, if one member of the pair is declared with a name, the other is declared with the same name.

For two structures, corresponding members shall be declared in the same order. For two unions declared in the same translation unit, corresponding members shall be declared in the same order. For two structures or unions, corresponding bit-fields shall have the same widths. For two enumerations, corresponding members shall have the same values; if one has a fixed underlying type, then the other shall have a compatible fixed underlying type. For determining type compatibility, anonymous structures and unions are considered a regular member of the containing structure or union type, and the type of an anonymous structure or union is considered compatible to the type of another anonymous structure or union, respectively, if their members fulfill the preceding requirements.

Furthermore, two structure, union, or enumerated types declared in separate translation units are compatible in the following cases:

— both are declared without tags and they fulfill the preceding requirements;

— both have the same tag and are completed somewhere in their respective translation units and they fulfill the preceding requirements;

— both have the same tag and at least one of the two types is not completed in its translation unit.

Additionally, if one of two structure or union types is a standard record type, then the types are compatible in the additional following cases:

— if one of the types is a types-only record type (✨6.7.3.3), both the tag of the structures or unions and the names of its corresponding members are not considered while fulfilling the preceding requirements;

— otherwise, if one of the types is a basic record type (✨6.7.3.3), the tag of the structures or unions is not considered while fulfilling the preceeding requirements;

Otherwise, the structure, union, or enumerated types are incompatible.

5.2. Modify Section §6.7.3.2 "Structure and union specifiers"

6.7.3.2 Structure and union specifiers
Syntax

struct-or-union-specifier:

struct-or-union record-modifieropt attribute-specifier-sequenceopt identifieropt { member-declaration-list }

struct-or-union attribute-specifier-sequenceopt identifier

5.3. Add a new Section §6.7.3.3 "Record modifiers"

6.7.3.3 Structure and union specifiers
Syntax

record-modifier:

_Record
_Record ( attribute-list )

A structure or union type with a record modifier is a record type. A record type with a record modifier of the form _Record, _Record ( ), or _Record followed by a parenthesized standard attribute listed in this subclause is a standard record type. Otherwise, it is an implementation record type.

Constraints

If present, the identifier in a standard attribute for a record moifier shall be types. Standard attributes shall only be present once in the attribute list for a record modifier.

A structure or union shall contain identical record modifiers on all of its definitions, if present. If a structure or union in two different translation units does not contain identical record modifiers, the behavior is undefined.

A record modifier shall not contain an attribute unrecognized by the implementation.

Semantics

Record types provide additional ways to specify the compatibility of types that would otherwise be incompatible.

The use of standard attributes in record modifiers is defined by this document. The use of attribute prefixed token sequences in record modifiers is implementation-defined. The order in which attribute tokens in a record modifier is not significant.

A record modifier of the form _Record or _Record ( ) classifies a standard record type as a basic record type. Basic record types modify their compatibility with other types as specified in 6.2.7.

A record modifier which contains the attribute token types classifies a standard record type as a types-only record type. Types-only record types modify their compatibility with other types as specified in 6.2.7.

Implementation record types, if any, have implementation-defined semantics.

EXAMPLE 1 Record modifiers allows for assignment between types that are meant to be related but otherwise would not be considered compatible:

typedef struct _Record catlib_range {
  void* ptr;
  size_t size;
} catlib_range;

typedef struct _Record doglib_slice {
  void* ptr;
  size_t size;
} doglib_slice;

void doglib_func(doglib_slice value);
void catlib_func(catlib value);
void doglib_ptr_func(doglib_slice *mem);
void catlib_ptr_func(catlib *mem);

int main () {
  unsigned char data[1];
  catlib_range cats = { .ptr = data, .size = sizeof(data) };
  doglib_slice dogs = { .ptr = data, .size = sizeof(data) };

  // dogs and cats, working together
  doglib_func(cats); // ok
  catlib_func(cats); // ok
  doglib_func(dogs); // ok
  catlib_func(dogs); // ok

  doglib_ptr_func(&cats); // ok
  catlib_ptr_func(&cats); // ok
  doglib_ptr_func(&dogs); // ok
  catlib_ptr_func(&dogs); // ok
  return 0;
}

EXAMPLE 2 Types-only record types allows for compatibility even if the name of members are different, even if only one of the types is considered compatible:

typedef struct liba_vec2 {
  float x;
  float y;
} liba_vec2;

typedef struct _Record(types) libd_vec2 {
  float mx;
  float my;
} libd_vec2; // compatible with libd_vec2

void f(liba_vec2 v);

int main () {
  liba_vec2 vec_a = {};
  libd_vec2 vec_d = { 1.0f, 1.0f };
  libd_vec2* d_thru_a = &vec_a; // ok
  vec_a = vec_d; // ok
  f(vec_d); // ok
  return 0;
}

EXAMPLE 3 Compatibility between types with different record modifiers works by checking: if either of the type is a types-only record type, then, if either is a basic record type; then, typical non-record type compatibility rules.

struct meow {
  char a;
};

struct _Record miaou {
  char b;
};

struct _Record(types) nya {
  char c;
};

int main () {
  struct meow ecat = {};
  struct miaou fcat = {};
  struct nya jcat = {};
	
  ecat = fcat; // constraint violation: incompatible types (basic record type),
               // tag names ignored, member names are different
  ecat = jcat; // ok: compatible types (types-only record type),
               // tag names ignored, member names ignored

  fcat = ecat; // constraint violation: incompatible types (basic record type),
               // tag names ignored, member names are different
  fcat = jcat; // ok: compatible types (types-only record type),
               // tag names ignored, member names ignored

  jcat = ecat; // ok: compatible types (types-only record type),
               // tag names ignored, member names ignored
  jcat = fcat; // ok: compatible types (types-only record type),
               // tag names ignored, member names ignored

  return 0;
}
Recommended Practice

Implementations interested in blessing specific forms of assignment, casting, and so-called "type-punning" between types typically not considered related should use implementation record types as a means of providing such behaviors to their end-users.

5.4. Modify Section §6.7.3.4 Tags

6.7.3.4 Tags
Constraints

A type specifier of the form

struct-or-union record-modifieropt attribute-specifier-sequenceopt identifieropt { member-declaration-list }

5.5. Automatically Update Annex J entries for implementation-defined behavior

6. Acknowledgements

Many thanks to the individuals who voiced their frustration with C’s current system to help spur this proposal along. Thanks to Martin Uecker for addressing the original problem, and Jens Gustedt for the compelling counterexample.

References

Informative References

[CGLM]
recp. cglm. September 2nd, 2024. URL: https://github.com/recp/cglm
[LINMATH-H]
Datenwolf. linmath.h. September 2nd, 2024. URL: https://github.com/datenwolf/linmath.h
[MATHC]
feselva. mathc. September 2nd, 2024. URL: https://github.com/felselva/mathc
[N3037]
Martin Uecker. N3037 - Improved Rules for Tag Compatibility. July 7th, 2022. URL: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3037.pdf
[N3260]
Aaron Ballman. N360 - Generic selection expression with a type operand. May 12th, 2024. URL: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3260.pdf
[OCAML-TYPES]
OCaml Contributors. OCaml Basic Data Types and Pattern Matching. September 2nd, 2024. URL: https://ocaml.org/docs/basic-data-types
[STRUCTURAL-TYPING]
Wikipedia. Structural type system. September 2nd, 2024. URL: https://en.wikipedia.org/wiki/Structural_type_system