a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
237 lines
5.4 KiB
Go
237 lines
5.4 KiB
Go
package cfg
|
||
|
||
import (
|
||
"math/bits"
|
||
"sort"
|
||
)
|
||
|
||
// Definition is one (statement, variable) write site. ID is the
|
||
// definition's bit position in the analysis bitsets.
|
||
type Definition struct {
|
||
ID int `json:"id"`
|
||
Stmt int `json:"stmt"`
|
||
Var string `json:"var"`
|
||
}
|
||
|
||
// UseChain links one variable read to every definition that can
|
||
// reach it along some control-flow path. Defs holds statement
|
||
// indices, ascending.
|
||
type UseChain struct {
|
||
Stmt int `json:"stmt"`
|
||
Var string `json:"var"`
|
||
Defs []int `json:"defs"`
|
||
}
|
||
|
||
// ReachingResult is the fixpoint output: the definition table, the
|
||
// per-block IN/OUT sets (definition IDs), and the statement-granular
|
||
// def→use chains.
|
||
type ReachingResult struct {
|
||
Defs []Definition
|
||
Chains []UseChain
|
||
In [][]int
|
||
Out [][]int
|
||
}
|
||
|
||
// ChainsFor returns the chains attached to one statement.
|
||
func (r *ReachingResult) ChainsFor(stmt int) []UseChain {
|
||
var out []UseChain
|
||
for _, c := range r.Chains {
|
||
if c.Stmt == stmt {
|
||
out = append(out, c)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ReachingDefinitions runs the classic GEN/KILL monotone fixpoint
|
||
// over the CFG:
|
||
//
|
||
// IN[b] = ∪ OUT[p] for p ∈ preds(b)
|
||
// OUT[b] = GEN[b] ∪ (IN[b] − KILL[b])
|
||
//
|
||
// then replays each block's statements against its IN set to link
|
||
// every use to the definitions reaching it. Bitsets keep the
|
||
// per-block transfer functions O(defs/64).
|
||
func (c *CFG) ReachingDefinitions() *ReachingResult {
|
||
res := &ReachingResult{}
|
||
|
||
// 1. Number every definition and group them by variable.
|
||
defsByVar := map[string][]int{}
|
||
defID := map[[2]interface{}]int{} // (stmt, var) → def ID; stmts dedupe vars already
|
||
for _, st := range c.Stmts {
|
||
for _, v := range st.Defs {
|
||
id := len(res.Defs)
|
||
res.Defs = append(res.Defs, Definition{ID: id, Stmt: st.Index, Var: v})
|
||
defsByVar[v] = append(defsByVar[v], id)
|
||
defID[[2]interface{}{st.Index, v}] = id
|
||
}
|
||
}
|
||
nDefs := len(res.Defs)
|
||
nBlocks := len(c.Blocks)
|
||
words := (nDefs + 63) / 64
|
||
|
||
newSet := func() bitset { return make(bitset, words) }
|
||
allDefsOf := func(v string) bitset {
|
||
s := newSet()
|
||
for _, id := range defsByVar[v] {
|
||
s.set(id)
|
||
}
|
||
return s
|
||
}
|
||
|
||
// 2. Per-block GEN (downward-exposed defs) and KILL (every def of
|
||
// a variable the block writes).
|
||
gen := make([]bitset, nBlocks)
|
||
kill := make([]bitset, nBlocks)
|
||
for i, bl := range c.Blocks {
|
||
g, k := newSet(), newSet()
|
||
last := map[string]int{}
|
||
for _, st := range bl.Stmts {
|
||
for _, v := range st.Defs {
|
||
k.or(allDefsOf(v))
|
||
last[v] = defID[[2]interface{}{st.Index, v}]
|
||
}
|
||
}
|
||
for _, id := range last {
|
||
g.set(id)
|
||
}
|
||
gen[i], kill[i] = g, k
|
||
}
|
||
|
||
// 3. Predecessor / successor lists.
|
||
preds := make([][]int, nBlocks)
|
||
succs := make([][]int, nBlocks)
|
||
for _, e := range c.Edges {
|
||
preds[e.To] = append(preds[e.To], e.From)
|
||
succs[e.From] = append(succs[e.From], e.To)
|
||
}
|
||
|
||
// 4. Worklist fixpoint.
|
||
in := make([]bitset, nBlocks)
|
||
out := make([]bitset, nBlocks)
|
||
for i := range c.Blocks {
|
||
in[i], out[i] = newSet(), newSet()
|
||
out[i].or(gen[i])
|
||
}
|
||
work := make([]int, 0, nBlocks)
|
||
inWork := make([]bool, nBlocks)
|
||
for i := 0; i < nBlocks; i++ {
|
||
work = append(work, i)
|
||
inWork[i] = true
|
||
}
|
||
for len(work) > 0 {
|
||
b := work[0]
|
||
work = work[1:]
|
||
inWork[b] = false
|
||
|
||
newIn := newSet()
|
||
for _, p := range preds[b] {
|
||
newIn.or(out[p])
|
||
}
|
||
in[b] = newIn
|
||
newOut := newIn.clone()
|
||
newOut.andNot(kill[b])
|
||
newOut.or(gen[b])
|
||
if !newOut.equal(out[b]) {
|
||
out[b] = newOut
|
||
for _, s := range succs[b] {
|
||
if !inWork[s] {
|
||
work = append(work, s)
|
||
inWork[s] = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Statement-granular replay: thread the live set through each
|
||
// block, linking uses before applying the statement's defs.
|
||
for bi, bl := range c.Blocks {
|
||
live := in[bi].clone()
|
||
for _, st := range bl.Stmts {
|
||
for _, v := range st.Uses {
|
||
ids := defsByVar[v]
|
||
if len(ids) == 0 {
|
||
continue
|
||
}
|
||
var reach []int
|
||
for _, id := range ids {
|
||
if live.get(id) {
|
||
reach = append(reach, res.Defs[id].Stmt)
|
||
}
|
||
}
|
||
if len(reach) == 0 {
|
||
continue
|
||
}
|
||
sort.Ints(reach)
|
||
res.Chains = append(res.Chains, UseChain{Stmt: st.Index, Var: v, Defs: reach})
|
||
}
|
||
for _, v := range st.Defs {
|
||
live.andNot(allDefsOf(v))
|
||
live.set(defID[[2]interface{}{st.Index, v}])
|
||
}
|
||
}
|
||
}
|
||
sort.SliceStable(res.Chains, func(i, j int) bool {
|
||
if res.Chains[i].Stmt != res.Chains[j].Stmt {
|
||
return res.Chains[i].Stmt < res.Chains[j].Stmt
|
||
}
|
||
return res.Chains[i].Var < res.Chains[j].Var
|
||
})
|
||
|
||
// 6. Export IN/OUT as sorted definition-ID lists.
|
||
res.In = make([][]int, nBlocks)
|
||
res.Out = make([][]int, nBlocks)
|
||
for i := 0; i < nBlocks; i++ {
|
||
res.In[i] = in[i].ids()
|
||
res.Out[i] = out[i].ids()
|
||
}
|
||
return res
|
||
}
|
||
|
||
// bitset is a fixed-width bit vector over definition IDs.
|
||
type bitset []uint64
|
||
|
||
func (s bitset) set(i int) { s[i/64] |= 1 << (uint(i) % 64) }
|
||
func (s bitset) get(i int) bool {
|
||
return s[i/64]&(1<<(uint(i)%64)) != 0
|
||
}
|
||
|
||
func (s bitset) or(o bitset) {
|
||
for i := range s {
|
||
s[i] |= o[i]
|
||
}
|
||
}
|
||
|
||
func (s bitset) andNot(o bitset) {
|
||
for i := range s {
|
||
s[i] &^= o[i]
|
||
}
|
||
}
|
||
|
||
func (s bitset) clone() bitset {
|
||
c := make(bitset, len(s))
|
||
copy(c, s)
|
||
return c
|
||
}
|
||
|
||
func (s bitset) equal(o bitset) bool {
|
||
for i := range s {
|
||
if s[i] != o[i] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ids enumerates the set bits ascending.
|
||
func (s bitset) ids() []int {
|
||
var out []int
|
||
for w, word := range s {
|
||
for word != 0 {
|
||
out = append(out, w*64+bits.TrailingZeros64(word))
|
||
word &= word - 1
|
||
}
|
||
}
|
||
return out
|
||
}
|