Answer architecture round 1: one invariant phrase in internal/invariant, held across every package, Source built by Dir and FS, coverage counted across packages, and file items 112 and 113
This commit is contained in:
@@ -31,7 +31,7 @@ type builtin struct {
|
||||
// operands names the fields the call reads, which a render reads for it; nil
|
||||
// for a builtin that reads none.
|
||||
operands func(args []string) []string
|
||||
// noRefOperands says no operand can be a reference, so a name read once there is not refused.
|
||||
// noRefOperands says no operand can be a reference.
|
||||
noRefOperands bool
|
||||
// proveNumber bounds the number a call's text reads as, token its body, and says which
|
||||
// datatypes that text is not; set it where every render reads as a finite number, which
|
||||
@@ -97,9 +97,13 @@ func ProveNumber(name, token string, args []string) (proven.Value, bool) {
|
||||
return b.proveNumber(token, b.prints, args), true
|
||||
}
|
||||
|
||||
// NumberCalls lists the calls printing a number, or with text also those whose text is
|
||||
// one, as an error names them.
|
||||
func NumberCalls(text bool) string {
|
||||
// TypedCalls lists the calls whose text a typed column may hold, as an error names them.
|
||||
func TypedCalls() string { return numberCalls(false) }
|
||||
|
||||
// OperandCalls lists the calls whose text a calc reads as a number, as an error names them.
|
||||
func OperandCalls() string { return numberCalls(true) }
|
||||
|
||||
func numberCalls(text bool) string {
|
||||
var calls []string
|
||||
for name, b := range builtins {
|
||||
if b.proveNumber != nil && (text || b.prints != datatype.String) {
|
||||
@@ -116,7 +120,3 @@ func plural(n int) string {
|
||||
}
|
||||
return "s"
|
||||
}
|
||||
|
||||
func internalError(format string, a ...any) string {
|
||||
return "fejkdata: internal error: " + fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/larvit/fejkdata/internal/datatype"
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
"github.com/larvit/fejkdata/internal/proven"
|
||||
)
|
||||
|
||||
@@ -104,7 +105,7 @@ func chars(alphabet string) func([]string) Call {
|
||||
|
||||
const hexDigits = "0123456789abcdef"
|
||||
|
||||
// formatFloat prints v to dp decimals, or shortestDecimals, and a zero unsigned.
|
||||
// formatFloat prints v to dp decimals, or proven.ShortestDecimals, and a zero unsigned.
|
||||
func formatFloat(v float64, dp int) string {
|
||||
s := strconv.FormatFloat(v, 'f', dp, 64)
|
||||
if strings.HasPrefix(s, "-") && strings.Trim(s, "-0.") == "" {
|
||||
@@ -119,7 +120,7 @@ func formatFloat(v float64, dp int) string {
|
||||
func atoi(s string) int {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(internalError("builtin arg %q reached prep unvalidated: %v", s, err))
|
||||
panic(invariant.Broken("builtin arg %q reached prep unvalidated: %v", s, err))
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -128,7 +129,7 @@ func atoi(s string) int {
|
||||
func atof(s string) float64 {
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
panic(internalError("builtin arg %q reached prep unvalidated: %v", s, err))
|
||||
panic(invariant.Broken("builtin arg %q reached prep unvalidated: %v", s, err))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
"github.com/larvit/fejkdata/internal/proven"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,7 @@ func evalCalc(n grammar.CalcNode, operands []string) float64 {
|
||||
case grammar.CalcBin:
|
||||
return Arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands))
|
||||
}
|
||||
panic(internalError("calc node %#v has no evaluation", n))
|
||||
panic(invariant.Broken("calc node %#v has no evaluation", n))
|
||||
}
|
||||
|
||||
// Arith applies a calc operator.
|
||||
@@ -80,7 +81,7 @@ func calcPrep(args []string) Call {
|
||||
func ParsedCalc(expr string) grammar.Calc {
|
||||
c, err := grammar.ParseCalc(expr)
|
||||
if err != nil {
|
||||
panic(internalError("calc(%q) passed its check unparsed: %v", expr, err))
|
||||
panic(invariant.Broken("calc(%q) passed its check unparsed: %v", expr, err))
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
)
|
||||
|
||||
const dayLayout = "2006-01-02"
|
||||
@@ -56,7 +57,7 @@ func layoutArg(a string) (string, error) {
|
||||
func layoutOf(a string) string {
|
||||
layout, err := layoutArg(a)
|
||||
if err != nil {
|
||||
panic(internalError("builtin arg %q reached prep unvalidated: %v", a, err))
|
||||
panic(invariant.Broken("builtin arg %q reached prep unvalidated: %v", a, err))
|
||||
}
|
||||
return layout
|
||||
}
|
||||
@@ -130,11 +131,11 @@ func timeArg(a []string) error {
|
||||
func datePrep(a []string) Call {
|
||||
from, err := time.Parse(dayLayout, a[0])
|
||||
if err != nil {
|
||||
panic(internalError("builtin arg %q reached prep unvalidated: %v", a[0], err))
|
||||
panic(invariant.Broken("builtin arg %q reached prep unvalidated: %v", a[0], err))
|
||||
}
|
||||
to, err := time.Parse(dayLayout, a[1])
|
||||
if err != nil {
|
||||
panic(internalError("builtin arg %q reached prep unvalidated: %v", a[1], err))
|
||||
panic(invariant.Broken("builtin arg %q reached prep unvalidated: %v", a[1], err))
|
||||
}
|
||||
layout, start, span := layoutOf(a[2]), from.Unix(), int(to.Unix()-from.Unix())+86400
|
||||
return func(s *drawstate.State, _ string, _ []string) string {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
)
|
||||
|
||||
// transforms are the builtins that rewrite one operand's value; they nest, so
|
||||
@@ -20,7 +21,7 @@ var transforms = map[string]func(string) string{
|
||||
func withTransforms(calls map[string]builtin) map[string]builtin {
|
||||
for name, fn := range transforms {
|
||||
if _, clash := calls[name]; clash {
|
||||
panic(internalError("%s is registered as a builtin and a transform", name))
|
||||
panic(invariant.Broken("%s is registered as a builtin and a transform", name))
|
||||
}
|
||||
calls[name] = builtin{arity: 1, checkArgs: transformArg, prep: transformPrep(fn), operands: transformOperand}
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func unwrapTransform(arg string) (leaf string, chain []func(string) string, err
|
||||
}
|
||||
}
|
||||
|
||||
// transformArg proves a transform's chain; checkReads proves the operand it reads.
|
||||
// transformArg proves a transform's chain.
|
||||
func transformArg(a []string) error {
|
||||
_, _, err := unwrapTransform(a[0])
|
||||
return err
|
||||
@@ -66,7 +67,7 @@ func transformPrep(outer func(string) string) func([]string) Call {
|
||||
return func(a []string) Call {
|
||||
_, chain, err := unwrapTransform(a[0])
|
||||
if err != nil {
|
||||
panic(internalError("transform arg %q reached prep unvalidated: %v", a[0], err))
|
||||
panic(invariant.Broken("transform arg %q reached prep unvalidated: %v", a[0], err))
|
||||
}
|
||||
return func(_ *drawstate.State, _ string, operands []string) string {
|
||||
v := operands[0]
|
||||
|
||||
@@ -12,15 +12,23 @@ import (
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
)
|
||||
|
||||
// Source is one tree to load: an fs.FS and the directory in it to start from. Label
|
||||
// prefixes file names in errors; OnDisk marks Label as a directory that must exist.
|
||||
// Source is one tree to load: an fs.FS and the directory in it to start from. label
|
||||
// prefixes file names in errors; onDisk marks label as a directory that must exist.
|
||||
type Source struct {
|
||||
FS fs.FS
|
||||
Label string
|
||||
OnDisk bool
|
||||
BaseDir string
|
||||
fsys fs.FS
|
||||
label string
|
||||
onDisk bool
|
||||
base string
|
||||
}
|
||||
|
||||
// Dir is the tree in the directory path, its errors labelled with it.
|
||||
func Dir(path string) Source {
|
||||
return Source{fsys: os.DirFS(path), label: path, onDisk: true}
|
||||
}
|
||||
|
||||
// FS is the tree in fsys below base, "" for its root.
|
||||
func FS(fsys fs.FS, base string) Source { return Source{fsys: fsys, base: base} }
|
||||
|
||||
// Category is one category file: the folders above it from the source's base, its name,
|
||||
// and its parsed JSON.
|
||||
type Category struct {
|
||||
@@ -30,37 +38,39 @@ type Category struct {
|
||||
rows *rowsFiles
|
||||
}
|
||||
|
||||
// ReadRows reads a rows file beside the category, marking it named.
|
||||
// ReadRows reads a rows file beside the category. Reading a file is what names it, so no
|
||||
// file is named and left unread.
|
||||
func (c Category) ReadRows(name string) (string, error) { return c.rows.read(name) }
|
||||
|
||||
func (s Source) labelled(p string) string {
|
||||
if s.Label == "" {
|
||||
if s.label == "" {
|
||||
return p
|
||||
}
|
||||
return path.Join(s.Label, p)
|
||||
return path.Join(s.label, p)
|
||||
}
|
||||
|
||||
func (s Source) base() string {
|
||||
if s.BaseDir == "" {
|
||||
return "."
|
||||
// dirPath is the folder dir names, as fsys spells it.
|
||||
func (s Source) dirPath(dir []string) string {
|
||||
if p := path.Join(append([]string{s.base}, dir...)...); p != "" {
|
||||
return p
|
||||
}
|
||||
return s.BaseDir
|
||||
return "."
|
||||
}
|
||||
|
||||
// Walk hands compile every category of the tree, folders and files in name order. A
|
||||
// folder holding no category anywhere below it is skipped; a hidden file or folder is
|
||||
// never data.
|
||||
func (s Source) Walk(compile func(Category) error) error {
|
||||
if s.OnDisk {
|
||||
if s.Label == "" {
|
||||
if s.onDisk {
|
||||
if s.label == "" {
|
||||
return fmt.Errorf("a data path is empty")
|
||||
}
|
||||
info, err := os.Stat(s.Label)
|
||||
info, err := os.Stat(s.label)
|
||||
if err != nil {
|
||||
return fmt.Errorf("data path %s: %w", s.Label, err)
|
||||
return fmt.Errorf("data path %s: %w", s.label, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("%s is not a directory", s.Label)
|
||||
return fmt.Errorf("%s is not a directory", s.label)
|
||||
}
|
||||
}
|
||||
_, err := s.walkDir(nil, compile)
|
||||
@@ -69,8 +79,8 @@ func (s Source) Walk(compile func(Category) error) error {
|
||||
|
||||
// walkDir walks the folder dir names, and reports whether it handed over any category.
|
||||
func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error) {
|
||||
full := path.Join(append([]string{s.base()}, dir...)...)
|
||||
entries, err := fs.ReadDir(s.FS, full)
|
||||
full := s.dirPath(dir)
|
||||
entries, err := fs.ReadDir(s.fsys, full)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w", s.labelled(full), err)
|
||||
}
|
||||
@@ -106,15 +116,15 @@ func (s Source) walkFolder(dir []string, compile func(Category) error) (bool, er
|
||||
return false, err
|
||||
}
|
||||
if err := grammar.CheckName(dir[len(dir)-1]); err != nil {
|
||||
return false, fmt.Errorf("%s: folder %w", s.labelled(path.Join(append([]string{s.base()}, dir...)...)), err)
|
||||
return false, fmt.Errorf("%s: folder %w", s.labelled(s.dirPath(dir)), err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Load hands compile the one category file name in the folder dir.
|
||||
func (s Source) Load(dir []string, name string, compile func(Category) error) error {
|
||||
full := path.Join(append([]string{s.base()}, dir...)...)
|
||||
entries, err := fs.ReadDir(s.FS, full)
|
||||
full := s.dirPath(dir)
|
||||
entries, err := fs.ReadDir(s.fsys, full)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", s.labelled(full), err)
|
||||
}
|
||||
@@ -133,7 +143,7 @@ func (s Source) compileFile(dir []string, file string, rows *rowsFiles, compile
|
||||
if err := grammar.CheckName(name); err != nil {
|
||||
return false, fmt.Errorf("%s: category %w", s.labelled(full), err)
|
||||
}
|
||||
b, err := fs.ReadFile(s.FS, full)
|
||||
b, err := fs.ReadFile(s.fsys, full)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w", s.labelled(full), err)
|
||||
}
|
||||
@@ -170,7 +180,7 @@ func (r *rowsFiles) read(name string) (string, error) {
|
||||
return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, r.src.labelled(r.dir))
|
||||
}
|
||||
r.tsv[name] = true
|
||||
b, err := fs.ReadFile(r.src.FS, path.Join(r.dir, name))
|
||||
b, err := fs.ReadFile(r.src.fsys, path.Join(r.dir, name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", r.src.labelled(path.Join(r.dir, name)), err)
|
||||
}
|
||||
|
||||
@@ -15,16 +15,16 @@ const (
|
||||
)
|
||||
|
||||
// Count is the number of datatypes, for an array indexed by one.
|
||||
const Count = len(names)
|
||||
const Count = Boolean + 1
|
||||
|
||||
var (
|
||||
names = [...]string{"string", "integer", "number", "boolean"}
|
||||
nouns = [...]string{"text", "an integer", "a number", "a boolean"}
|
||||
names = [Count]string{"string", "integer", "number", "boolean"}
|
||||
nouns = [Count]string{"text", "an integer", "a number", "a boolean"}
|
||||
)
|
||||
|
||||
// String is the datatype as data spells it.
|
||||
func (d DataType) String() string {
|
||||
if d < 0 || int(d) >= len(names) {
|
||||
if d < 0 || d >= Count {
|
||||
return fmt.Sprintf("DataType(%d)", int(d))
|
||||
}
|
||||
return names[d]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Package invariant spells what every package panics with when an invariant breaks.
|
||||
package invariant
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Broken is the panic value for a broken invariant, one phrase every break greps to.
|
||||
func Broken(format string, a ...any) string {
|
||||
return "fejkdata: internal error: " + fmt.Sprintf(format, a...)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/larvit/fejkdata/internal/datatype"
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
)
|
||||
|
||||
// Value is what a proof knows of every render of a node: bounds on the number each
|
||||
@@ -81,7 +82,7 @@ func bound(n grammar.CalcNode, operand func(name string) Value) (Value, string)
|
||||
}
|
||||
return combine(n, l, r)
|
||||
}
|
||||
panic(fmt.Sprintf("fejkdata: internal error: calc node %T has no bound", n))
|
||||
panic(invariant.Broken("calc node %T has no bound", n))
|
||||
}
|
||||
|
||||
// combine bounds one operation from the bounds of its sides.
|
||||
@@ -150,7 +151,7 @@ func printedInteger(token string, v Value) Value {
|
||||
// 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++ {
|
||||
for d := datatype.Integer; d < datatype.Count; 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))
|
||||
}
|
||||
@@ -161,7 +162,7 @@ func Printing(token string, prints datatype.DataType, v Value) Value {
|
||||
// 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++ {
|
||||
for d := datatype.Integer; d < datatype.Count; d++ {
|
||||
v.Not[d] = why
|
||||
}
|
||||
return v
|
||||
|
||||
Reference in New Issue
Block a user