> On Mar 28, 2025, at 9:05 AM, Qing Zhao <qing.z...@oracle.com> wrote: > > > >> On Mar 28, 2025, at 08:51, Yeoul Na <yeoul...@apple.com> wrote: >> >> >> >>> On Mar 27, 2025, at 9:17 AM, Qing Zhao <qing.z...@oracle.com> wrote: >>> >>> Yeoul, >>> >>> Thanks for the writeup. >>> >>> So, basically, This writeup insisted on introducing a new “structure scope” >>> (similar as the instance scope in C++) into C language ONLY for counted_by >>> attribute: >>> >>> 1. Inside counted_by attribute, the name lookup starts: >>> >>> A. Inside the current structure first (the NEW structure scope added to >>> C); >>> B. Then outside the structure; (other current C scopes, local scope or >>> global scope) >>> >>> 2. When trying to reference a variable outside of the structure scope that >>> name_conflicts with >>> a structure member, a new builtin function “__builtin_global_ref” is >>> introduced for such >>> purpose. >>> >>> ( I think that __builtin_global_ref might not accurate, because the outer >>> scope might be either global scope or local scope) >> >> >> Clarification: __builtin_global_ref will see the global scope directly. This >> is similar to global scope resolution syntax (‘::’) in C++. > > Yes, that’s my thought too. > > Then, you still need another builtin to refer to the local variable with the > same name as the structure member, for example, > In the below example, if the “len” inside the counted_by refers to the “const > int len = 20”, how do you specify this? >> >> constexpr int len = 10; >> >> void foo (void) >> { >> const int len = 20; >> >> struct s { >> int len; >> int *__counted_by(__builtin_global_ref(len)) buf; // refers to global >> ‘len' >> }; >> } >> >> Here are some reasons why we chose to provide a global scope resolution >> builtin, not a builtin to see an outer scope or just a local scope: >> >> 1) The builtin is a substitute for some “scope resolution specifier”. Scope >> specifiers typically meant to choose a “specific" scope. >> 2) To the best of my knowledge there is no precedence in any other C family >> language to provide a scope resolution for local scopes. > > However, there is possibility that in the above example, the “len” might > refer to the local variable len, not the global one. How do you specify that? > >> 3) Name conflicts with local variables can be easily renamed. > > Then more source code change in different places is needed, I am not sure > whether this is easy to do in some cases.
The change will be only to the local variable and IMOH, renaming local variable would likely not a trouble in practice. In fact, when a local variable shadows a global variable like below, there is currently no way to choose the global variable ‘var’ in C. The programmer renames their local variable in the case like this. int var; void foo(void) { int var; // ... var = 0; // actually wanted to use a global var } That said, I do not object to introducing some builtin to specify local scope at some point, if there is a compelling use case is found during adoption. > >> 4) If we provide a builtin that selects outer scope instead, there is no way >> to choose a global ‘len' if it’s shadowed by a local variable, so then the >> member name has to be renamed anyway in order to choose a global `len`. > > Yes, that’s true. So maybe two builtins are needed? > >> 5) This way, code can be written compatibly both in C and C++. >> >>> >>> 3. Where there is confliction between counted_by and VLA such as: >>> >>> constexpr int len = 10; >>> >>> struct s { >>> int len; >>> int *__counted_by(len) buf; // refers to struct member `len`. >>> int arr[len]; // refers to global constexpr `len` >>> }; >>> >>> Issue compiler warning to user to ask the user to use __builtin_global_ref >>> to distinguish. >> >> Additionally, our proposal suggests __builtin_member_ref to explicitly use a >> member in a similar situation. >> The builtin could be replaced by ‘__self' or some other syntax once the >> standard committee decides in the future, but earlier in the thread JeanHeyd >> pointed out that: >> >> "I would like to gently push back about __self__, or __self, or self, >> because all of these identifiers are fairly common identifiers in code. When >> I writing the paper for __self_func ( >> https://thephd.dev/_vendor/future_cxx/papers/C%20-%20__self_func.html ), I >> searched GitHub and other source code indexing and repository services: >> __self, __self__, and self has a substantial amount of uses. If there's an >> alternative spelling to consider, I think that would be helpful." >> >> Thus, I think instead of trying to stick to a certain syntax right now, >> using some builtin will allow us to easily migrate to a new syntax by >> guarding the current usage under a macro. >> >> Writing the builtin could be cumbersome but this shall be written only when >> there is an ambiguity. Btw, I’m open to any other name suggestions for the >> builtins! > > I think that it’s better to stick to one approach: Just to clarify by "to stick to a certain syntax”, I meant there seems to be no agreement which syntax to use “__self.”, “.”, "this->”, etc for the member scope resolution syntax. JeanHeyd pointed out earlier that the spelling “__self” has a substantial amount of uses already in C code. And dot has other problems too. It seems it’s gonna take a while to choose the right one. And we might have to change yet again when the C standard committee decides on some other syntax. Therefore, I think using builtins instead will help because it can be macro-defined easily. > > A. Add a new keyword “__self”/ or __builtin_self() to explicitly refer to the > member variable, keep all other no changes. > > OR: > > A. Add one new instance scope into C, lookup the name inside the new scope > first, then outer scope. If try to refer to variables outside the instance > scope, using new added “scope resolution specifier”, such as > __builtin_global_… __builtin_local_… for that purpose. > For A, fixing the VLA inside structure to have the same lookup rule as > counted-by. > > > Anything mixing these two is not good to me... Ok, I would be fine with the second option. >> >>> >>> Are the above the correct understanding of your writeup? >> >> Yes, it’s mostly correct, except some clarifications I made above. Thank you! > > Thank you for the clarifications. > > Qing >> >>> >>> >>> From my understanding: >>> >>> 1. This design started from the C++’s point of view by adding a new >>> “structure scope” to C; >>> 2. This design conflicts with the current VLA default scope rule (which >>> based on the default C scopes) in C. >>> In the above example that mixes counted_by and VLA, it’s so weird that >>> there are two difference name >>> lookup rules inside the same structure. >>> It’s clearly a design bug. Either VLA or counted_by need to be fixed to >>> make them consistent. >>> >>> >>> I personally do not completely object to introduce a new “structure scope” >>> into C, but it’s so hard for me to accept >>> that there are two different name lookup rules inside the same structure: >>> one rule for VLA, another rule for counted_by >>> attribute. (If introducing a new “structure scope” to C, I think it’s >>> better to change VLA to “structure scope” too, not sure >>> whether this is feasible or not) >>> >>> I still think that introduce a new keyword “__self” for referring member >>> variable inside structure without adding >>> a new “structure scope" should be the best approach to resolve this issue >>> in C. >>> >>> However, I am really hoping that the discussion can be converged soon. So, >>> I am okay with adding a new “structure scope” >>> If most of people agreed on that approach. >> >> Thanks for the flexibility! >> >>> >>> Qing >>> >>> >>>> On Mar 26, 2025, at 12:59, Yeoul Na <yeoul...@apple.com> wrote: >>>> >>>> Hi all, >>>> >>>> Thanks for all the discussions. >>>> >>>> I posted the design rationale for our current approach in >>>> https://discourse.llvm.org/t/rfc-forward-referencing-a-struct-member-within-bounds-annotations/85510. >>>> This clarifies some of the questions that are asked in this thread. The >>>> document also proposes diagnostics to mitigate potential ambiguity, and >>>> propose new builtins that can be used as a suppression and disambiguation >>>> mechanism. >>>> >>>> Best regards, >>>> Yeoul >>>> >>>>> On Mar 26, 2025, at 9:11 AM, Yeoul Na <yeoul...@apple.com> wrote: >>>>> >>>>> Sorry for the delay. >>>>> >>>>> I’m planning on sending out our design rationale of the current approach >>>>> without the new syntax today. >>>>> >>>>> - Yeoul >>>>> >>>>>> On Mar 14, 2025, at 9:22 PM, John McCall <rjmcc...@apple.com> wrote: >>>>>> >>>>>> On 14 Mar 2025, at 15:18, Martin Uecker wrote: >>>>>> Am Freitag, dem 14.03.2025 um 14:42 -0400 schrieb John McCall: >>>>>> On 14 Mar 2025, at 14:13, Martin Uecker wrote: >>>>>> Am Freitag, dem 14.03.2025 um 10:11 -0700 schrieb David Tarditi: >>>>>> Hi Martin, >>>>>> The C design of VLAs misunderstood dependent typing. >>>>>> They probably did not care about theory, but the design is >>>>>> not inconsistent with theory. >>>>>> This is almost true, but for bad reasons. The theory of dependent types >>>>>> is heavily concerned with deciding whether two types are the same, and C >>>>>> simply sidesteps this question because type identity is largely >>>>>> meaningless in C. Every value of variably-modified type is (or decays >>>>>> to) a pointer, and all pointers in C freely convert to one another >>>>>> (within the object/function categories). _Generic is based on type >>>>>> compatibility, not equality. So in that sense, the standard doesn’t say >>>>>> anything inconsistent with theory because it doesn’t even try to say >>>>>> anything. >>>>>> The reason it is not quite true is that C does have rules for compatible >>>>>> and composite types, and alas, those rules for variably-modified types >>>>>> are not consistent with theory. Two VLA types of compatible element type >>>>>> are always statically considered compatible, and it’s simply UB if the >>>>>> sizes aren’t the same. The composite type of a VLA and a fixed-size >>>>>> array type is always the fixed-size array type. The standard is >>>>>> literally incomplete about the composite type of two VLAs; if you use a >>>>>> ternary operator where both operands are casts to VLA types, the >>>>>> standard just says it’s straight-up just undefined behavior (because one >>>>>> of the types has a bound that’s unevaluated) and doesn’t even bother >>>>>> telling us what the static type is supposed to be. >>>>>> Yes, I guess this is all true. >>>>>> But let's rephrase my point a bit more precisely: One could take >>>>>> a strict subset of C that includes variably modified types but >>>>>> obviously has to forbid a lot other things (e.g. arbitrary pointer >>>>>> conversions or unsafe down-casts and much more) and make this a >>>>>> memory-safe language with dependent types. This would also >>>>>> require adding run-time checks at certain places where there >>>>>> is now UB, in particular where two VLA types need to be compatible. >>>>>> Mmm. You can certainly subset C to the point that it’s memory-safe, but >>>>>> it wouldn’t really be anything like C anymore. As long as C has a heap, >>>>>> I don’t see any path to achieving temporal safety without significant >>>>>> extensions to the language. But if we’re just talking about spatial >>>>>> safety, >>>>>> then sure, that could be a lot closer to C today. >>>>>> Is that your vision, then, that you’d like to see the same sort of checks >>>>>> that -fbounds-safety does, but you want them based firmly in the language >>>>>> as a dynamic check triggered by pointer type conversion, with bounds >>>>>> specified using variably-modified types? It’s a pretty elegant vision, >>>>>> and >>>>>> I can see the attraction. It has some real merits, which I’ll get to >>>>>> below. >>>>>> I do see at least two significant challenges, though. >>>>>> The first and biggest problem is that, in general, array bounds can only >>>>>> be >>>>>> expressed on a pointer value if it’s got pointer to array type. Most C >>>>>> array >>>>>> code today works primarily with pointers to elements; programmers just >>>>>> use >>>>>> array types to create concrete arrays, and they very rarely use pointers >>>>>> to >>>>>> array type at all. There are a bunch of reasons for that: >>>>>> • Pointers to arrays have to be dereferenced twice: (*ptr)[idx] instead >>>>>> of ptr[idx]. >>>>>> • That makes them more error-prone, because it is easy to do pointer >>>>>> arithmetic at the wrong level, e.g. by writing ptr[idx], which will >>>>>> stride by multiples of the entire array size. That may even pass the >>>>>> compiler without complaint because of C’s laxness about conversions. >>>>>> • Keeping the bound around in the pointer type is more work and >>>>>> doesn’t do >>>>>> anything useful right now. >>>>>> • A lot of C programmers dislike nested declarator syntax and can’t >>>>>> remember >>>>>> how it works. Those of us who can write it off the top of our heads are >>>>>> quite atypical. >>>>>> Now, there is an exception: you can write a parameter using an array >>>>>> type, >>>>>> and it actually declares a pointer parameter. You could imagine using >>>>>> this >>>>>> as a syntax for an enforceable array bound for arguments, although the >>>>>> committee did already decide that these bounds were meaningless without >>>>>> static. Unfortunately, you can’t do this in any other position and still >>>>>> end up with just a pointer, so it’s not helpful as a general syntax for >>>>>> associating bounds with pointers. >>>>>> The upshot is that this isn’t really something people can just adopt by >>>>>> adding annotations. It’s not just a significant rewrite, it’s a rewrite >>>>>> that >>>>>> programmers will have very legitimate objections to. I think that makes >>>>>> this >>>>>> at best a complement to the “sidecar” approach taken by -fbounds-safety >>>>>> where we can track top-level bounds to a specific pointer value. >>>>>> The second problem is that there are some extralingual problems that >>>>>> -fbounds-safety has to solve around bounds that aren’t just local >>>>>> evaluations of bounds expressions, and a type-conversion-driven approach >>>>>> doesn’t help with any of them. >>>>>> As you mentioned, the design of variably-modified types is based on >>>>>> evaluating the bounds expression at some specific point in the program >>>>>> execution. Since these types can only be written locally, the evaluation >>>>>> point is obvious. If we wanted to dynamically enforce bounds during >>>>>> initialization, it would simply be another use of the same computed >>>>>> bound: >>>>>> int count = ...; >>>>>> int (*ptr)[count * 10] = source_ptr; >>>>>> >>>>>> Here we would evaluate count * 10 exactly once and use it both as (1) >>>>>> part >>>>>> of the destination type when initializing ptr with source_ptr and (2) >>>>>> part of the type of ptr for all uses of it. For example, if source_ptr >>>>>> were of type int (*)[100], we would dynamically check that >>>>>> count * 10 <= 100. This all works perfectly with an arbitrary bounds >>>>>> expression; it could even contain an opaque function call. >>>>>> Note that we don’t need any special behavior specifically for >>>>>> initialization. If we later assign a new value into ptr, we will still be >>>>>> converting the new value to the type int (*)[< count * 10 >], using the >>>>>> value computed at the time of declaration of the variable. This model >>>>>> would >>>>>> simply require that conversion to validate the bounds during assignment >>>>>> just >>>>>> as it would during initialization. >>>>>> Now, with nested arrays, variance does become a problem. Let’s reduce >>>>>> bounds expression to their evaluated bounds to make this easier to write. >>>>>> • int (*)[11] can be converted to int(*)[10] because we’re simply >>>>>> allowing fewer elements to be used. >>>>>> • By the same token, int (*(*)[11])[5] can be converted to >>>>>> int (*(*)[10])[5]. This is the same logic as the above, just with an >>>>>> element type that happens to be a pointer to array type. >>>>>> • But int (*(*)[11])[5] cannot be safely converted to int >>>>>> (*(*)[11])[4], >>>>>> because while it’s safe to read an int (*)[4] from this array, it’s >>>>>> not safe to assign one into it. >>>>>> • int (* const (*)[11])[5] can be safely converted to >>>>>> int (* const (*)[11])[4], but only if this dialect also enforces const- >>>>>> correctness, at least on array pointers. >>>>>> Anyway, a lot of this changes if we want to use the same concept for >>>>>> non-local pointers to arrays, because we no longer have an obvious point >>>>>> of >>>>>> execution at which to evaluate the bounds expression. Instead, we are >>>>>> forced >>>>>> into re-evaluating it every time we access the variable holding the >>>>>> array. >>>>>> Consider: >>>>>> struct X { >>>>>> int count; >>>>>> int (*ptr)[count * 10]; // using my preferred syntax >>>>>> }; >>>>>> >>>>>> void test(struct X *xp) { >>>>>> // For the purposes of the conversion check here, the >>>>>> // source type is int (*)[< xp->count * 10 >], freshly >>>>>> // evaluated as part of the member access. >>>>>> int (*local)[100] = xp->ptr; >>>>>> } >>>>>> >>>>>> This has several immediate consequences. >>>>>> Firstly, we need to already be able to compute the correct bound when we >>>>>> do >>>>>> the dynamic checks for assignments into this field. For local variably- >>>>>> modified types, everything in the expression was already in scope and >>>>>> presumably initialized, so this wasn’t a problem. Here, we’re not helped >>>>>> by scope, and we are dependent on the count field already having been >>>>>> initialized. >>>>>> Secondly, we must be very concerned about anything that could change the >>>>>> result of this evaluation. So we cannot allow an arbitrary expression; >>>>>> it must be something that we can fully analyze for what could change it. >>>>>> And if refers to variables or fields (which it presumably always will), >>>>>> we >>>>>> must prevent assignments to those, or at least validate that any >>>>>> assignments aren’t causing unsound changes to the bound expression. >>>>>> Thirdly, that concern must apply non-locally: if we allow the address of >>>>>> the >>>>>> pointer field to be taken (which is totally fine in the local case!), >>>>>> we can no directly reason about mutations through that pointer, so we >>>>>> have to prevent changes to the bounds variables/fields while the pointer >>>>>> is >>>>>> outstanding. >>>>>> And finally, we must be able to recognize combinations of assignments, >>>>>> because when we’re initializing (or completely rewriting) this structure, >>>>>> we will need to able to assign to both count and ptr and not have the >>>>>> same restrictions in place that we would for separate assignments. >>>>>> None of this falls out naturally from separate, local language rules; it >>>>>> all has to be invented for the purpose of serving this dynamic check. And >>>>>> in fact, -fbounds-safety has to do all of this already just to make >>>>>> basic checks involving pointers in structs work. >>>>>> If that can all be established, though, I think the type-conversion-based >>>>>> approach using variably-modified types has some very nice properties as a >>>>>> complement to what we’re doing in -fbounds-safety. >>>>>> For one, it interacts with the -fbounds-safety analysis very cleanly. If >>>>>> bounds in types are dynamically enforced (which is not true in normal C, >>>>>> but could be in this dialect), then the type becomes a source for >>>>>> reliable >>>>>> reliable information for the bounds-safety analysis. Conversely, if >>>>>> a pointer is converted to a variably-modified type, the analysis done >>>>>> by -bounds-safety could be used as an input to the conversion check. >>>>>> For another, I think it may lead towards an cleaner story for arrays of >>>>>> pointers to arrays than -fbounds-safety can achieve today, as long as >>>>>> the inner arrays are of uniform length. >>>>>> But ultimately, I think it’s still at best a complement to the attributes >>>>>> we need for -fbounds-safety. >>>>>> John. >>>>> >>>> >>> >> >> Yeoul Thanks, Yeoul