// Copyright 2024 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 liveness // This file defines an "Intervals" helper type that stores a // sorted sequence of disjoint ranges or intervals. An Intervals // example: { [0,5) [9-12) [100,101) }, which corresponds to the // numbers 0-4, 9-11, and 100. Once an Intervals object is created, it // can be tested to see if it has any overlap with another Intervals // object, or it can be merged with another Intervals object to form a // union of the two. // // The intended use case for this helper is in describing object or // variable lifetime ranges within a linearized program representation // where each IR instruction has a slot or index. Example: // // b1: // 0 VarDef abc // 1 memset(abc,0) // 2 VarDef xyz // 3 memset(xyz,0) // 4 abc.f1 = 2 // 5 xyz.f3 = 9 // 6 if q goto B4 // 7 B3: z = xyz.x // 8 goto B5 // 9 B4: z = abc.x // // fallthrough // 10 B5: z++ // // To describe the lifetime of the variables above we might use these // intervals: // // "abc" [1,7), [9,10) // "xyz" [3,8) // // Clients can construct an Intervals object from a given IR sequence // using the "IntervalsBuilder" helper abstraction (one builder per // candidate variable), by making a // backwards sweep and invoking the Live/Kill methods to note the // starts and end of a given lifetime. For the example above, we would // expect to see this sequence of calls to Live/Kill: // // abc: Live(9), Kill(8), Live(6), Kill(0) // xyz: Live(8), Kill(2) import ( "fmt" "os" "slices" "strings" ) const debugtrace = false // Interval hols the range [st,en). type Interval struct { st, en int } // Intervals is a sequence of sorted, disjoint intervals. type Intervals []Interval func (i Interval) String() string { return fmt.Sprintf("[%d,%d)", i.st, i.en) } // TEMPORARY until bootstrap version catches up. func imin(i, j int) int { if i < j { return i } return j } // TEMPORARY until bootstrap version catches up. func imax(i, j int) int { if i > j { return i } return j } // Overlaps returns true if here is any overlap between i and i2. func (i Interval) Overlaps(i2 Interval) bool { return (imin(i.en, i2.en) - imax(i.st, i2.st)) > 0 } // adjacent returns true if the start of one interval is equal to the // end of another interval (e.g. they represent consecutive ranges). func (i1 Interval) adjacent(i2 Interval) bool { return i1.en == i2.st || i2.en == i1.st } // MergeInto merges interval i2 into i1. This version happens to // require that the two intervals either overlap or are adjacent. func (i1 *Interval) MergeInto(i2 Interval) error { if !i1.Overlaps(i2) && !i1.adjacent(i2) { return fmt.Errorf("merge method invoked on non-overlapping/non-adjacent") } i1.st = imin(i1.st, i2.st) i1.en = imax(i1.en, i2.en) return nil } // IntervalsBuilder is a helper for constructing intervals based on // live dataflow sets for a series of BBs where we're making a // backwards pass over each BB looking for uses and kills. The // expected use case is: // // - invoke MakeIntervalsBuilder to create a new object "b" // - series of calls to b.Live/b.Kill based on a backwards reverse layout // order scan over instructions // - invoke b.Finish() to produce final set // // See the Live method comment for an IR example. type IntervalsBuilder struct { s Intervals // index of last instruction visited plus 1 lidx int } func (c *IntervalsBuilder) last() int { return c.lidx - 1 } func (c *IntervalsBuilder) setLast(x int) { c.lidx = x + 1 } func (c *IntervalsBuilder) Finish() (Intervals, error) { // Reverse intervals list and check. slices.Reverse(c.s) if err := check(c.s); err != nil { return Intervals{}, err } r := c.s return r, nil } // Live method should be invoked on instruction at position p if instr // contains an upwards-exposed use of a resource. See the example in // the comment at the beginning of this file for an example. func (c *IntervalsBuilder) Live(pos int) error { if pos < 0 { return fmt.Errorf("bad pos, negative") } if c.last() == -1 { c.setLast(pos) if debugtrace { fmt.Fprintf(os.Stderr, "=-= begin lifetime at pos=%d\n", pos) } c.s = append(c.s, Interval{st: pos, en: pos + 1}) return nil } if pos >= c.last() { return fmt.Errorf("pos not decreasing") } // extend lifetime across this pos c.s[len(c.s)-1].st = pos c.setLast(pos) return nil } // Kill method should be invoked on instruction at position p if instr // should be treated as having a kill (lifetime end) for the // resource. See the example in the comment at the beginning of this // file for an example. Note that if we see a kill at position K for a // resource currently live since J, this will result in a lifetime // segment of [K+1,J+1), the assumption being that the first live // instruction will be the one after the kill position, not the kill // position itself. func (c *IntervalsBuilder) Kill(pos int) error { if pos < 0 { return fmt.Errorf("bad pos, negative") } if c.last() == -1 { return nil } if pos >= c.last() { return fmt.Errorf("pos not decreasing") } c.s[len(c.s)-1].st = pos + 1 // terminate lifetime c.setLast(-1) if debugtrace { fmt.Fprintf(os.Stderr, "=-= term lifetime at pos=%d\n", pos) } return nil } // check examines the intervals in "is" to try to find internal // inconsistencies or problems. func check(is Intervals) error { for i := 0; i < len(is); i++ { st := is[i].st en := is[i].en if en <= st { return fmt.Errorf("bad range elem %d:%d, en<=st", st, en) } if i == 0 { continue } // check for badly ordered starts pst := is[i-1].st pen := is[i-1].en if pst >= st { return fmt.Errorf("range start not ordered %d:%d less than prev %d:%d", st, en, pst, pen) } // check end of last range against start of this range if pen > st { return fmt.Errorf("bad range elem %d:%d overlaps prev %d:%d", st, en, pst, pen) } } return nil } func (is *Intervals) String() string { var sb strings.Builder for i := range *is { if i != 0 { sb.WriteString(" ") } sb.WriteString((*is)[i].String()) } return sb.String() } // intWithIdx holds an interval i and an index pairIndex storing i's // position (either 0 or 1) within some previously specified interval // pair ; a pairIndex of -1 is used to signal "end of // iteration". Used for Intervals operations, not expected to be // exported. type intWithIdx struct { i Interval pairIndex int } func (iwi intWithIdx) done() bool { return iwi.pairIndex == -1 } // pairVisitor provides a way to visit (iterate through) each interval // within a pair of Intervals in order of increasing start time. Expected // usage model: // // func example(i1, i2 Intervals) { // var pairVisitor pv // cur := pv.init(i1, i2); // for !cur.done() { // fmt.Printf("interval %s from i%d", cur.i.String(), cur.pairIndex+1) // cur = pv.nxt() // } // } // // Used internally for Intervals operations, not expected to be exported. type pairVisitor struct { cur intWithIdx i1pos int i2pos int i1, i2 Intervals } // init initializes a pairVisitor for the specified pair of intervals // i1 and i2 and returns an intWithIdx object that points to the first // interval by start position within i1/i2. func (pv *pairVisitor) init(i1, i2 Intervals) intWithIdx { pv.i1, pv.i2 = i1, i2 pv.cur = pv.sel() return pv.cur } // nxt advances the pairVisitor to the next interval by starting // position within the pair, returning an intWithIdx that describes // the interval. func (pv *pairVisitor) nxt() intWithIdx { if pv.cur.pairIndex == 0 { pv.i1pos++ } else { pv.i2pos++ } pv.cur = pv.sel() return pv.cur } // sel is a helper function used by 'init' and 'nxt' above; it selects // the earlier of the two intervals at the current positions within i1 // and i2, or a degenerate (pairIndex -1) intWithIdx if we have no // more intervals to visit. func (pv *pairVisitor) sel() intWithIdx { var c1, c2 intWithIdx if pv.i1pos >= len(pv.i1) { c1.pairIndex = -1 } else { c1 = intWithIdx{i: pv.i1[pv.i1pos], pairIndex: 0} } if pv.i2pos >= len(pv.i2) { c2.pairIndex = -1 } else { c2 = intWithIdx{i: pv.i2[pv.i2pos], pairIndex: 1} } if c1.pairIndex == -1 { return c2 } if c2.pairIndex == -1 { return c1 } if c1.i.st <= c2.i.st { return c1 } return c2 } // Overlaps returns whether any of the component ranges in is overlaps // with some range in is2. func (is Intervals) Overlaps(is2 Intervals) bool { // check for empty intervals if len(is) == 0 || len(is2) == 0 { return false } li := len(is) li2 := len(is2) // check for completely disjoint ranges if is[li-1].en <= is2[0].st || is[0].st >= is2[li2-1].en { return false } // walk the combined sets of intervals and check for piecewise // overlap. var pv pairVisitor first := pv.init(is, is2) for { second := pv.nxt() if second.done() { break } if first.pairIndex == second.pairIndex { first = second continue } if first.i.Overlaps(second.i) { return true } first = second } return false } // Merge combines the intervals from "is" and "is2" and returns // a new Intervals object containing all combined ranges from the // two inputs. func (is Intervals) Merge(is2 Intervals) Intervals { if len(is) == 0 { return is2 } else if len(is2) == 0 { return is } // walk the combined set of intervals and merge them together. var ret Intervals var pv pairVisitor cur := pv.init(is, is2) for { second := pv.nxt() if second.done() { break } // Check for overlap between cur and second. If no overlap // then add cur to result and move on. if !cur.i.Overlaps(second.i) && !cur.i.adjacent(second.i) { ret = append(ret, cur.i) cur = second continue } // cur overlaps with second; merge second into cur cur.i.MergeInto(second.i) } ret = append(ret, cur.i) return ret }