An array spilling over an output's table blocked it, its anchor showing #REF!, which the array read, came out smaller and freed the output, which spilled over again once sent its rows: the two went around without settling, and a reopened file ended elsewhere (randedit seed 10782). The array is now blocked by the cycle, and checked again when the table moves.
- commit
- 31f8f178a438fde501dc7f2a6e4920843e9afcff
- parent
- 07dc4228e10d21a249f6894d1cf6074546f66f10
- author
- Dami <dami.etoile@gmail.com>
- kind
- fix risk score 1.8 of 3, Jev's estimate
| M | internal/ | 56 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 30 | ++++++++++++++++ |
| M | internal/ | 10 | +++++ |
| M | internal/ | 13 | ++++++- |
@@ -132,3 +132,59 @@ func (s *Sheet) wakeAllRegions() {
132132 }133133 }134134 }135+136+// table is the cells the region with meta me takes: its table shown or,137+// blocked, the one it needs; none before its rows first arrive.138+func (me *regionMeta) table() (Rect, bool) {139+ switch {140+ case me == nil || !me.has:141+ return Rect{}, false142+ case me.why != "":143+ return me.need, true144+ }145+ return me.written, true146+}147+148+// regionMoved returns the anchors of the arrays over the region's149+// table, before and after it changed, spilling or blocked by a cycle,150+// to compute again: an array reading the table's cells is blocked by151+// it (spillsIntoItself) wherever the table goes, which the cells it152+// reads may not tell it.153+func (s *Sheet) regionMoved(before, after Rect, had bool) []loc {154+ var out []loc155+ for b, sp := range s.spills {156+ if (sp.why == "" || sp.circular) && (had && overlaps(sp.area, before) || overlaps(sp.area, after)) {157+ sp.stale = true158+ out = append(out, loc{s, b})159+ }160+ }161+ return out162+}163+164+// heldByArrays is the region r's table, when an array spilling over it165+// decides whether it shows: shown, or blocked by arrays alone, which it166+// would show without.167+func (s *Sheet) heldByArrays(r Region) (Rect, bool) {168+ me := s.regionMeta[nameKey(r.Name)]169+ t, ok := me.table()170+ if !ok || me.why == "" {171+ return t, ok172+ }173+ if t.To.Row >= MaxRows || t.To.Col >= MaxCols {174+ return Rect{}, false175+ }176+ for a := range s.cells.anyKeysIn(t) {177+ if a == r.At || !s.cells.filledAt(a) {178+ continue179+ }180+ if _, spilled := s.SpillAnchor(a); !spilled || s.cells.derivedAt(a) != slotSpill {181+ return Rect{}, false182+ }183+ }184+ for _, q := range s.regions.list {185+ if q.Name != r.Name && t.Contains(q.At) {186+ return Rect{}, false187+ }188+ }189+ return t, true190+}@@ -101,3 +101,33 @@ func TestPasteIntoARegionsBlankCell(t *testing.T) {
101101 apply(t, s, op)102102 wantShown(t, s, map[string]string{"A1": "#REF!", "A2": "", "B2": "9"})103103 }104+105+// An array that would spill over a region's table whose cells it reads106+// is a circular dependency: spilling, it would block the region and107+// read its #REF!, come out smaller and free it. It shows #REF! and the108+// region its table, whichever came first, and opened again.109+func TestArrayOverRegionItReadsTheCellsOf(t *testing.T) {110+ op := LiveOp{Region: "a", Reset: true, Header: liveRow("x", "y"), Rows: []LiveRow{liveRow("1", "2")}}111+ feeds := map[string]LiveOp{"a": op}112+ want := map[string]string{"I9": "#REF!", "H10": "x", "I11": "2"}113+ for _, arrayFirst := range []bool{false, true} {114+ s := New()115+ sent(t, s, "a", "H10")116+ if !arrayFirst {117+ apply(t, s, op)118+ }119+ s.Set(at("I9"), "=SORT(H9:H11)")120+ if arrayFirst {121+ apply(t, s, op)122+ }123+ feedStale(s.Book(), feeds)124+ if stale := s.Book().StaleOutputs(); len(stale) > 0 {125+ t.Errorf("array first %v: still stale %v", arrayFirst, stale)126+ }127+ wantShown(t, s, want)128+ opened := roundTrip(t, s)129+ feedAll(opened.Book(), feeds)130+ feedStale(opened.Book(), feeds)131+ wantShown(t, opened, want)132+ }133+}@@ -76,6 +76,9 @@ func (s *Sheet) writeRegions() []loc {
7676 for k, me := range s.regionMeta {7777 if s.regionIndex(k) < 0 {7878 changed = append(changed, s.clearOwned(me, noRect)...)79+ if t, ok := me.table(); ok {80+ changed = append(changed, s.regionMoved(t, noRect, true)...)81+ }7982 delete(s.regionMeta, k)8083 s.wakeAllRegions()8184 for u := range s.wb.nameUsers[nameKey(regionPrefix+k)] {@@ -97,6 +100,9 @@ func (s *Sheet) writeRegions() []loc {
97100 // naming it read #REF! until then, so they're among the cells returned.98101 func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {99102 changed := s.clearOwned(me, noRect)103+ if t, ok := me.table(); ok {104+ changed = append(changed, s.regionMoved(t, noRect, true)...)105+ }100106 if me.has {101107 changed = s.regionReaders(r, changed)102108 }@@ -115,6 +121,7 @@ func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {
115121 func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []LiveRow, reset bool) []loc {116122 cols, nrows := tableSize(me, header, rows, reset)117123 was, had, shape := me.written, me.has, [2]int{me.rows, me.cols}124+ before, _ := me.table()118125 need := s.tableArea(r, cols, nrows)119126 changed := s.makeRoomAt(r)120127 whole := me.why != "" || me.placed // it may not hold every cell it wrote@@ -160,6 +167,9 @@ func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []Live
160167 for _, c := range changed {161168 s.freedFor(c.a)162169 }170+ if after, _ := me.table(); !had || after != before {171+ changed = append(changed, s.regionMoved(before, after, had)...)172+ }163173 if !had || was != me.written || shape != [2]int{me.rows, me.cols} { // moved, or its table changed shape164174 changed = s.regionReaders(r, changed)165175 }@@ -19,7 +19,13 @@ import "slices"
1919 // (regionsGiveWay), so what reads it by name reads #REF! once the array2020 // spills, whatever the array holds: the walk doesn't follow it, or the2121 // array would be blocked or not by whether the region's rows arrived22-// before it spilled.22+// before it spilled. A formula reading the table's cells, though, leads23+// the walk from each of them, those outside area too: spilling empties24+// them, the anchor showing #REF!, which may make the array smaller and25+// free the region, spilling again once it's sent its rows, without26+// end. The table is the one shown or, blocked by arrays alone, the one27+// it needs, so the array is blocked either way (regionMoved has it28+// checked again).2329 func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {2430 if area.From == area.To {2531 return "", nil@@ -30,6 +36,11 @@ func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {
3036 if w.cells(s, area, a, loc{}, true) {3137 return circular(w.origin), nil3238 }39+ for _, r := range s.regions.list {40+ if t, ok := s.heldByArrays(r); ok && overlaps(t, area) && w.cells(s, t, a, loc{}, true) {41+ return "Circular dependency: the array would spill over " + r.Name + "'s table, whose cells its formula reads", nil42+ }43+ }3344 for i := 0; i < len(w.queue); i++ {3445 u := w.queue[i]3546 w.origin, w.parent = w.nodes[i].from, i