# Pure annotation for stateless functions

**URL:** https://internals.rust-lang.org/t/pure-annotation-for-stateless-functions/23178
**Category:** language design
**Created:** [July 4, 2025, 12:06pm UTC](https://internals.rust-lang.org/t/pure-annotation-for-stateless-functions/23178 "2025-07-04T12:06:46Z")
**Posts on this page:** 2
**Page:** 4

<div class="post-metadata">

### Author: ![Evian-Zhang](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/evian-zhang/32/12170_2.png) [@Evian-Zhang](https://internals.rust-lang.org/u/Evian-Zhang)
#### Post date: [August 9, 2025, 2:59am UTC](https://internals.rust-lang.org/t/pure-annotation-for-stateless-functions/23178/61 "2025-08-09T02:59:41Z")

</div>

Did a little dig further, by `opt -O2 -print-after-all`, we can locate that it is exactly the second EarlyCSEPass of LLVM that conducts the optimization.

By looking at the source code of LLVM, the core logic is in [these lines](https://github.com/llvm/llvm-project/blob/6f53f1c8d2bdd13e30da7d1b85ed6a3ae4c4a856/llvm/lib/Transforms/Scalar/EarlyCSE.cpp#L1632-L1670):

```cpp
    // If this is a read-only or write-only call, process it. Skip store
    // MemInsts, as they will be more precisely handled later on. Also skip
    // memsets, as DSE may be able to optimize them better by removing the
    // earlier rather than later store.
    if (CallValue::canHandle(&Inst) &&
        (!MemInst.isValid() || !MemInst.isStore()) && !isa<MemSetInst>(&Inst)) {
      // If we have an available version of this call, and if it is the right
      // generation, replace this instruction.
      std::pair<Instruction *, unsigned> InVal = AvailableCalls.lookup(&Inst);
      if (InVal.first != nullptr &&
          isSameMemGeneration(InVal.second, CurrentGeneration, InVal.first,
                              &Inst) &&
          InVal.first->mayReadFromMemory() == Inst.mayReadFromMemory()) {
        LLVM_DEBUG(dbgs() << "EarlyCSE CSE CALL: " << Inst
                          << " to: " << *InVal.first << '\n');
        if (!DebugCounter::shouldExecute(CSECounter)) {
          LLVM_DEBUG(dbgs() << "Skipping due to debug counter\n");
          continue;
        }
        combineIRFlags(Inst, InVal.first);
        if (!Inst.use_empty())
          Inst.replaceAllUsesWith(InVal.first);
        salvageKnowledge(&Inst, &AC);
        removeMSSA(Inst);
        Inst.eraseFromParent();
        Changed = true;
        ++NumCSECall;
        continue;
      }

      // Increase memory generation for writes. Do this before inserting
      // the call, so it has the generation after the write occurred.
      if (Inst.mayWriteToMemory())
        ++CurrentGeneration;

      // Otherwise, remember that we have this instruction.
      AvailableCalls.insert(&Inst, std::make_pair(&Inst, CurrentGeneration));
      continue;
    }

```

Hmmmm, as far as I can tell, they just look at how the callee accesses memories as @comex said.

And I tried another thing: I copy-paste the attribute of `foo` in the pure version to the `panic` version in LLVM IR, and it turns out that the `opt` can still conduct CSE successfully 😄

---

<div class="post-metadata">

### Author: ![ais523](https://avatars.discourse-cdn.com/v4/letter/a/a183cd/32.png) [@ais523](https://internals.rust-lang.org/u/ais523)
#### Post date: [October 12, 2025, 10:14pm UTC](https://internals.rust-lang.org/t/pure-annotation-for-stateless-functions/23178/62 "2025-10-12T22:14:35Z")

</div>

> [@newpavlov](#):
>
> I wish compilers would use a more relaxed a definition of program equivalence in the presence of exceptions, which would allow for such reordering. Or, at least, provide an option to opt out of the strict definition. Most people do not care whether `f` or `g` has panicked first, only that a panic has happened and why.

I think there's a semantically consistent way to implement this: imagine a macro `delayed_panic!(…)` with the following behaviour: it allows the program to panic (with the given message) at any time after the call, and any subsequent execution of the progam must not depend on the value returned by `delayed_panic!` (i.e. the subsequent program execution can only do things that don't depend at all on the result of the `delayed_panic!()` call). This is always possible to implement because it could always be implemented as panicking immediately, but gives the compiler more scope for optimisation.

I fear that this might not actually work well in practice, though: it's very similar to C++'s ill-fated `memory_order_consume` which ended up being deprecated because no compiler was actually able to do nontrivial optimisations using it (they compiled it in a way that complied with the specification but ignored all the theoretical performance advantages). It might also be quite unintuitive, e.g. you couldn't use it at type `()` because then the delayed panic could be optimised out entirely, so it would mostly only be useful in "the value couldn't be calculated from these inputs" situations.

EDIT: I just realised that this sort of thing could maybe fix the `drop(Rc::clone(rc))` problem – in current Rust that can't be optimised out because it has to check that the reference count isn't `usize::MAX` and panic if it is, but you conceptually want it to be able to be optimised out even if that changes the timing or existence of panics. That's actually making me think that the potential performance gains could be quite large, if the compiler could be made to provide them.

[Previous page](https://internals.rust-lang.org/t/pure-annotation-for-stateless-functions/23178.md?page=3)
