640 lines
18 KiB
Go
640 lines
18 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
|
// read, so the evaluator touches neither the rng nor the node tree.
|
|
type calcNode interface {
|
|
eval(operands []string) float64
|
|
}
|
|
|
|
type calcNum float64 // a number literal
|
|
type calcVar string // a sibling-field name, before indexVars places it
|
|
type calcIdx int // an operand, by its position in the values expand read
|
|
type calcNeg struct{ x calcNode }
|
|
type calcBin struct { // a + - * / b
|
|
op byte
|
|
l, r calcNode
|
|
}
|
|
|
|
func (n calcNum) eval([]string) float64 { return float64(n) }
|
|
|
|
func (n calcIdx) eval(operands []string) float64 {
|
|
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n]), 64)
|
|
if err != nil {
|
|
return math.NaN() // a non-numeric operand stays visible, never an error
|
|
}
|
|
return v
|
|
}
|
|
|
|
// eval on an unplaced name cannot happen: calcPrep runs indexVars over every
|
|
// expression it compiles, so only a calcIdx reaches a render. It panics rather
|
|
// than returning NaN, so a node kind indexVars forgets is a stack trace and not a
|
|
// silently wrong number.
|
|
func (n calcVar) eval([]string) float64 {
|
|
panic(fmt.Sprintf("fejkdata: calc operand %q was never placed", string(n)))
|
|
}
|
|
|
|
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
|
|
|
func (n calcBin) eval(operands []string) float64 {
|
|
l, r := n.l.eval(operands), n.r.eval(operands)
|
|
switch n.op {
|
|
case '+':
|
|
return l + r
|
|
case '-':
|
|
return l - r
|
|
case '*':
|
|
return l * r
|
|
default: // '/'
|
|
return l / r
|
|
}
|
|
}
|
|
|
|
// checkCalc validates a calc token at compile time: a parseable expression whose
|
|
// operands all name existing fields, and an optional non-negative integer dp. It
|
|
// is a builtin check (fields first), so calc dispatches through the registry like
|
|
// every other {name(args)} function.
|
|
func checkCalc(fields map[string]node, args []string) error {
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
|
|
}
|
|
expr, err := parseCalc(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("calc(%q): %w", args[0], err)
|
|
}
|
|
for _, name := range calcVars(expr) {
|
|
operand, ok := fields[name]
|
|
if !ok {
|
|
return fmt.Errorf("calc(%q): no field %q", args[0], name)
|
|
}
|
|
if text, never := neverNumeric(operand); never {
|
|
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
|
}
|
|
}
|
|
if divisor, zero := constantZeroDivisor(expr, fields); zero {
|
|
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
|
}
|
|
if len(args) == 2 {
|
|
dp, err := plainInt(args[1])
|
|
if err != nil {
|
|
return fmt.Errorf("calc decimals %w", err)
|
|
}
|
|
if dp < 0 || dp > maxDecimals {
|
|
return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
|
// literals and fixed operands folded, anything that varies left unknown.
|
|
func constantZeroDivisor(n calcNode, fields map[string]node) (string, bool) {
|
|
switch n := n.(type) {
|
|
case calcNeg:
|
|
return constantZeroDivisor(n.x, fields)
|
|
case calcBin:
|
|
if n.op == '/' {
|
|
if v, known := constantValue(n.r, fields); known && v == 0 {
|
|
return calcText(n.r), true
|
|
}
|
|
}
|
|
if d, zero := constantZeroDivisor(n.l, fields); zero {
|
|
return d, true
|
|
}
|
|
return constantZeroDivisor(n.r, fields)
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// constantValue evaluates an expression whose every operand is fixed.
|
|
func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
|
switch n := n.(type) {
|
|
case calcNum:
|
|
return float64(n), true
|
|
case calcVar:
|
|
t, ok := fields[string(n)].(*template)
|
|
if !ok || !t.fixed || t.repeat > 1 {
|
|
return 0, false
|
|
}
|
|
v, err := strconv.ParseFloat(strings.TrimSpace(t.lit), 64)
|
|
return v, err == nil
|
|
case calcNeg:
|
|
v, ok := constantValue(n.x, fields)
|
|
return -v, ok
|
|
case calcBin:
|
|
l, lok := constantValue(n.l, fields)
|
|
r, rok := constantValue(n.r, fields)
|
|
if !lok || !rok {
|
|
return 0, false
|
|
}
|
|
return calcBin{n.op, calcNum(l), calcNum(r)}.eval(nil), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// calcText spells an expression node the way an author would read it.
|
|
func calcText(n calcNode) string {
|
|
switch n := n.(type) {
|
|
case calcNum:
|
|
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
|
case calcVar:
|
|
return string(n)
|
|
case calcNeg:
|
|
return "-" + calcText(n.x)
|
|
case calcBin:
|
|
return "(" + calcText(n.l) + " " + string(n.op) + " " + calcText(n.r) + ")"
|
|
}
|
|
return "?"
|
|
}
|
|
|
|
// neverNumeric reports a node no render of which is a number: a null, fixed text that
|
|
// does not parse, or a choice of only such items. text is one such render.
|
|
func neverNumeric(n node) (text string, never bool) {
|
|
switch n := n.(type) {
|
|
case *null:
|
|
return "", true
|
|
case *template:
|
|
if !n.fixed || n.repeat > 1 {
|
|
return "", false
|
|
}
|
|
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
|
|
return n.lit, true
|
|
}
|
|
case *choice:
|
|
for _, it := range n.items {
|
|
t, itemNever := neverNumeric(it)
|
|
if !itemNever {
|
|
return "", false
|
|
}
|
|
text = t
|
|
}
|
|
return text, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// calcPrep parses the expression and decimals once, at compile time, and places each
|
|
// operand name at the position expand will read it into. checkCalc proved both args
|
|
// valid, so no step here can fail; dp -1 prints the minimal form.
|
|
func calcPrep(args []string) callFn {
|
|
expr, err := parseCalc(args[0])
|
|
if err != nil { // a nil AST would be a nil dereference per render, with no message
|
|
panic(fmt.Sprintf("fejkdata: calc(%q) reached prep unparsed: %v", args[0], err))
|
|
}
|
|
at := make(map[string]int)
|
|
for i, name := range calcVars(expr) {
|
|
at[name] = i
|
|
}
|
|
placed := indexVars(expr, at)
|
|
dp := calcDecimals(args)
|
|
return func(_ *session, _ string, operands []string) string {
|
|
return strconv.FormatFloat(placed.eval(operands), 'f', dp, 64)
|
|
}
|
|
}
|
|
|
|
// indexVars replaces each operand name with its position in the values expand reads.
|
|
// Both sides take that order from calcVars, so they cannot drift.
|
|
func indexVars(n calcNode, at map[string]int) calcNode {
|
|
switch n := n.(type) {
|
|
case calcVar:
|
|
i, placed := at[string(n)]
|
|
if !placed { // calcVars named every operand, so a miss means the two disagree
|
|
panic(fmt.Sprintf("fejkdata: calc operand %q is not among the names read for it", string(n)))
|
|
}
|
|
return calcIdx(i)
|
|
case calcNeg:
|
|
return calcNeg{indexVars(n.x, at)}
|
|
case calcBin:
|
|
return calcBin{n.op, indexVars(n.l, at), indexVars(n.r, at)}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// calcOperands lists the sibling-field names a calc's args read. checkCalc reports
|
|
// an expression that does not parse, so one that does not simply names nothing.
|
|
func calcOperands(args []string) []string {
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
expr, err := parseCalc(args[0])
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return calcVars(expr)
|
|
}
|
|
|
|
// calcVars lists the distinct field names an expression reads, in the order it first
|
|
// names each. That order is the contract between expand, which reads the operands
|
|
// into a slice, and indexVars, which places each name at its position in it.
|
|
func calcVars(n calcNode) []string {
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
var walk func(calcNode)
|
|
walk = func(n calcNode) {
|
|
switch n := n.(type) {
|
|
case calcVar:
|
|
if name := string(n); !seen[name] {
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
case calcNeg:
|
|
walk(n.x)
|
|
case calcBin:
|
|
walk(n.l)
|
|
walk(n.r)
|
|
}
|
|
}
|
|
walk(n)
|
|
return out
|
|
}
|
|
|
|
// calcParser is a recursive-descent parser over the expression runes, threading
|
|
// expr -> term -> factor for the standard * / before + - precedence.
|
|
type calcParser struct {
|
|
rs []rune
|
|
pos int
|
|
}
|
|
|
|
// parseCalc parses a whole expression, requiring it to consume all input.
|
|
func parseCalc(expr string) (calcNode, error) {
|
|
p := &calcParser{rs: []rune(expr)}
|
|
if p.space(); p.pos >= len(p.rs) {
|
|
return nil, fmt.Errorf("empty expression")
|
|
}
|
|
n, err := p.expr()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.space(); p.pos != len(p.rs) {
|
|
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (p *calcParser) space() {
|
|
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
|
p.pos++
|
|
}
|
|
}
|
|
|
|
func (p *calcParser) expr() (calcNode, error) { return p.binary(p.term, '+', '-') }
|
|
func (p *calcParser) term() (calcNode, error) { return p.binary(p.factor, '*', '/') }
|
|
|
|
// binary parses a left-associative run of next() operands joined by the given
|
|
// operators, the one shape expr and term share.
|
|
func (p *calcParser) binary(next func() (calcNode, error), ops ...byte) (calcNode, error) {
|
|
n, err := next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for {
|
|
p.space()
|
|
if p.pos >= len(p.rs) || !contains(ops, byte(p.rs[p.pos])) {
|
|
return n, nil
|
|
}
|
|
op := byte(p.rs[p.pos])
|
|
p.pos++
|
|
r, err := next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n = calcBin{op, n, r}
|
|
}
|
|
}
|
|
|
|
// factor is a table-shaped scanner, one case per token kind, kept whole on purpose.
|
|
func (p *calcParser) factor() (calcNode, error) {
|
|
p.space()
|
|
if p.pos >= len(p.rs) {
|
|
return nil, fmt.Errorf("unexpected end of expression")
|
|
}
|
|
switch c := p.rs[p.pos]; {
|
|
case c == '-':
|
|
p.pos++
|
|
x, err := p.factor()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return calcNeg{x}, nil
|
|
case c == '(':
|
|
p.pos++
|
|
n, err := p.expr()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
|
return nil, fmt.Errorf("missing ')'")
|
|
}
|
|
p.pos++
|
|
return n, nil
|
|
case c == '.' || c >= '0' && c <= '9':
|
|
return p.number()
|
|
case c == '_' || unicode.IsLetter(c):
|
|
return p.ident()
|
|
default:
|
|
return nil, fmt.Errorf("unexpected %q", string(c))
|
|
}
|
|
}
|
|
|
|
func (p *calcParser) number() (calcNode, error) {
|
|
start, dot := p.pos, false
|
|
for p.pos < len(p.rs) {
|
|
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
|
p.pos++
|
|
} else if c == '.' && !dot {
|
|
dot, p.pos = true, p.pos+1
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
|
}
|
|
return calcNum(v), nil
|
|
}
|
|
|
|
// ident reads a field name: a letter or '_', then letters, digits or '_'. A '-'
|
|
// is always the minus operator, so a hyphenated field name can't be an operand.
|
|
func (p *calcParser) ident() (calcNode, error) {
|
|
start := p.pos
|
|
for p.pos < len(p.rs) {
|
|
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
|
p.pos++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return calcVar(string(p.rs[start:p.pos])), nil
|
|
}
|
|
|
|
func contains(bs []byte, b byte) bool {
|
|
for _, x := range bs {
|
|
if x == b {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// calcDecimals is a calc's decimals count, or -1 for the shortest form.
|
|
func calcDecimals(args []string) int {
|
|
if len(args) == 2 {
|
|
return atoi(args[1])
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// calcLimit is the largest magnitude a proof accepts as finite, far enough below
|
|
// math.MaxFloat64 that rounding in the bounds cannot hide an overflow.
|
|
const calcLimit = 1e300
|
|
|
|
// maxOperandLen is the longest operand text a proof bounds by its length, so that
|
|
// bound, 10^maxOperandLen, stays within calcLimit.
|
|
const maxOperandLen = 300
|
|
|
|
// calcBound is what a proof knows of every value a calc can take: it lies in [lo, hi],
|
|
// is at least nonZero from zero unless nonZero is 0, and is whole when integral.
|
|
type calcBound struct {
|
|
lo, hi, nonZero float64
|
|
integral bool
|
|
}
|
|
|
|
func magnitude(b calcBound) float64 { return math.Max(math.Abs(b.lo), math.Abs(b.hi)) }
|
|
|
|
// doubt is why a proof could not show a calc finite, and the render that shows it.
|
|
type doubt struct{ render, why string }
|
|
|
|
type bounded struct {
|
|
b calcBound
|
|
d *doubt
|
|
}
|
|
|
|
// calcProof bounds a typed column's calcs from their operands' renders, to show each
|
|
// prints a number rather than NaN or Inf.
|
|
type calcProof struct {
|
|
decimal *textLanguage
|
|
operands map[node]bounded
|
|
lengths map[node]int
|
|
}
|
|
|
|
func newCalcProof() *calcProof {
|
|
p := &calcProof{operands: map[node]bounded{}, lengths: map[node]int{}}
|
|
p.decimal = newTextLanguage(decimalGrammar, p)
|
|
return p
|
|
}
|
|
|
|
// call bounds one calc token of t.
|
|
func (p *calcProof) call(t *template, args []string) (calcBound, *doubt) {
|
|
expr, err := parseCalc(args[0])
|
|
if err != nil {
|
|
panic(fmt.Sprintf("fejkdata: calc(%q) reached a proof unparsed: %v", args[0], err))
|
|
}
|
|
b, d := p.expr(expr, t.fields)
|
|
if d != nil {
|
|
return b, &doubt{d.render, fmt.Sprintf("{calc(%s)}: %s", strings.Join(args, ", "), d.why)}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func (p *calcProof) expr(n calcNode, fields map[string]node) (calcBound, *doubt) {
|
|
switch n := n.(type) {
|
|
case calcNum:
|
|
v := float64(n)
|
|
return calcBound{v, v, v, v == math.Trunc(v)}, nil
|
|
case calcVar:
|
|
return p.operand(string(n), fields[string(n)])
|
|
case calcNeg:
|
|
b, d := p.expr(n.x, fields)
|
|
return calcBound{-b.hi, -b.lo, b.nonZero, b.integral}, d
|
|
case calcBin:
|
|
l, d := p.expr(n.l, fields)
|
|
if d != nil {
|
|
return l, d
|
|
}
|
|
r, d := p.expr(n.r, fields)
|
|
if d != nil {
|
|
return r, d
|
|
}
|
|
return combine(n, l, r)
|
|
}
|
|
panic(fmt.Sprintf("fejkdata: calc node %T has no bound", n))
|
|
}
|
|
|
|
// combine bounds one operation from the bounds of its sides.
|
|
func combine(n calcBin, l, r calcBound) (calcBound, *doubt) {
|
|
b := calcBound{integral: l.integral && r.integral}
|
|
switch n.op {
|
|
case '+':
|
|
b.lo, b.hi = l.lo+r.lo, l.hi+r.hi
|
|
case '-':
|
|
b.lo, b.hi = l.lo-r.hi, l.hi-r.lo
|
|
case '*':
|
|
b.lo = min(l.lo*r.lo, l.lo*r.hi, l.hi*r.lo, l.hi*r.hi)
|
|
b.hi = max(l.lo*r.lo, l.lo*r.hi, l.hi*r.lo, l.hi*r.hi)
|
|
b.nonZero = l.nonZero * r.nonZero
|
|
default:
|
|
if r.nonZero == 0 {
|
|
return b, &doubt{"+Inf", fmt.Sprintf("divides by %s, which can be zero", calcText(n.r))}
|
|
}
|
|
m := magnitude(l) / r.nonZero
|
|
b = calcBound{lo: -m, hi: m, nonZero: l.nonZero / magnitude(r)}
|
|
}
|
|
if b.lo > 0 || b.hi < 0 {
|
|
b.nonZero = math.Max(b.nonZero, math.Min(math.Abs(b.lo), math.Abs(b.hi)))
|
|
}
|
|
if !(magnitude(b) <= calcLimit) {
|
|
return b, &doubt{"+Inf", calcText(n) + " can overflow"}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// operand bounds a calc operand, once per node.
|
|
func (p *calcProof) operand(name string, n node) (calcBound, *doubt) {
|
|
if seen, done := p.operands[n]; done {
|
|
return seen.b, seen.d
|
|
}
|
|
b, d := p.measure(name, n)
|
|
p.operands[n] = bounded{b, d}
|
|
return b, d
|
|
}
|
|
|
|
// measure bounds an operand through the calc it renders when that is all it renders,
|
|
// and otherwise from its text: a plain decimal of at most maxOperandLen bytes.
|
|
func (p *calcProof) measure(name string, n node) (calcBound, *doubt) {
|
|
if t, ok := n.(*template); ok {
|
|
if args, isCalc := soleCalc(t); isCalc {
|
|
b, d := p.call(t, args)
|
|
return rounded(b, calcDecimals(args)), d
|
|
}
|
|
}
|
|
text := p.decimal.node(n, nil)
|
|
if w, escapes := text.escape(decimalAccept); escapes {
|
|
why := fmt.Sprintf("operand %q can render %s, which is not a plain decimal", name, w)
|
|
if w.why != "" {
|
|
why += ": " + w.why
|
|
}
|
|
return calcBound{}, &doubt{"NaN", why}
|
|
}
|
|
size := p.length(n)
|
|
if size > maxOperandLen {
|
|
return calcBound{}, &doubt{"NaN", fmt.Sprintf("operand %q can render more than %d bytes, too many to bound", name, maxOperandLen)}
|
|
}
|
|
ends, m := text.to[1], math.Pow(10, float64(size))
|
|
b := calcBound{hi: m, nonZero: 1 / m, integral: ends&decimalFractional == 0}
|
|
if ends&decimalNegative != 0 {
|
|
b.lo = -m
|
|
}
|
|
if ends&decimalZero != 0 {
|
|
b.nonZero = 0
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// soleCalc reports a template that renders one calc and nothing else, with its args.
|
|
func soleCalc(t *template) ([]string, bool) {
|
|
if t.repeat != 1 || len(t.ops) != 1 || t.ops[0].kind != 'b' {
|
|
return nil, false
|
|
}
|
|
name, args, _ := funcCall(t.format[1 : len(t.format)-1])
|
|
return args, name == "calc"
|
|
}
|
|
|
|
// rounded is b once printed to dp decimals, which moves a value by up to half a unit.
|
|
func rounded(b calcBound, dp int) calcBound {
|
|
if dp < 0 {
|
|
return b
|
|
}
|
|
half := math.Pow(10, -float64(dp)) / 2
|
|
return calcBound{b.lo - half, b.hi + half, math.Max(0, b.nonZero-half), b.integral || dp == 0}
|
|
}
|
|
|
|
// length is the most bytes a render of n can take, anything past maxOperandLen
|
|
// reported as maxOperandLen+1.
|
|
func (p *calcProof) length(n node) int {
|
|
if size, done := p.lengths[n]; done {
|
|
return size
|
|
}
|
|
size := 0
|
|
switch n := n.(type) {
|
|
case *choice:
|
|
for _, it := range n.items {
|
|
size = max(size, p.length(it))
|
|
}
|
|
case *template:
|
|
size = p.formatLength(n)*n.repeat + len(n.separator)*(n.repeat-1)
|
|
}
|
|
size = min(size, maxOperandLen+1)
|
|
p.lengths[n] = size
|
|
return size
|
|
}
|
|
|
|
func (p *calcProof) formatLength(t *template) int {
|
|
size := 0
|
|
_ = eachToken(t.format, func(tok ftoken) error {
|
|
if tok.kind == 'l' {
|
|
size += utf8.RuneLen(tok.r)
|
|
} else {
|
|
size += p.tokenLength(t, tok.body)
|
|
}
|
|
size = min(size, maxOperandLen+1)
|
|
return nil
|
|
})
|
|
return size
|
|
}
|
|
|
|
// tokenLength is the most bytes one token can print. A transform never lengthens a
|
|
// render that reads as a decimal: it maps each non-ASCII rune, two bytes or more, to at
|
|
// most two ASCII letters.
|
|
func (p *calcProof) tokenLength(t *template, body string) int {
|
|
name, args, isFunc := funcCall(body)
|
|
var arms []arm
|
|
switch _, isTransform := transforms[name]; {
|
|
case !isFunc:
|
|
arms = splitArms(body, t.refs)
|
|
case isTransform:
|
|
leaf, _, _ := unwrapTransform(args[0])
|
|
arms = []arm{splitArm(leaf, t.refs)}
|
|
case name == "calc":
|
|
b, d := p.call(t, args)
|
|
if d != nil {
|
|
return len(d.render)
|
|
}
|
|
return shapeLength(printedFloat(b.lo, b.hi, calcDecimals(args), b.integral))
|
|
default:
|
|
return shapeLength(builtins[name].emits(args))
|
|
}
|
|
size := 0
|
|
for _, a := range arms {
|
|
for _, leaf := range pathLeaves(t.fields[a.key], a.tail) {
|
|
size = max(size, p.length(leaf))
|
|
}
|
|
}
|
|
return size
|
|
}
|
|
|
|
// shapeLength is the most bytes a shape can emit, anything past maxOperandLen reported
|
|
// as maxOperandLen+1.
|
|
func shapeLength(s textShape) int {
|
|
longest := 0
|
|
for _, alt := range s {
|
|
size := 0
|
|
for _, run := range alt {
|
|
if run.max < 0 {
|
|
return maxOperandLen + 1
|
|
}
|
|
size += run.max
|
|
}
|
|
longest = max(longest, size)
|
|
}
|
|
return min(longest, maxOperandLen+1)
|
|
}
|