Alternatively: When we encounter a load, we record, in the copy_propagation_state, an array of copy_propagation_value's for that load. Then in copy_propagation_transform_swizzle(), instead of calling copy_propagation_get_value(), we use that value array. That way we are looking up the "live" values at the time of the load, rather than later after things may have been invalidated.
Yeah, it feels to me as this is the core problem: the copy propagation algorithm was originally written having only loads in mind, so each time we process a load we have the updated state of variables at that time. When we process a swizzle on a load, the state is that of the swizzle, which might have changed since the load. Therefore, mistakes.
At the same time, keeping all all the past `copy_propagation_value` objects seems a bit inefficient. I wonder if we should do the other way around: each time we see a load we process it with our current knowledge; then, immediately, we look at all instructions using that load and, if there is any swizzle, we record a notice of where the value for that swizzle should truly come from; then we resume processing after the load. Once we arrive to the swizzle we look at all the recorded values and we have an opportunity to change it (either with a constant or with a swizzled load, depending of what we see).
I haven't tried writing this, but superficially it looks like it should work. And I would prefer a proper fix too, rather than something that it is not clear (to me at least) to what extent it should work.