cat / 012
clone

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

main
012 / internal / functions / eval.go
eval.go 9.0K
1package functions23import (4 "src.rest/cat/012/internal/formula"5 "src.rest/cat/012/internal/value"6)78// Eval computes a formula where one value is wanted, reading the cells9// it refers to through get. A range where one value is wanted reads as10// the cell in the row or column last given to EvalAt (see intersect),11// and an array as its first entry.12func Eval(n Node, get *Reader) Value { return EvalAt(n, get, get.here) }1314// EvalAt computes the formula in the cell at as one value, so that a15// range used where one value is wanted reads as the cell in at's row or16// column (implicit intersection, as Sheets and Excel do). Evaluations17// nest, one cell's formula reading another's through the same Reader, so18// the cell before is given back afterwards.19func EvalAt(n Node, get *Reader, at Addr) Value { return evalTop(n, get, at, false) }2021// EvalCell computes the formula in the cell at, as EvalAt, keeping the22// array it computes when that is several values for Spilled: the cell23// shows the first, and the engine spills the array from it. A 1x1 array24// is one value. A range that is the whole formula is an array, as Sheets25// spills =B2:B9.26func EvalCell(n Node, get *Reader, at Addr) Value { return evalTop(n, get, at, true) }2728// Spilled returns the array the formula EvalCell computed last spills,29// or nil when it computed one value.30func (rd *Reader) Spilled() *Array { return rd.spilled }3132// wholeRange is a range that is a cell's whole formula read through33// ARRAYFORMULA, so it spills.34//35//go:noinline36func wholeRange(n Node) Node { return formula.Call{Fn: arrayFormula, Args: []Node{n}} }3738// evalTop evaluates a cell's formula with a fresh evaluation state,39// giving the state of the formula it nests in (a cell read while another40// is evaluated) back afterwards. A chain of formulas nests once per41// cell, and the goroutine's stack grows with every level, so evalTop42// keeps its frame small: the common state, outside any array context or43// LET, in two locals, the rest on the Reader's own stack.44func evalTop(n Node, get *Reader, at Addr, arrays bool) Value {45 if _, ok := n.(formula.Range); ok && arrays {46 n = wholeRange(n)47 }48 here, want := get.here, get.wantArr49 rich := get.lift > 0 || get.scope != nil50 if rich {51 get.push()52 }53 get.here, get.wantArr = at, arrays54 get.nest++55 v := eval(n, get)56 get.spilled = nil57 if v.Kind == value.Array {58 v = get.spill(v)59 }60 get.here, get.wantArr = here, want61 if rich {62 get.pop()63 }64 if get.nest--; get.nest == 0 && len(get.arena) > 0 {65 get.empty()66 }67 return v68}6970// push saves the evaluation state on the Reader's stack, and pop gives71// it back.72//73//go:noinline74func (rd *Reader) push() {75 rd.outer = append(rd.outer, rd.evalState)76 rd.lift, rd.scope = 0, nil77}7879//go:noinline80func (rd *Reader) pop() {81 last := len(rd.outer) - 182 rd.evalState = rd.outer[last]83 rd.outer[last] = evalState{}84 rd.outer = rd.outer[:last]85}8687// spill keeps the array v stands for, for Spilled, and returns its88// first value.89//90//go:noinline91func (rd *Reader) spill(v Value) Value {92 rd.spilled = rd.arrayOf(v)93 return rd.first(v)94}9596// empty empties the arena.97//98//go:noinline99func (rd *Reader) empty() {100 clear(rd.arena)101 rd.arena = rd.arena[:0]102}103104// Reset forgets the evaluations in progress, which the engine abandoned105// part way (its evaluate.go), so the next starts afresh.106func (rd *Reader) Reset() {107 rd.evalState, rd.nest, rd.spilled = evalState{}, 0, nil108 clear(rd.outer)109 rd.outer = rd.outer[:0]110 rd.empty()111}112113func eval(n Node, get lookup) Value {114 switch n := n.(type) {115 case formula.Num:116 return num(n.V)117 case formula.Str:118 return Value{Kind: value.Text, Str: n.V}119 case formula.Bool:120 return boolean(n.V)121 case formula.Ref:122 return get.cell(n.Sheet, n.Addr)123 case formula.Name:124 return value.ErrName125 case formula.RefErr:126 return value.ErrRef127 case formula.Empty:128 return Value{}129 case formula.Range:130 if get.lift > 0 {131 return get.rangeArray(n.Sheet, n.Rect)132 }133 if a, ok := get.intersect(n.Rect); ok {134 return get.cell(n.Sheet, a)135 }136 return value.ErrValue137 case formula.Unary:138 return evalUnary(n, get, false)139 case decUnary:140 return evalUnary(formula.Unary(n), get, true)141 case formula.Binary:142 return evalBinary(n, get, false)143 case decBinary:144 return evalBinary(formula.Binary(n), get, true)145 case formula.Call:146 // In an array context a function of one value is mapped over the147 // arrays it's given (lift.go); otherwise its arguments are read as148 // it reads them, and what it returns is one value unless the149 // caller asked for an array.150 *get.depth++ // see the engine's evaluate.go; operators count in theirs151 f := funcOf(n)152 var v Value153 if get.lift > 0 && f.arrays != takesArrays {154 v = liftCall(f, n.Args, get)155 } else if sv, ok := get.streamed(f, n.Args); ok {156 v = sv157 } else {158 want := get.wantArr159 get.wantArr = f.arrays != liftScalar160 v = f.call(n.Args, get)161 get.wantArr = want162 }163 *get.depth--164 if v.Kind == value.Array {165 return get.reduce(v)166 }167 return v168 }169 return get.reduce(evalOther(n, get))170}171172// evalOther evaluates what formulas rarely hold: array literals, names173// LET and LAMBDA bind, LAMBDA calls, arguments standing in for arrays174// being mapped over, and parts of a formula being stepped through175// (probe.go).176func evalOther(n Node, get lookup) Value {177 switch n := n.(type) {178 case probe:179 return n.eval(get)180 case formula.Array:181 return get.literal(n)182 case formula.Local:183 return get.local(n.Name)184 case formula.Invoke:185 return get.invoke(n)186 case liftArg:187 return n.one(get)188 case constArg:189 return n.v190 }191 return value.ErrValue192}193194// evalUnary computes a prefix operator or the postfix %; with dec, % is195// decimal (decimal.go).196func evalUnary(n formula.Unary, get lookup, dec bool) Value {197 *get.depth++198 x := eval(n.X, get)199 *get.depth--200 if x.Kind == value.Array {201 return get.reduce(get.elementwise(x, Value{}, func(x, _ Value) Value { return unaryOp(n.Op, x, dec) }))202 }203 return unaryOp(n.Op, x, dec)204}205206func unaryOp(op string, x Value, dec bool) Value {207 if x.Kind == value.Error {208 return x209 }210 switch op {211 case "-":212 f, err := toNum(x)213 if err != nil {214 return *err215 }216 return num(-f)217 case "#NOT#":218 f, err := toNum(x)219 if err != nil {220 return *err221 }222 return boolean(f == 0)223 case "%":224 f, err := toNum(x)225 if err != nil {226 return *err227 }228 if dec {229 if v, ok := decArith("/", f, 100); ok {230 return v231 }232 }233 return num(f / 100)234 }235 return x // unary + is identity236}237238// evalBinary computes a binary operator; with dec, arithmetic is decimal239// (decimal.go).240func evalBinary(n formula.Binary, get lookup, dec bool) Value {241 *get.depth++242 l, r := eval(n.L, get), eval(n.R, get)243 *get.depth--244 switch {245 case l.Kind == value.Array || r.Kind == value.Array:246 return get.binaryArrays(n.Op, l, r, dec)247 case l.Kind == value.Error:248 return l249 case r.Kind == value.Error:250 return r251 case !dec && l.Kind == value.Number && r.Kind == value.Number:252 // Arithmetic on numbers, the commonest case, at once.253 switch n.Op {254 case "+":255 return num(l.Num + r.Num)256 case "-":257 return num(l.Num - r.Num)258 case "*":259 return num(l.Num * r.Num)260 }261 }262 return binaryOp(n.Op, l, r, dec)263}264265// binaryArrays applies a binary operator to arrays, value by value.266//267//go:noinline268func (rd *Reader) binaryArrays(op string, l, r Value, dec bool) Value {269 return rd.reduce(rd.elementwise(l, r, func(l, r Value) Value { return binaryOp(op, l, r, dec) }))270}271272func binaryOp(op string, l, r Value, dec bool) Value {273 if l.Kind == value.Error {274 return l275 }276 if r.Kind == value.Error {277 return r278 }279 switch op {280 case "&":281 return Value{Kind: value.Text, Str: text(l) + text(r)}282 case "=", "<>", "<", ">", "<=", ">=":283 return boolean(cmpResult(op, value.CompareShown(l, r)))284 }285 a, err := toNum(l)286 if err != nil {287 return *err288 }289 b, err := toNum(r)290 if err != nil {291 return *err292 }293 if dec {294 if v, ok := decArith(op, a, b); ok {295 return v296 }297 }298 switch op {299 case "+":300 return num(a + b)301 case "-":302 return num(a - b)303 case "*":304 return num(a * b)305 case "/":306 if b == 0 {307 return value.ErrDiv0308 }309 return num(a / b)310 case "^":311 return powerOf(a, b)312 case "#AND#":313 return boolean(a != 0 && b != 0)314 case "#OR#":315 return boolean(a != 0 || b != 0)316 }317 return value.ErrValue318}319320// intersect is the cell a range stands for where one value is wanted321// (Intersect), in the formula being evaluated.322func (rd *Reader) intersect(r Rect) (Addr, bool) {323 return Intersect(r, rd.here)324}325326// Intersect is the cell of r a formula in the cell at reads where it327// wants one value: a single cell is itself; a single column gives the328// cell in at's row, a single row the cell in at's column, when that lies329// within r (implicit intersection, on whatever sheet r is on). Any other330// range has no such cell, which is #VALUE!.331func Intersect(r Rect, at Addr) (Addr, bool) {332 switch {333 case r.From == r.To:334 return r.From, true335 case r.From.Col == r.To.Col && at.Row >= r.From.Row && at.Row <= r.To.Row:336 return Addr{Col: r.From.Col, Row: at.Row}, true337 case r.From.Row == r.To.Row && at.Col >= r.From.Col && at.Col <= r.To.Col:338 return Addr{Col: at.Col, Row: r.From.Row}, true339 }340 return Addr{}, false341}