// Package proven holds what a proof knows of every render of a value, and the arithmetic // that bounds a calc from its operands. package proven import ( "fmt" "math" "regexp" "strconv" "strings" "github.com/larvit/fejkdata/internal/datatype" "github.com/larvit/fejkdata/internal/grammar" ) // Value is what a proof knows of every render of a node: bounds on the number each // reads as, and per datatype why some render's text is not one ("" when none). type Value struct { Lo, Hi float64 NonZero float64 // every value is at least this far from zero; 0 when one can be zero Integral bool NotOperand string // why some render reads as no finite number, the way calc reads it Not [datatype.Count]string Nullable bool // some draw of a column is null } // ShortestDecimals prints the fewest digits that read back as the same float64. const ShortestDecimals = -1 // limit is the largest magnitude a proof accepts as finite, far enough below // math.MaxFloat64 that rounding in the bounds cannot hide an overflow. const limit = 1e300 // Or is what a proof knows of a render that is either v or w. func (v Value) Or(w Value) Value { v.Lo, v.Hi, v.NonZero = min(v.Lo, w.Lo), max(v.Hi, w.Hi), min(v.NonZero, w.NonZero) v.Integral, v.Nullable = v.Integral && w.Integral, v.Nullable || w.Nullable if v.NotOperand == "" { v.NotOperand = w.NotOperand } for d := range v.Not { if v.Not[d] == "" { v.Not[d] = w.Not[d] } } return v } // Calc bounds a calc expression from its operands, or says why it cannot. func Calc(expr grammar.CalcNode, operand func(name string) Value) (Value, string) { v, doubt := bound(expr, operand) if doubt == "" && !(magnitude(v) <= limit) { doubt = grammar.CalcText(expr) + " is not proven within 1e300" } return v, doubt } func bound(n grammar.CalcNode, operand func(name string) Value) (Value, string) { switch n := n.(type) { case grammar.CalcNum: v := float64(n) return Bounded(v, v, v == math.Trunc(v)), "" case grammar.CalcVar: v := operand(n.Name) if v.NotOperand != "" { return Value{}, fmt.Sprintf("operand %q: %s", n.Name, v.NotOperand) } return Value{Lo: v.Lo, Hi: v.Hi, NonZero: v.NonZero, Integral: v.Integral}, "" case grammar.CalcNeg: v, doubt := bound(n.X, operand) v.Lo, v.Hi = -v.Hi, -v.Lo return v, doubt case grammar.CalcBin: l, doubt := bound(n.L, operand) if doubt != "" { return l, doubt } r, doubt := bound(n.R, operand) if doubt != "" { return r, doubt } return combine(n, l, r) } panic(fmt.Sprintf("fejkdata: internal error: calc node %T has no bound", n)) } // combine bounds one operation from the bounds of its sides. func combine(n grammar.CalcBin, l, r Value) (Value, string) { var v Value integral := l.Integral && r.Integral switch n.Operator { case '+': v = Bounded(l.Lo+r.Lo, l.Hi+r.Hi, integral) case '-': v = Bounded(l.Lo-r.Hi, l.Hi-r.Lo, integral) case '*': v = Bounded(min(l.Lo*r.Lo, l.Lo*r.Hi, l.Hi*r.Lo, l.Hi*r.Hi), max(l.Lo*r.Lo, l.Lo*r.Hi, l.Hi*r.Lo, l.Hi*r.Hi), integral) v.NonZero = max(v.NonZero, l.NonZero*r.NonZero) default: if r.NonZero == 0 { return v, fmt.Sprintf("divides by %s, which is not proven nonzero", grammar.CalcText(n.R)) } m := magnitude(l) / r.NonZero v = Value{Lo: -m, Hi: m, NonZero: l.NonZero / magnitude(r)} } if !(magnitude(v) <= limit) { return v, grammar.CalcText(n) + " is not proven within 1e300" } return v, "" } // Bounded is a number in [lo, hi], its distance from zero read off the bounds. func Bounded(lo, hi float64, integral bool) Value { v := Value{Lo: lo, Hi: hi, Integral: integral} switch { case lo > 0: v.NonZero = lo case hi < 0: v.NonZero = -hi } return v } func magnitude(v Value) float64 { return math.Max(math.Abs(v.Lo), math.Abs(v.Hi)) } // PrintedNumber is what a token printing v to dp decimals holds. func PrintedNumber(token string, v Value, dp int) Value { if dp == ShortestDecimals { if v.Integral { // a whole value prints with no point return printedInteger(token, v) } return Printing(token, datatype.Number, v) } half, _ := strconv.ParseFloat("5e-"+strconv.Itoa(dp+1), 64) v = Value{Lo: v.Lo - half, Hi: v.Hi + half, NonZero: math.Max(0, v.NonZero-half), Integral: v.Integral || dp == 0} if dp == 0 { return printedInteger(token, v) } return Printing(token, datatype.Number, v) } func printedInteger(token string, v Value) Value { v = Printing(token, datatype.Integer, v) if !(magnitude(v) < math.MaxInt64) { v.Not[datatype.Integer] = fmt.Sprintf("{%s} is not proven within int64", token) } return v } // Printing is v for a token whose every render is text of datatype prints, with a reason // against each datatype that text is not. func Printing(token string, prints datatype.DataType, v Value) Value { for d := datatype.Integer; d <= datatype.Boolean; d++ { if prints != d && !(prints == datatype.Integer && d == datatype.Number) { v.Not[d] = fmt.Sprintf("{%s} prints %s, not %s", token, datatype.Noun(prints), datatype.Noun(d)) } } return v } // Unproven is a render no datatype and no calc can take, for why. func Unproven(why string) Value { v := Value{NotOperand: why} for d := datatype.Integer; d <= datatype.Boolean; d++ { v.Not[d] = why } return v } var ( integerText = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`) numberText = regexp.MustCompile(`^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$`) ) // Literal proves fixed text: the number calc reads it as, and each datatype it is. func Literal(text string) Value { var v Value if f, err := strconv.ParseFloat(strings.TrimSpace(text), 64); err != nil || math.IsNaN(f) || math.IsInf(f, 0) { v.NotOperand = fmt.Sprintf("%q is not a number", text) } else { v = Bounded(f, f, f == math.Trunc(f)) } if _, err := strconv.ParseInt(text, 10, 64); !integerText.MatchString(text) { v.Not[datatype.Integer] = fmt.Sprintf("%q is not an integer", text) } else if err != nil { v.Not[datatype.Integer] = fmt.Sprintf("%q is past the int64 range", text) } if v.NotOperand != "" || !numberText.MatchString(text) { v.Not[datatype.Number] = fmt.Sprintf("%q is not a number", text) } if text != "true" && text != "false" { v.Not[datatype.Boolean] = fmt.Sprintf("%q is not a boolean", text) } return signedZero(text, v) } // signedZero refuses a zero written with a sign as a typed value, naming it unsigned. func signedZero(text string, v Value) Value { mantissa, _, _ := strings.Cut(strings.ToLower(text), "e") if !strings.HasPrefix(text, "-") || v.NotOperand != "" || strings.Trim(mantissa, "-0.") != "" { return v } for _, d := range []datatype.DataType{datatype.Integer, datatype.Number} { if v.Not[d] == "" { v.Not[d] = fmt.Sprintf("%q is zero written with a sign; write %q", text, text[1:]) } } return v }