For concreteness, I was actually thinking that readnone means “does not access memory”, where “thread_id” is considered to not be part of memory. That is:
- Introduce new attribute
noread_thread_id - Emit
noread_thread_idin addition toreadnonein the frontend for functions that are known to not read the thread ID implicitly or explicitly. - Mark the TLS intrinsic as only as
readnone. - Many checks for
hasAttribute(ReadNone)(e.g. to guard CSE) have to becomehasAttribute(ReadNone) && (!isCoroutine(CurrentFn) || !hasAttribute(NoReadThreadID). Though, note that this isn’t the first time that happens. For example, we have a bunch of places that need to checkhasAttribute(ReadNone) && !hasAttribute(Convergent). So there’s precedent.
It ends up being functionally equivalent to how you interpreted what you wrote, but I’d argue that what I’m describing here is more composable. The way I like to think about it is that there are certain “capabilities” that code may be using: reading memory, writing memory, freeing memory, synchronization, accessing globals, indirect memory accesses; and attributes are used to express which of these capabilities code may be using. Capabilities can be thought of as (almost) a lattice, so defining the attributes in terms of “atomic” capabilities is better for composability.
I understand that there is a bit of tension here between what looks like raw software engineering pragmatism and formal semantics arguments. So I think I can mostly understand where you’re coming from, it’s just that personally, I’ve been burned too often by holes in semantics of IR which is why I fall more in the camp of “let’s please get the semantics right” ![]()