blob: 912d7ea171c9423d3d40e24da6a0eb9677cd0667 [file]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssacompile
import (
"cmd/compile/internal/ssa"
blockpkg "cmd/compile/internal/ssa/block"
"cmd/compile/internal/ssa/ssaop"
"cmd/compile/internal/ssarewrite/rewritegeneric"
)
// ----------------------------------------------------------------------------
// Sparse Conditional Constant Propagation
//
// Described in
// Mark N. Wegman, F. Kenneth Zadeck: Constant Propagation with Conditional Branches.
// TOPLAS 1991.
//
// This algorithm uses three level lattice for SSA value
//
// Top undefined
// / | \
// .. 1 2 3 .. constant
// \ | /
// Bottom not constant
//
// It starts with optimistically assuming that all SSA values are initially Top
// and then propagates constant facts only along reachable control flow paths.
// Since some basic blocks are not visited yet, corresponding inputs of phi become
// Top, we use the meet(phi) to compute its lattice.
//
// Top ∩ any = any
// Bottom ∩ any = Bottom
// ConstantA ∩ ConstantA = ConstantA
// ConstantA ∩ ConstantB = Bottom
//
// Each lattice value is lowered most twice(Top to Constant, Constant to Bottom)
// due to lattice depth, resulting in a fast convergence speed of the algorithm.
// In this way, sccp can discover optimization opportunities that cannot be found
// by just combining constant folding and constant propagation and dead code
// elimination separately.
// Three level lattice holds compile time knowledge about SSA value
const (
top int8 = iota // undefined
constant // constant
bottom // not a constant
)
type lattice struct {
tag int8 // lattice type
val *ssa.Value // constant value
}
type worklist struct {
f *ssa.Func // the target function to be optimized out
edges []ssa.Edge // propagate constant facts through edges
inUses *ssa.SparseSet // IDs already in uses, for duplicate check
uses []*ssa.Value // re-visiting set
visited map[ssa.Edge]bool // visited edges
latticeCells map[*ssa.Value]lattice // constant lattices
defUse map[*ssa.Value][]*ssa.Value // def-use chains for some values
defBlock map[*ssa.Value][]*ssa.Block // use blocks of def
visitedBlock []bool // visited block
}
// sccp stands for sparse conditional constant propagation, it propagates constants
// through CFG conditionally and applies constant folding, constant replacement and
// dead code elimination all together.
func sccp(f *ssa.Func) {
var t worklist
t.f = f
t.edges = make([]ssa.Edge, 0)
t.visited = make(map[ssa.Edge]bool)
t.edges = append(t.edges, ssa.Edge{B: f.Entry, I: 0})
t.defUse = make(map[*ssa.Value][]*ssa.Value)
t.defBlock = make(map[*ssa.Value][]*ssa.Block)
t.latticeCells = make(map[*ssa.Value]lattice)
t.visitedBlock = f.Cache.AllocBoolSlice(f.NumBlocks())
t.inUses = f.NewSparseSet(f.NumValues())
defer f.RetSparseSet(t.inUses)
defer f.Cache.FreeBoolSlice(t.visitedBlock)
// build it early since we rely heavily on the def-use chain later
t.buildDefUses()
// pick up either an edge or SSA value from worklist, process it
for {
if len(t.edges) > 0 {
edge := t.edges[0]
t.edges = t.edges[1:]
if _, exist := t.visited[edge]; !exist {
dest := edge.B
destVisited := t.visitedBlock[dest.ID]
// mark edge as visited
t.visited[edge] = true
t.visitedBlock[dest.ID] = true
for _, val := range dest.Values {
if val.Op == ssaop.OpPhi || !destVisited {
t.visitValue(val)
}
}
// propagates constants facts through CFG, taking condition test
// into account
if !destVisited {
t.propagate(dest)
}
}
continue
}
if len(t.uses) > 0 {
use := t.uses[0]
t.uses = t.uses[1:]
t.inUses.Remove(use.ID)
t.visitValue(use)
continue
}
break
}
// apply optimizations based on discovered constants
constCnt, rewireCnt := t.replaceConst()
if f.Pass.Debug > 0 {
if constCnt > 0 || rewireCnt > 0 {
f.Warnl(f.Entry.Pos, "Phase SCCP for %v : %v constants, %v dce", f.Name, constCnt, rewireCnt)
}
}
}
func equals(a, b lattice) bool {
if a == b {
// fast path
return true
}
if a.tag != b.tag {
return false
}
if a.tag == constant {
// The same content of const value may be different, we should
// compare with auxInt instead
v1 := a.val
v2 := b.val
if v1.Op == v2.Op && v1.AuxInt == v2.AuxInt {
return true
} else {
return false
}
}
return true
}
// possibleConst checks if Value can be folded to const. For those Values that can
// never become constants(e.g. StaticCall), we don't make futile efforts.
func possibleConst(val *ssa.Value) bool {
if isConst(val) {
return true
}
switch val.Op {
case ssaop.OpCopy:
return true
case ssaop.OpPhi:
return true
case
// negate
ssaop.OpNeg8, ssaop.OpNeg16, ssaop.OpNeg32, ssaop.OpNeg64, ssaop.OpNeg32F, ssaop.OpNeg64F,
ssaop.OpCom8, ssaop.OpCom16, ssaop.OpCom32, ssaop.OpCom64,
// math
ssaop.OpFloor, ssaop.OpCeil, ssaop.OpTrunc, ssaop.OpRoundToEven, ssaop.OpSqrt,
// conversion
ssaop.OpTrunc16to8, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc64to8,
ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpCvt32to32F, ssaop.OpCvt32to64F,
ssaop.OpCvt64to32F, ssaop.OpCvt64to64F, ssaop.OpCvt32Fto32, ssaop.OpCvt32Fto64,
ssaop.OpCvt64Fto32, ssaop.OpCvt64Fto64, ssaop.OpCvt32Fto64F, ssaop.OpCvt64Fto32F,
ssaop.OpCvtBoolToUint8,
ssaop.OpZeroExt8to16, ssaop.OpZeroExt8to32, ssaop.OpZeroExt8to64, ssaop.OpZeroExt16to32,
ssaop.OpZeroExt16to64, ssaop.OpZeroExt32to64, ssaop.OpSignExt8to16, ssaop.OpSignExt8to32,
ssaop.OpSignExt8to64, ssaop.OpSignExt16to32, ssaop.OpSignExt16to64, ssaop.OpSignExt32to64,
// bit
ssaop.OpCtz8, ssaop.OpCtz16, ssaop.OpCtz32, ssaop.OpCtz64,
// mask
ssaop.OpSlicemask,
// safety check
ssaop.OpIsNonNil,
// not
ssaop.OpNot:
return true
case
// add
ssaop.OpAdd64, ssaop.OpAdd32, ssaop.OpAdd16, ssaop.OpAdd8,
ssaop.OpAdd32F, ssaop.OpAdd64F,
// sub
ssaop.OpSub64, ssaop.OpSub32, ssaop.OpSub16, ssaop.OpSub8,
ssaop.OpSub32F, ssaop.OpSub64F,
// mul
ssaop.OpMul64, ssaop.OpMul32, ssaop.OpMul16, ssaop.OpMul8,
ssaop.OpMul32F, ssaop.OpMul64F,
// div
ssaop.OpDiv32F, ssaop.OpDiv64F,
ssaop.OpDiv8, ssaop.OpDiv16, ssaop.OpDiv32, ssaop.OpDiv64,
ssaop.OpDiv8u, ssaop.OpDiv16u, ssaop.OpDiv32u, ssaop.OpDiv64u,
ssaop.OpMod8, ssaop.OpMod16, ssaop.OpMod32, ssaop.OpMod64,
ssaop.OpMod8u, ssaop.OpMod16u, ssaop.OpMod32u, ssaop.OpMod64u,
// compare
ssaop.OpEq64, ssaop.OpEq32, ssaop.OpEq16, ssaop.OpEq8,
ssaop.OpEq32F, ssaop.OpEq64F,
ssaop.OpLess64, ssaop.OpLess32, ssaop.OpLess16, ssaop.OpLess8,
ssaop.OpLess64U, ssaop.OpLess32U, ssaop.OpLess16U, ssaop.OpLess8U,
ssaop.OpLess32F, ssaop.OpLess64F,
ssaop.OpLeq64, ssaop.OpLeq32, ssaop.OpLeq16, ssaop.OpLeq8,
ssaop.OpLeq64U, ssaop.OpLeq32U, ssaop.OpLeq16U, ssaop.OpLeq8U,
ssaop.OpLeq32F, ssaop.OpLeq64F,
ssaop.OpEqB, ssaop.OpNeqB,
// shift
ssaop.OpLsh64x64, ssaop.OpRsh64x64, ssaop.OpRsh64Ux64, ssaop.OpLsh32x64,
ssaop.OpRsh32x64, ssaop.OpRsh32Ux64, ssaop.OpLsh16x64, ssaop.OpRsh16x64,
ssaop.OpRsh16Ux64, ssaop.OpLsh8x64, ssaop.OpRsh8x64, ssaop.OpRsh8Ux64,
// safety check
ssaop.OpIsInBounds, ssaop.OpIsSliceInBounds,
// bit
ssaop.OpAnd8, ssaop.OpAnd16, ssaop.OpAnd32, ssaop.OpAnd64,
ssaop.OpOr8, ssaop.OpOr16, ssaop.OpOr32, ssaop.OpOr64,
ssaop.OpXor8, ssaop.OpXor16, ssaop.OpXor32, ssaop.OpXor64:
return true
default:
return false
}
}
func (t *worklist) getLatticeCell(val *ssa.Value) lattice {
if !possibleConst(val) {
// they are always worst
return lattice{bottom, nil}
}
lt, exist := t.latticeCells[val]
if !exist {
return lattice{top, nil} // optimistically for un-visited value
}
return lt
}
func isConst(val *ssa.Value) bool {
switch val.Op {
case ssaop.OpConst64, ssaop.OpConst32, ssaop.OpConst16, ssaop.OpConst8,
ssaop.OpConstBool, ssaop.OpConst32F, ssaop.OpConst64F:
return true
default:
return false
}
}
// buildDefUses builds def-use chain for some values early, because once the
// lattice of a value is changed, we need to update lattices of use. But we don't
// need all uses of it, only uses that can become constants would be added into
// re-visit worklist since no matter how many times they are revisited, uses which
// can't become constants lattice remains unchanged, i.e. Bottom.
func (t *worklist) buildDefUses() {
for _, block := range t.f.Blocks {
for _, val := range block.Values {
for _, arg := range val.Args {
// find its uses, only uses that can become constants take into account
if possibleConst(arg) && possibleConst(val) {
// Phi may refer to itself as uses, avoid duplicate visits
if arg == val {
continue
}
if _, exist := t.defUse[arg]; !exist {
t.defUse[arg] = make([]*ssa.Value, 0, arg.Uses)
}
t.defUse[arg] = append(t.defUse[arg], val)
}
}
}
for _, ctl := range block.ControlValues() {
// for control values that can become constants, find their use blocks
if possibleConst(ctl) {
t.defBlock[ctl] = append(t.defBlock[ctl], block)
}
}
}
}
// addUses finds all uses of value and appends them into work list for further process
func (t *worklist) addUses(val *ssa.Value) {
for _, use := range t.defUse[val] {
// Provenly not a constant, ignore
useLt := t.getLatticeCell(use)
if useLt.tag == bottom {
continue
}
// Avoid duplicate visits
if !t.inUses.Contains(use.ID) {
t.inUses.Add(use.ID)
t.uses = append(t.uses, use)
}
}
for _, block := range t.defBlock[val] {
if t.visitedBlock[block.ID] {
t.propagate(block)
}
}
}
// meet meets all of phi arguments and computes result lattice
func (t *worklist) meet(val *ssa.Value) lattice {
optimisticLt := lattice{top, nil}
for i := 0; i < len(val.Args); i++ {
edge := ssa.Edge{B: val.Block, I: i}
// If incoming edge for phi is not visited, assume top optimistically.
// According to rules of meet:
// Top ∩ any = any
// Top participates in meet() but does not affect the result, so here
// we will ignore Top and only take other lattices into consideration.
if _, exist := t.visited[edge]; exist {
lt := t.getLatticeCell(val.Args[i])
if lt.tag == constant {
if optimisticLt.tag == top {
optimisticLt = lt
} else {
if !equals(optimisticLt, lt) {
// ConstantA ∩ ConstantB = Bottom
return lattice{bottom, nil}
}
}
} else if lt.tag == bottom {
// Bottom ∩ any = Bottom
return lattice{bottom, nil}
} else {
// Top ∩ any = any
}
} else {
// Top ∩ any = any
}
}
// ConstantA ∩ ConstantA = ConstantA or Top ∩ any = any
return optimisticLt
}
func computeLattice(f *ssa.Func, val *ssa.Value, args ...*ssa.Value) lattice {
// In general, we need to perform constant evaluation based on constant args:
//
// res := lattice{constant, nil}
// switch op {
// case OpAdd16:
// res.val = newConst(argLt1.val.AuxInt16() + argLt2.val.AuxInt16())
// case OpAdd32:
// res.val = newConst(argLt1.val.AuxInt32() + argLt2.val.AuxInt32())
// case OpDiv8:
// if !isDivideByZero(argLt2.val.AuxInt8()) {
// res.val = newConst(argLt1.val.AuxInt8() / argLt2.val.AuxInt8())
// }
// ...
// }
//
// However, this would create a huge switch for all opcodes that can be
// evaluated during compile time. Moreover, some operations can be evaluated
// only if its arguments satisfy additional conditions(e.g. divide by zero).
// It's fragile and error-prone. We did a trick by reusing the existing rules
// in generic rules for compile-time evaluation. But generic rules rewrite
// original value, this behavior is undesired, because the lattice of values
// may change multiple times, once it was rewritten, we lose the opportunity
// to change it permanently, which can lead to errors. For example, We cannot
// change its value immediately after visiting Phi, because some of its input
// edges may still not be visited at this moment.
constValue := f.NewValue(val.Op, val.Type, f.Entry, val.Pos)
constValue.AddArgs(args...)
matched := rewritegeneric.RewriteValue(constValue)
if matched {
if isConst(constValue) {
return lattice{constant, constValue}
}
}
// Either we can not match generic rules for given value or it does not
// satisfy additional constraints(e.g. divide by zero), in these cases, clean
// up temporary value immediately in case they are not dominated by their args.
constValue.Reset(ssaop.OpInvalid)
return lattice{bottom, nil}
}
func (t *worklist) visitValue(val *ssa.Value) {
// Impossible to be a constant, fast fail
if !possibleConst(val) {
return
}
// Provenly not a constant, fast fail
oldLt := t.getLatticeCell(val)
if oldLt.tag == bottom {
return
}
// Re-visit all uses of value if its lattice is changed
defer func() {
newLt := t.getLatticeCell(val)
if !equals(newLt, oldLt) {
if oldLt.tag > newLt.tag {
t.f.Fatalf("Must lower lattice\n")
}
t.addUses(val)
}
}()
switch val.Op {
// they are constant values, aren't they?
case ssaop.OpConst64, ssaop.OpConst32, ssaop.OpConst16, ssaop.OpConst8,
ssaop.OpConstBool, ssaop.OpConst32F, ssaop.OpConst64F: //TODO: support ConstNil ConstString etc
t.latticeCells[val] = lattice{constant, val}
// lattice value of copy(x) actually means lattice value of (x)
case ssaop.OpCopy:
t.latticeCells[val] = t.getLatticeCell(val.Args[0])
// phi should be processed specially
case ssaop.OpPhi:
t.latticeCells[val] = t.meet(val)
// fold 1-input operations:
case
// negate
ssaop.OpNeg8, ssaop.OpNeg16, ssaop.OpNeg32, ssaop.OpNeg64, ssaop.OpNeg32F, ssaop.OpNeg64F,
ssaop.OpCom8, ssaop.OpCom16, ssaop.OpCom32, ssaop.OpCom64,
// math
ssaop.OpFloor, ssaop.OpCeil, ssaop.OpTrunc, ssaop.OpRoundToEven, ssaop.OpSqrt,
// conversion
ssaop.OpTrunc16to8, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc64to8,
ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpCvt32to32F, ssaop.OpCvt32to64F,
ssaop.OpCvt64to32F, ssaop.OpCvt64to64F, ssaop.OpCvt32Fto32, ssaop.OpCvt32Fto64,
ssaop.OpCvt64Fto32, ssaop.OpCvt64Fto64, ssaop.OpCvt32Fto64F, ssaop.OpCvt64Fto32F,
ssaop.OpCvtBoolToUint8,
ssaop.OpZeroExt8to16, ssaop.OpZeroExt8to32, ssaop.OpZeroExt8to64, ssaop.OpZeroExt16to32,
ssaop.OpZeroExt16to64, ssaop.OpZeroExt32to64, ssaop.OpSignExt8to16, ssaop.OpSignExt8to32,
ssaop.OpSignExt8to64, ssaop.OpSignExt16to32, ssaop.OpSignExt16to64, ssaop.OpSignExt32to64,
// bit
ssaop.OpCtz8, ssaop.OpCtz16, ssaop.OpCtz32, ssaop.OpCtz64,
// mask
ssaop.OpSlicemask,
// safety check
ssaop.OpIsNonNil,
// not
ssaop.OpNot:
lt1 := t.getLatticeCell(val.Args[0])
if lt1.tag == constant {
// here we take a shortcut by reusing generic rules to fold constants
t.latticeCells[val] = computeLattice(t.f, val, lt1.val)
} else {
t.latticeCells[val] = lattice{lt1.tag, nil}
}
// fold 2-input operations
case
// add
ssaop.OpAdd64, ssaop.OpAdd32, ssaop.OpAdd16, ssaop.OpAdd8,
ssaop.OpAdd32F, ssaop.OpAdd64F,
// sub
ssaop.OpSub64, ssaop.OpSub32, ssaop.OpSub16, ssaop.OpSub8,
ssaop.OpSub32F, ssaop.OpSub64F,
// mul
ssaop.OpMul64, ssaop.OpMul32, ssaop.OpMul16, ssaop.OpMul8,
ssaop.OpMul32F, ssaop.OpMul64F,
// div
ssaop.OpDiv32F, ssaop.OpDiv64F,
ssaop.OpDiv8, ssaop.OpDiv16, ssaop.OpDiv32, ssaop.OpDiv64,
ssaop.OpDiv8u, ssaop.OpDiv16u, ssaop.OpDiv32u, ssaop.OpDiv64u, //TODO: support div128u
// mod
ssaop.OpMod8, ssaop.OpMod16, ssaop.OpMod32, ssaop.OpMod64,
ssaop.OpMod8u, ssaop.OpMod16u, ssaop.OpMod32u, ssaop.OpMod64u,
// compare
ssaop.OpEq64, ssaop.OpEq32, ssaop.OpEq16, ssaop.OpEq8,
ssaop.OpEq32F, ssaop.OpEq64F,
ssaop.OpLess64, ssaop.OpLess32, ssaop.OpLess16, ssaop.OpLess8,
ssaop.OpLess64U, ssaop.OpLess32U, ssaop.OpLess16U, ssaop.OpLess8U,
ssaop.OpLess32F, ssaop.OpLess64F,
ssaop.OpLeq64, ssaop.OpLeq32, ssaop.OpLeq16, ssaop.OpLeq8,
ssaop.OpLeq64U, ssaop.OpLeq32U, ssaop.OpLeq16U, ssaop.OpLeq8U,
ssaop.OpLeq32F, ssaop.OpLeq64F,
ssaop.OpEqB, ssaop.OpNeqB,
// shift
ssaop.OpLsh64x64, ssaop.OpRsh64x64, ssaop.OpRsh64Ux64, ssaop.OpLsh32x64,
ssaop.OpRsh32x64, ssaop.OpRsh32Ux64, ssaop.OpLsh16x64, ssaop.OpRsh16x64,
ssaop.OpRsh16Ux64, ssaop.OpLsh8x64, ssaop.OpRsh8x64, ssaop.OpRsh8Ux64,
// safety check
ssaop.OpIsInBounds, ssaop.OpIsSliceInBounds,
// bit
ssaop.OpAnd8, ssaop.OpAnd16, ssaop.OpAnd32, ssaop.OpAnd64,
ssaop.OpOr8, ssaop.OpOr16, ssaop.OpOr32, ssaop.OpOr64,
ssaop.OpXor8, ssaop.OpXor16, ssaop.OpXor32, ssaop.OpXor64:
lt1 := t.getLatticeCell(val.Args[0])
lt2 := t.getLatticeCell(val.Args[1])
if lt1.tag == constant && lt2.tag == constant {
// here we take a shortcut by reusing generic rules to fold constants
t.latticeCells[val] = computeLattice(t.f, val, lt1.val, lt2.val)
} else {
if lt1.tag == bottom || lt2.tag == bottom {
t.latticeCells[val] = lattice{bottom, nil}
} else {
t.latticeCells[val] = lattice{top, nil}
}
}
default:
// Any other type of value cannot be a constant, they are always worst(Bottom)
}
}
// propagate propagates constants facts through CFG. If the block has single successor,
// add the successor anyway. If the block has multiple successors, only add the
// branch destination corresponding to lattice value of condition value.
func (t *worklist) propagate(block *ssa.Block) {
switch block.Kind {
case blockpkg.BlockExit, blockpkg.BlockRet, blockpkg.BlockRetJmp, blockpkg.BlockInvalid:
// control flow ends, do nothing then
break
case blockpkg.BlockDefer:
// we know nothing about control flow, add all branch destinations
t.edges = append(t.edges, block.Succs...)
case blockpkg.BlockFirst:
fallthrough // always takes the first branch
case blockpkg.BlockPlain:
t.edges = append(t.edges, block.Succs[0])
case blockpkg.BlockIf, blockpkg.BlockJumpTable:
cond := block.ControlValues()[0]
condLattice := t.getLatticeCell(cond)
if condLattice.tag == bottom {
// we know nothing about control flow, add all branch destinations
t.edges = append(t.edges, block.Succs...)
} else if condLattice.tag == constant {
// add branchIdx destinations depends on its condition
var branchIdx int64
if block.Kind == blockpkg.BlockIf {
branchIdx = 1 - condLattice.val.AuxInt
} else {
branchIdx = condLattice.val.AuxInt
if branchIdx < 0 || branchIdx >= int64(len(block.Succs)) {
// unreachable code, do nothing then
break
}
}
t.edges = append(t.edges, block.Succs[branchIdx])
} else {
// condition value is not visited yet, don't propagate it now
}
default:
t.f.Fatalf("All kind of block should be processed above.")
}
}
// rewireSuccessor rewires corresponding successors according to constant value
// discovered by previous analysis. As the result, some successors become unreachable
// and thus can be removed in further deadcode phase
func rewireSuccessor(block *ssa.Block, constVal *ssa.Value) bool {
switch block.Kind {
case blockpkg.BlockIf:
block.RemoveEdge(int(constVal.AuxInt))
block.Kind = blockpkg.BlockPlain
block.Likely = ssa.BranchUnknown
block.ResetControls()
return true
case blockpkg.BlockJumpTable:
// Remove everything but the known taken branch.
idx := int(constVal.AuxInt)
if idx < 0 || idx >= len(block.Succs) {
// This can only happen in unreachable code,
// as an invariant of jump tables is that their
// input index is in range.
// See issue 64826.
return false
}
block.SwapSuccessorsByIdx(0, idx)
for len(block.Succs) > 1 {
block.RemoveEdge(1)
}
block.Kind = blockpkg.BlockPlain
block.Likely = ssa.BranchUnknown
block.ResetControls()
return true
default:
return false
}
}
// replaceConst will replace non-constant values that have been proven by sccp
// to be constants.
func (t *worklist) replaceConst() (int, int) {
constCnt, rewireCnt := 0, 0
for val, lt := range t.latticeCells {
if lt.tag == constant {
if !isConst(val) {
if t.f.Pass.Debug > 0 {
t.f.Warnl(val.Pos, "Replace %v with %v", val.LongString(), lt.val.LongString())
}
val.Reset(lt.val.Op)
val.AuxInt = lt.val.AuxInt
constCnt++
}
// If const value controls this block, rewires successors according to its value
ctrlBlock := t.defBlock[val]
for _, block := range ctrlBlock {
if rewireSuccessor(block, lt.val) {
rewireCnt++
if t.f.Pass.Debug > 0 {
t.f.Warnl(block.Pos, "Rewire %v %v successors", block.Kind, block)
}
}
}
}
}
return constCnt, rewireCnt
}