cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Cycles and arrays settle the same whatever order cells are computed in

Dami committed d3cfa07
- Every cell of a cycle is #REF!, even one whose formula catches or
  skips errors (IFERROR, COUNTIF), as in Sheets.
- An array that would spill into cells its formula reads, directly or
  through other formulas and arrays, is a circular dependency; the
  arrays of such a cycle are blocked together.
- When two arrays need the same cells, blank ones included, the one
  anchored first, row by row, spills.
- An array blocked before shows #REF! until it can spill, and one freed
  by another array or a merge going away spills again.
commit
d3cfa075f1c29680cafeea75a1847e9cd6986509
parent
d0c7d959e17c836f864abb0a2edd85c772a9e565
author
Dami <dami.etoile@gmail.com>
kind
fix risk score 2.0 of 3, Jev's estimate
10 files changed, +505 -65
Mdocs/formulas/README.md2+-
Mdocs/formulas/arrays.md7+-
Minternal/sheet/evaluate.go43+++++++-
Minternal/sheet/evaluate_test.go25+++++
Minternal/sheet/merge_undo_test.go14+++
Minternal/sheet/recalc.go47++++++---
Minternal/sheet/spill.go134+++++++++++++++++++++-----
Minternal/sheet/spill_test.go98+++++++++++++++++++
Ainternal/sheet/spillblock.go156++++++++++++++++++++++++++++++
Minternal/sheet/view.go44+++++---
M docs/formulas/README.md +1 -1
M docs/formulas/arrays.md +6 -1
M internal/sheet/evaluate.go +38 -5
@@ -34,6 +34,14 @@ type tooDeep struct{}
3434 type evaluator struct {3535 w *Workbook3636 pending []loc // cells put off, each needed by the one before37+ // stack holds the formulas being evaluated, outermost first, and38+ // cycle the cells found to be part of a cycle: every formula on the39+ // stack from the cell reached again up. Each of them is #REF!,40+ // whatever its formula makes of the #REF! it read (IFERROR, COUNTIF),41+ // as in Sheets, so a cycle's values don't depend on the order its42+ // cells were evaluated in.43+ stack []loc44+ cycle map[loc]bool3745 }3846 3947 // evaluate computes the cells marked dirty in each sheet's calc. Cells
@@ -79,7 +87,10 @@ func (e *evaluator) compute(s *Sheet, a Addr) Value {
7987 return v8088 }8189 switch st := s.calc[a]; {82- case st == visiting, st == deferred:90+ case st == visiting:91+ e.inCycle(loc{s, a})92+ return ErrRef93+ case st == deferred:8394 w.Circular = true8495 return ErrRef8596 case st != dirty:
@@ -119,17 +130,38 @@ func (e *evaluator) formula(s *Sheet, a Addr, c *Cell) {
119130 s.calc[a] = visiting120131 w.depth++121132 expr := s.bound(c)122- outer := w.evaluating123- w.evaluating = loc{s, a}133+ outer, l := w.evaluating, loc{s, a}134+ w.evaluating = l135+ e.stack = append(e.stack, l)124136 c.Value = functions.EvalCell(w.arith(expr), s.calcGet.lib, a)125- if s.calcGet.lib.Spilled() != nil || s.spills != nil {126- w.noteSpill(s, a, c)137+ e.stack = e.stack[:len(e.stack)-1]138+ arr := s.calcGet.lib.Spilled()139+ if e.cycle[l] {140+ c.Value, arr = ErrRef, nil141+ }142+ if arr != nil || s.spills != nil {143+ w.noteSpill(s, a, c, arr)127144 }128145 w.evaluating = outer129146 c.auto = functions.InferFormat(expr, s.calcFmt)130147 w.depth--131148 }132149 150+// inCycle notes that the formula at l, being evaluated, was reached151+// again: it and the formulas it led to, up the stack, form a cycle.152+func (e *evaluator) inCycle(l loc) {153+ e.w.Circular = true154+ if e.cycle == nil {155+ e.cycle = map[loc]bool{}156+ }157+ for i := len(e.stack) - 1; i >= 0; i-- {158+ e.cycle[e.stack[i]] = true159+ if e.stack[i] == l {160+ return161+ }162+ }163+}164+133165 // sweep evaluates s's dirty cells, reporting false if it went too deep134166 // and put off a cell.135167 func (e *evaluator) sweep(s *Sheet) (finished bool) {
@@ -188,5 +220,6 @@ func (e *evaluator) abandon(finished *bool) {
188220 }189221 e.w.depth = 0190222 e.w.evaluating = loc{}223+ e.stack = e.stack[:0]191224 *finished = false192225 }
M internal/sheet/evaluate_test.go +25 -0
M internal/sheet/merge_undo_test.go +14 -0
M internal/sheet/recalc.go +31 -16
@@ -81,10 +81,9 @@ func (w *Workbook) affected(changed []loc, volatiles bool) int {
8181 // readers marked too, but not those cells: spilling changed what an8282 // anchor shows (spill.go), not its formula.8383 func (w *Workbook) affectedBy(changed, readers []loc, volatiles bool) int {84- m := marking{w: w, queue: append([]loc(nil), changed...), named: w.namedInUse(), byName: map[*Sheet]bool{}}84+ m := marking{readerLookup: w.readerLookup(), queue: append([]loc(nil), changed...)}8585 for _, s := range w.sheets {8686 s.calc = make(map[Addr]int)87- m.byName[s] = w.crossKeys[formula.SheetKey(s.name)] > 08887 if !volatiles {8988 continue9089 }
@@ -109,15 +108,28 @@ func (w *Workbook) affectedBy(changed, readers []loc, volatiles bool) int {
109108 return n110109 }111110 112-// marking is the state of affected: the cells still to mark, and what it113-// looks up once rather than for every cell.111+// marking is the state of affected: the cells still to mark.114112 type marking struct {113+ readerLookup114+ queue []loc115+}116+117+// readerLookup finds the formulas that read a cell, with what it looks118+// up once rather than for every cell.119+type readerLookup struct {115120 w *Workbook116- queue []loc117121 named []namedUsers118122 byName map[*Sheet]bool // whether some formula names the sheet119123 }120124 125+func (w *Workbook) readerLookup() readerLookup {126+ r := readerLookup{w: w, named: w.namedInUse(), byName: make(map[*Sheet]bool, len(w.sheets))}127+ for _, s := range w.sheets {128+ r.byName[s] = w.crossKeys[formula.SheetKey(s.name)] > 0129+ }130+ return r131+}132+121133 // namedUsers are the formulas that use a named range, with its cells.122134 type namedUsers struct {123135 s *Sheet
@@ -144,27 +156,30 @@ func (m *marking) push(u loc) {
144156 }145157 }146158 147-// readersOf queues the formulas that read the cell l: by reference, by a148-// range over it, through a named range, or by naming its sheet.149-func (m *marking) readersOf(l loc) {159+// readersOf queues the formulas that read the cell l.160+func (m *marking) readersOf(l loc) { m.each(l, m.push) }161+162+// each calls fn with the formulas that read the cell l: by reference, by163+// a range over it, through a named range, or by naming its sheet.164+func (r readerLookup) each(l loc, fn func(loc)) {150165 s, a := l.s, l.a151166 for d := range s.dependents[a] {152- m.push(loc{s, d})167+ fn(loc{s, d})153168 }154- s.rangeUsers.readers(a, func(u Addr) { m.push(loc{s, u}) })155- for _, nu := range m.named {169+ s.rangeUsers.readers(a, func(u Addr) { fn(loc{s, u}) })170+ for _, nu := range r.named {156171 if nu.s == s && nu.r.Contains(a) {157172 for u := range nu.users {158- m.push(u)173+ fn(u)159174 }160175 }161176 }162- if !m.byName[s] {177+ if !r.byName[s] {163178 return // no formula names this sheet164179 }165- for u := range m.w.crossUsers {166- if m.w.crossReads(u, l) {167- m.push(u)180+ for u := range r.w.crossUsers {181+ if r.w.crossReads(u, l) {182+ fn(u)168183 }169184 }170185 }
M internal/sheet/spill.go +109 -25
@@ -33,6 +33,9 @@ import (
3333 type spill struct {3434 area Rect // the cells it covers, the anchor first; when blocked, those it needs3535 why string // why the anchor shows #REF! instead, or ""36+ // circular is set when it's blocked by a cycle through its cells37+ // (spillblock.go).38+ circular bool3639 // arr and auto are the array written and its format, so the same3740 // array computed again isn't written again, unless stale says a cell3841 // of area changed since.
@@ -55,22 +58,29 @@ type spillWork map[loc]pendingSpill
5558 5659 type pendingSpill struct {5760 arr *functions.Array // nil when the formula computed one value58- v Value // what the formula computed, before spilling61+ v Value // what the anchor showed formulas read meanwhile62+ top Value // what the formula computed, the array's first value5963 }6064 61-// noteSpill records what the formula of c, at a on s, computed, when it62-// spills or did.65+// noteSpill records what the formula of c, at a on s, computed (top,66+// and arr, nil for one value), when it spills or did. An array blocked67+// before keeps showing #REF! until spilling finds it free, so formulas68+// reading it don't see it come and go while it stays blocked.6369 //6470 //go:noinline65-func (w *Workbook) noteSpill(s *Sheet, a Addr, c *Cell) {66- arr, v, l := s.calcGet.lib.Spilled(), c.Value, loc{s, a}67- if arr == nil && s.spills[a] == nil {71+func (w *Workbook) noteSpill(s *Sheet, a Addr, c *Cell, arr *functions.Array) {72+ l, top := loc{s, a}, c.Value73+ sp := s.spills[a]74+ if arr == nil && sp == nil {6875 return6976 }77+ if arr != nil && sp != nil && sp.why != "" {78+ c.Value = ErrRef79+ }7080 if w.spillWork == nil {7181 w.spillWork = spillWork{}7282 }73- w.spillWork[l] = pendingSpill{arr, v}83+ w.spillWork[l] = pendingSpill{arr, c.Value, top}7484 }7585 7686 // settleSpills writes the arrays evaluated, then recalculates what reads
@@ -125,22 +135,39 @@ func (s *Sheet) applySpill(a Addr, p pendingSpill) ([]loc, bool) {
125135 if c == nil || !c.IsFormula() || p.arr == nil {126136 return s.dropSpill(a), false127137 }138+ area, why, later := s.spillArea(a, p.arr)139+ var cycle []loc140+ circ := false141+ if why == "" {142+ // Checked each time the anchor is computed: a formula typed since143+ // it spilled may lead from its cells back to it.144+ why, cycle = s.spillsIntoItself(a, area)145+ circ = why != ""146+ }128147 old := s.spills[a]129- if old != nil && old.why == "" && !old.stale && old.auto == c.auto && sameArray(old.arr, p.arr) {148+ if why == "" && len(later) == 0 && old != nil && old.why == "" && !old.stale && old.auto == c.auto && sameArray(old.arr, p.arr) {130149 return nil, false // written already, and nothing has changed in its way131150 }132- area, why := s.spillArea(a, p.arr)151+ var changed []loc152+ if circ != (old != nil && old.circular) {153+ changed = s.wb.recheckArrays(loc{s, a}) // a cycle through arrays began or ended here154+ }133155 if why != "" {134- changed := s.dropSpill(a)135- s.setSpill(a, &spill{area: area, why: why})156+ changed = append(changed, s.dropSpill(a)...)157+ s.setSpill(a, &spill{area: area, why: why, circular: circ})136158 c.Value = ErrRef137- return changed, p.v != ErrRef159+ return append(changed, blockCircular(cycle)...), p.v != ErrRef160+ }161+ for _, b := range later {162+ // b's array gives way, and b is computed again to find itself163+ // blocked by this one.164+ changed = append(append(changed, s.dropSpill(b)...), loc{s, b})138165 }139- var changed []loc140166 if old != nil && old.why == "" {141- changed = s.clearSpilled(old.area, area)167+ changed = append(changed, s.clearSpilled(old.area, area)...)142168 }143169 s.setSpill(a, &spill{area: area, arr: p.arr, auto: c.auto})170+ c.Value = p.top144171 for r := range area.To.Row - a.Row + 1 {145172 for col := range area.To.Col - a.Col + 1 {146173 if r == 0 && col == 0 {
@@ -152,40 +179,82 @@ func (s *Sheet) applySpill(a Addr, p pendingSpill) ([]loc, bool) {
152179 }153180 }154181 }155- return changed, false182+ return changed, p.v != p.top156183 }157184 158185 // spillArea is where the array computed at a goes, or why it can't: past159186 // the sheet's edge, past max-cells, or over a cell that isn't empty.160187 // Blank entries past the array's data aren't spilled, so a whole column161188 // read as an array spills what it holds.162-func (s *Sheet) spillArea(a Addr, arr *functions.Array) (Rect, string) {189+//190+// When two arrays need the same cells, the one whose anchor comes first,191+// row by row, gets them, whichever spilled first: later lists the192+// anchors whose arrays give way to this one. So what spills doesn't193+// depend on the order formulas were typed or computed in, and a file194+// opens as it was saved.195+func (s *Sheet) spillArea(a Addr, arr *functions.Array) (area Rect, why string, later []Addr) {163196 rows, cols := arr.Rows, arr.Cols164197 if arr.Fill.Kind == Empty {165198 rows, cols = min(rows, max(arr.DRows, 1)), min(cols, max(arr.DCols, 1))166199 }167- area := Rect{From: a, To: Addr{Col: a.Col + cols - 1, Row: a.Row + rows - 1}}200+ area = Rect{From: a, To: Addr{Col: a.Col + cols - 1, Row: a.Row + rows - 1}}168201 switch {169202 case area.To.Row >= MaxRows || area.To.Col >= MaxCols:170203 area.To = Addr{Col: min(area.To.Col, MaxCols-1), Row: min(area.To.Row, MaxRows-1)}171- return area, "Array result was not expanded because it would go past the edge of the sheet"204+ return area, "Array result was not expanded because it would go past the edge of the sheet", nil172205 case rows*cols > MaxCells():173- return area, fmt.Sprintf("Array result was not expanded because it would write more cells than max-cells allows (%d)", MaxCells())206+ return area, fmt.Sprintf("Array result was not expanded because it would write more cells than max-cells allows (%d)", MaxCells()), nil174207 }175208 if ms := s.MergesIn(area); len(ms) > 0 {176- return area, "Array result was not expanded because it would overwrite merged cells in " + ms[0].String()209+ return area, "Array result was not expanded because it would overwrite merged cells in " + ms[0].String(), nil210+ }211+ if at, blocked := s.arraysInTheWay(a, area, &later); blocked {212+ return area, "Array result was not expanded because it would overwrite data in " + at.String(), nil177213 }178214 old := s.spills[a]179215 for at := range s.cells.anyKeysIn(area) {180216 if at == a || !s.cells.filledAt(at) {181217 continue182218 }183- if s.cells.derivedAt(at) == slotSpill && old != nil && old.why == "" && old.area.Contains(at) {184- continue219+ if s.cells.derivedAt(at) == slotSpill {220+ if old != nil && old.why == "" && old.area.Contains(at) {221+ continue222+ }223+ if _, ok := s.SpillAnchor(at); ok {224+ continue // an array's, found above225+ }185226 }186- return area, "Array result was not expanded because it would overwrite data in " + at.String()227+ return area, "Array result was not expanded because it would overwrite data in " + at.String(), nil187228 }188- return area, ""229+ return area, "", later230+}231+232+// arraysInTheWay finds the other arrays spilled over area, where the233+// array at a would go, blank cells of theirs included, as Sheets keeps234+// an array's whole range: it reports a cell of one whose anchor comes235+// before a, which blocks it, and adds to later those whose anchors come236+// after.237+func (s *Sheet) arraysInTheWay(a Addr, area Rect, later *[]Addr) (Addr, bool) {238+ if n := len(s.spills); n == 0 || n == 1 && s.spills[a] != nil {239+ return Addr{}, false240+ }241+ var at Addr242+ blocked := false243+ for row := area.From.Row; row <= area.To.Row && !blocked; row++ {244+ for col := area.From.Col; col <= area.To.Col && !blocked; col++ {245+ cell := Addr{Col: col, Row: row}246+ s.spillAt.readers(cell, func(b Addr) {247+ switch sp := s.spills[b]; {248+ case b == a || sp == nil || sp.why != "" || blocked:249+ case b.Row < a.Row || b.Row == a.Row && b.Col < a.Col:250+ at, blocked = cell, true251+ case !slices.Contains(*later, b):252+ *later = append(*later, b)253+ }254+ })255+ }256+ }257+ return at, blocked189258 }190259 191260 // writeSpilled makes the cell at a show v as a spilled cell, keeping its
@@ -229,7 +298,22 @@ func (s *Sheet) clearSpilled(area, keep Rect) []loc {
229298 s.setDerived(at, s.cells.get(at).leftover())230299 changed = append(changed, loc{s, at})231300 }232- return changed301+ return append(changed, s.wakeBlocked(area, keep)...)302+}303+304+// wakeBlocked returns the anchors of arrays blocked from spilling over305+// the cells of area outside keep, which an array leaving them freed, to306+// compute again. The cells it left may have been blank, which changing307+// wouldn't find.308+func (s *Sheet) wakeBlocked(area, keep Rect) []loc {309+ var out []loc310+ for b, sp := range s.spills {311+ if x, ok := intersectRect(sp.area, area); ok && sp.why != "" && !(keep.Contains(x.From) && keep.Contains(x.To)) {312+ sp.stale = true313+ out = append(out, loc{s, b})314+ }315+ }316+ return out233317 }234318 235319 // dropSpill forgets the anchor at a's spill and clears its cells,
M internal/sheet/spill_test.go +98 -0
@@ -204,3 +204,101 @@ func TestSpillRules(t *testing.T) {
204204 t.Errorf("A4's look = %+v, want styled and invalid", l)205205 }206206 }207+208+// When two arrays need the same cells, the one anchored first, row by209+// row, gets them, whichever was typed first, even where the other's210+// cells are blank; reopening the file agrees.211+func TestOverlappingArraysFirstAnchorWins(t *testing.T) {212+ cases := []struct {213+ first, second [2]string214+ win, lose string215+ }{216+ {[2]string{"C9", "=SORT(E4:G8)"}, [2]string{"D8", "=SEQUENCE(2)"}, "D8", "C9"},217+ {[2]string{"D8", "=SEQUENCE(2)"}, [2]string{"C9", "=SORT(E4:G8)"}, "D8", "C9"},218+ {[2]string{"E5", "=SEQUENCE(3,2)"}, [2]string{"F4", "=SEQUENCE(3)"}, "F4", "E5"},219+ {[2]string{"F4", "=SEQUENCE(3)"}, [2]string{"E5", "=SEQUENCE(3,2)"}, "F4", "E5"},220+ }221+ for _, c := range cases {222+ s := sheetOf(t, map[string]string{"E4": "5", "G8": "1"})223+ s.Set(at(c.first[0]), c.first[1])224+ s.Set(at(c.second[0]), c.second[1])225+ for name, sh := range map[string]*Sheet{"typed": s, "reopened": roundTrip(t, s)} {226+ if v := sh.Value(at(c.lose)); v != ErrRef {227+ t.Errorf("%v then %v, %s: %s = %v", c.first, c.second, name, c.lose, v)228+ }229+ if _, ok := sh.SpillArea(at(c.win)); !ok {230+ t.Errorf("%v then %v, %s: %s doesn't spill", c.first, c.second, name, c.win)231+ }232+ }233+ // Taking the second back frees the first to spill.234+ s.Undo()235+ if _, ok := s.SpillArea(at(c.first[0])); !ok {236+ t.Errorf("%v then %v, undone: %s doesn't spill", c.first, c.second, c.first[0])237+ }238+ }239+}240+241+// A formula reading an array blocked by that formula's own cell reads242+// the anchor's #REF!, settling rather than going back and forth as the243+// formula's own array comes and goes.244+func TestReadingABlockedArray(t *testing.T) {245+ s := New()246+ s.Set(at("A18"), "=SORT(A16:C16)")247+ s.Set(at("A16"), "=SEQUENCE(3)")248+ for name, sh := range map[string]*Sheet{"typed": s, "reopened": roundTrip(t, s)} {249+ for _, a := range []string{"A16", "A18"} {250+ if v := sh.Value(at(a)); v != ErrRef {251+ t.Errorf("%s: %s = %v", name, a, v)252+ }253+ }254+ if sh.Book().Circular {255+ t.Errorf("%s: arrays kept moving", name)256+ }257+ }258+}259+260+// An array whose formula reads its own cells through another formula261+// is a circular dependency too: #REF! for it and what reads it, typed,262+// recalculated or opened, without going back and forth.263+func TestSpillIntoItsInputThroughAFormula(t *testing.T) {264+ s := New()265+ s.Set(at("F11"), "=VLOOKUP(F14,C17:D19,1,FALSE)")266+ s.Set(at("C19"), "=SORT(E9:F11)")267+ s.Set(at("H15"), "=MAX(E:C)")268+ check := func(name string, sh *Sheet) {269+ t.Helper()270+ for _, a := range []string{"C19", "H15"} {271+ if v := sh.Value(at(a)); v != ErrRef {272+ t.Errorf("%s: %s = %v", name, a, v)273+ }274+ }275+ if got := sh.ExplainError(at("C19")); !strings.Contains(got, "Circular dependency") {276+ t.Errorf("%s: %q", name, got)277+ }278+ }279+ check("typed", s)280+ check("reopened", roundTrip(t, s))281+ s.RecalcAll()282+ check("recalculated", s)283+ if s.Book().Circular {284+ t.Error("arrays kept moving")285+ }286+}287+288+// An array that would spill into the cells its formula reads is a289+// circular dependency: #REF!, whether typed or opened, rather than a290+// result that depends on what was computed first.291+func TestSpillIntoItsInput(t *testing.T) {292+ s := sheetOf(t, map[string]string{"C8": "3", "A9": "=SORT(B8:D9)"})293+ for name, sh := range map[string]*Sheet{"typed": s, "reopened": roundTrip(t, s)} {294+ if v := sh.Value(at("A9")); v != ErrRef {295+ t.Errorf("%s: A9 = %v", name, v)296+ }297+ if sh.Filled(at("B9")) || sh.Filled(at("B10")) {298+ t.Errorf("%s: spilled into its input", name)299+ }300+ if got := sh.ExplainError(at("A9")); !strings.Contains(got, "Circular dependency") {301+ t.Errorf("%s: %q", name, got)302+ }303+ }304+}
A internal/sheet/spillblock.go +156 -0
@@ -0,0 +1,156 @@
1+package sheet2+3+// spillsIntoItself finds whether the array at a would spill over a cell4+// its formula reads, other than a itself, directly or through other5+// formulas and the arrays they spill: spilling would change what6+// computed it, a circular dependency, whose values would depend on the7+// order cells were computed in. It returns why, or "", and the other8+// arrays on the way, which are part of the cycle and give way too.9+//10+// It walks the formulas that read the area's cells, and those that read11+// them or what they spill, looking for the anchor: the cells a12+// recalculation after the spill would visit anyway. An array blocked by13+// a cycle counts as spilling over the cells it needs, so the arrays of14+// a cycle stay blocked together whichever was computed first.15+func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {16+ if area.From == area.To {17+ return "", nil18+ }19+ w := &spillWalk{look: s.wb.readerLookup(), target: loc{s, a}, seen: map[loc]bool{{s, a}: true}, parent: -1}20+ if w.cells(s, area, a, loc{}, true) {21+ return circular(w.origin), nil22+ }23+ for i := 0; i < len(w.queue); i++ {24+ u := w.queue[i]25+ w.origin, w.parent = w.nodes[i].from, i26+ // An array's cells first, so a cycle through them is found as27+ // one, and the array blocked with it, rather than through the28+ // anchor alone.29+ if sp := u.s.spills[u.a]; sp != nil && (sp.why == "" || sp.circular) {30+ w.cells(u.s, sp.area, u.a, u, false)31+ }32+ if !w.found {33+ w.look.each(u, w.visitVia(loc{}))34+ }35+ if w.found {36+ return circular(w.origin), w.arrays()37+ }38+ }39+ return "", nil40+}41+42+// spillWalk is the state of spillsIntoItself: the formulas reached, in43+// the order found, each with how it was reached.44+type spillWalk struct {45+ look readerLookup46+ target loc47+ seen map[loc]bool48+ queue []loc49+ nodes []walkNode50+ origin Addr // the cell of the area the walk is on51+ parent int // the node the walk is on, -1 at the start52+ via loc // the array whose cells the walk is on, if any53+ found bool54+ last int // the node the target was reached from55+}56+57+// walkNode is how the walk reached a formula: from which cell of the58+// area, from which node, and through the cells of which array.59+type walkNode struct {60+ from Addr61+ parent int62+ via loc63+}64+65+// visitVia returns what the walk does with each formula it reaches66+// through the cells of the array via (none when zero).67+func (w *spillWalk) visitVia(via loc) func(loc) {68+ w.via = via69+ return w.visit70+}71+72+func (w *spillWalk) visit(u loc) {73+ switch {74+ case w.found:75+ case u == w.target:76+ w.found, w.last = true, len(w.nodes)77+ w.nodes = append(w.nodes, walkNode{w.origin, w.parent, w.via})78+ case !w.seen[u]:79+ w.seen[u] = true80+ w.queue = append(w.queue, u)81+ w.nodes = append(w.nodes, walkNode{w.origin, w.parent, w.via})82+ }83+}84+85+// cells visits the formulas reading the cells of area on s other than86+// skip, the cells of the array via, and reports whether one leads to87+// the target. At the start, each cell is its own origin.88+func (w *spillWalk) cells(s *Sheet, area Rect, skip Addr, via loc, start bool) bool {89+ visit := w.visitVia(via)90+ for row := area.From.Row; row <= area.To.Row; row++ {91+ for col := area.From.Col; col <= area.To.Col; col++ {92+ at := Addr{Col: col, Row: row}93+ if at == skip {94+ continue95+ }96+ if start {97+ w.origin = at98+ }99+ if w.look.each(loc{s, at}, visit); w.found {100+ return true101+ }102+ }103+ }104+ return false105+}106+107+// arrays are the arrays whose cells the path to the target went108+// through.109+func (w *spillWalk) arrays() []loc {110+ var out []loc111+ for i := w.last; i >= 0; i = w.nodes[i].parent {112+ if v := w.nodes[i].via; v.s != nil {113+ out = append(out, v)114+ }115+ }116+ return out117+}118+119+func circular(at Addr) string {120+ return "Circular dependency: the array would spill into " + at.String() + ", which its formula reads"121+}122+123+// recheckArrays returns every array's anchor but l's, spilling or124+// blocked by a cycle, to compute again: a cycle through arrays that125+// begins or ends at l may take in or let go of others, which checking126+// each again finds, whichever order they're in.127+func (w *Workbook) recheckArrays(l loc) []loc {128+ var out []loc129+ for _, s := range w.sheets {130+ for b, sp := range s.spills {131+ if (loc{s, b}) != l && (sp.why == "" || sp.circular) {132+ sp.stale = true133+ out = append(out, loc{s, b})134+ }135+ }136+ }137+ return out138+}139+140+// blockCircular blocks the arrays at l, part of a cycle with another,141+// and returns the cells that changed, with the anchors, to compute142+// again, finding the cycle themselves.143+func blockCircular(arrays []loc) []loc {144+ var changed []loc145+ for _, l := range arrays {146+ sp := l.s.spills[l.a]147+ if sp == nil || sp.circular {148+ continue149+ }150+ area := sp.area151+ changed = append(changed, l.s.dropSpill(l.a)...)152+ l.s.setSpill(l.a, &spill{area: area, why: "Circular dependency through another array", circular: true})153+ changed = append(changed, l)154+ }155+ return changed156+}
M internal/sheet/view.go +27 -17