Vector zext tends to get legalized into a vector anyext (represented as a vector shuffle with an undef vector + a bitcast) that gets ANDed with a mask that zeroes the undef elements.
Combine this into an explicit shuffle with a zero vector instead. This allows shuffle lowering to match it as a zext, instead of matching it as an anyext and emitting an explicit and like it does now.
This doesn't cover all the cases, as you can see in the test, but it's a start.