Typed columns and null: a datatype option, null items, and a load check that every typed render parses
Tests / vet + fmt + tests (pull_request) Successful in 58s
Tests / vet + fmt + tests (pull_request) Successful in 58s
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
||||
@@ -154,10 +155,12 @@ func calcText(n calcNode) string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
// neverNumeric reports a node no render of which is a number: fixed text that does
|
||||
// not parse, or a choice of only such items. text is one such render.
|
||||
// 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
|
||||
@@ -191,10 +194,7 @@ func calcPrep(args []string) callFn {
|
||||
at[name] = i
|
||||
}
|
||||
placed := indexVars(expr, at)
|
||||
dp := -1
|
||||
if len(args) == 2 {
|
||||
dp = atoi(args[1])
|
||||
}
|
||||
dp := calcDecimals(args)
|
||||
return func(_ *session, _ string, operands []string) string {
|
||||
return strconv.FormatFloat(placed.eval(operands), 'f', dp, 64)
|
||||
}
|
||||
@@ -384,3 +384,256 @@ func contains(bs []byte, b byte) bool {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user