Background
PR ruby#16828 introduced a block-local HIR canonicalize pass(zjit/src/hir.rs:4977-5006).
For each block in RPO, it walks instructions and rewrites each operand through union-find plus a per-block rewrite_map keyed on the most recent Guard* result for that value.
The map is cleared at every block boundary (zjit/src/hir.rs:4980).
This covers two redundant-guard shapes:
- Within-block consecutive guards on the same value.
- CFG-join via block parameters: branch-edge args (
Jump,CondBranch) get rewritten before the next block's clear(),
so guard-narrowed values flow into merge-block parameters and infer_types + fold_constants can drop the redundant join-block guards.
Gap
A third shape is not caught: a value defined and guarded in a dominator block, then used directly in a dominated block without going through a block parameter.
ZJIT's HIR (Cranelift-style) allows direct references to values defined in any dominator, so this is a common pattern — early-return chains, nested conditionals, loops, etc.
Example:
def early_return(n)
return -1 if n < 0 # bb0: GuardType %n (for FixnumLess)
return 0 if n == 0 # bb2: GuardType %n (redundant)
n * 2 # bb4: GuardType %n (redundant)
end
In the HIR for this function, bb2 and bb4 reference %n directly (no block parameter).
I believe the block-local pass clears rewrite_map on entering them, so the %n → %3 rewrite established in bb0 is lost,
and I believe the redundant GuardType instructions in bb2 and bb4 survive all the way to machine code.
Follow-up from PR ruby#16828 review
@tekknolagi suggested this extension during PR review:
This can later (not in this PR, please) be extended to walk the dominator tree using a scoped hashmap or something so that we get cross-block results that don't correspond to block parameters.
(ruby#16828 (comment))
Background
PR ruby#16828 introduced a block-local HIR
canonicalizepass(zjit/src/hir.rs:4977-5006).For each block in RPO, it walks instructions and rewrites each operand through union-find plus a per-block
rewrite_mapkeyed on the most recentGuard*result for that value.The map is cleared at every block boundary (
zjit/src/hir.rs:4980).This covers two redundant-guard shapes:
Jump,CondBranch) get rewritten before the next block'sclear(),so guard-narrowed values flow into merge-block parameters and
infer_types+fold_constantscan drop the redundant join-block guards.Gap
A third shape is not caught: a value defined and guarded in a dominator block, then used directly in a dominated block without going through a block parameter.
ZJIT's HIR (Cranelift-style) allows direct references to values defined in any dominator, so this is a common pattern — early-return chains, nested conditionals, loops, etc.
Example:
In the HIR for this function, bb2 and bb4 reference %n directly (no block parameter).
I believe the block-local pass clears rewrite_map on entering them, so the %n → %3 rewrite established in bb0 is lost,
and I believe the redundant GuardType instructions in bb2 and bb4 survive all the way to machine code.
Follow-up from PR ruby#16828 review
@tekknolagi suggested this extension during PR review:
(ruby#16828 (comment))