diff --git a/README.md b/README.md index 59a81f5..dbcf2da 100644 --- a/README.md +++ b/README.md @@ -978,6 +978,7 @@ internal/datafiles/ the walk of a data tree: its folders, its category files and internal/datatype/ what a record column holds, which DataType aliases internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax, and whether an argument is a template or a path +internal/invariant/ the one phrase every package panics with when an invariant breaks internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md) diff --git a/calc.go b/calc.go index b6ab37c..1cf8ff5 100644 --- a/calc.go +++ b/calc.go @@ -8,6 +8,7 @@ import ( "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" ) // checkCalcFields refuses a calc operand a field holds that is never a number, and a division @@ -40,7 +41,7 @@ func operandNodes(o op) func(name string) []node { return func(name string) []node { a := o.operands[slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })] if len(a.leaves) == 0 { - panic(internalError("calc operand %q was read before it was compiled", name)) + panic(invariant.Broken("calc operand %q was read before it was compiled", name)) } return a.leaves } diff --git a/compose.yaml b/compose.yaml index 06e9c91..7540813 100644 --- a/compose.yaml +++ b/compose.yaml @@ -17,7 +17,7 @@ services: cover: <<: *go - command: go test -cover ./... + command: go test -cover -coverpkg=./... ./... bench: <<: *go diff --git a/datatype.go b/datatype.go index 5c2a123..ed6a782 100644 --- a/datatype.go +++ b/datatype.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/larvit/fejkdata/internal/datatype" + "github.com/larvit/fejkdata/internal/invariant" ) // DataType is what a record column holds, which decides how a record writes its value. @@ -41,7 +42,7 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) { if name == DataTypeString.String() { return 0, fmt.Errorf("datatype %q is the default, so it has no effect; drop it", name) } - for d := DataTypeInteger; d <= DataTypeBoolean; d++ { + for d := DataTypeInteger; d < datatype.Count; d++ { if name != d.String() { continue } @@ -129,7 +130,7 @@ func columnItems(n node) (items []*template, nullable bool) { nullable = true case *tableColumn: default: - panic(internalError("columnItems has no case for node %T", n)) + panic(invariant.Broken("columnItems has no case for node %T", n)) } } collect(n) @@ -175,7 +176,7 @@ func disagreement(a *template, da DataType, b *template, db DataType) error { case kindText: switch kindOf(other) { case kindText: - panic(internalError("two text items hold one datatype, so they never disagree")) + panic(invariant.Broken("two text items hold one datatype, so they never disagree")) case kindReads: if !fits { return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, %s`, fix.format, datatype.Noun(want), other.format, asText(other)) @@ -186,7 +187,7 @@ func disagreement(a *template, da DataType, b *template, db DataType) error { } return fmt.Errorf(`item %q declares no datatype beside one holding %s; a column holds one, so give it "datatype": %q`, fix.format, want, want) } - panic(internalError("disagreement has no case for item kind %d", kindOf(fix))) + panic(invariant.Broken("disagreement has no case for item kind %d", kindOf(fix))) } // typedAs names the spelling giving a column-read item datatype d, keeping the other keys an object diff --git a/docs/decisions.md b/docs/decisions.md index 05b2ae3..09ffaba 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -837,8 +837,7 @@ because several passes each write part of the same structs; the template languag recursion reads few of those types. Moving the engine before items 101 and 104 to 106 give each value one writer would spread that knot across packages, behind forwarding calls from every public method. The draw state, the grammar, `DataType`, what a proof knows of a -value, the builtins and the reading of data files read no engine type, so they sit in -`internal/drawstate`, `internal/grammar`, `internal/datatype`, `internal/proven`, -`internal/builtinfunc` and `internal/datafiles`, behind an import list a test checks. A +value, the builtins and the reading of data files read no engine type, so each sits in a +package under `internal/`, behind the import list `imports_test.go` checks. A table's rows stop reading any engine type once items 101 and 102 remove those reads; item 102 then moves them into `internal/rows` and adds it to that list. Valid until `todo.md` item 107 moves the engine. diff --git a/fejkdata.go b/fejkdata.go index c6c9d95..0d2b830 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -6,13 +6,13 @@ import ( "errors" "fmt" "io/fs" - "os" "reflect" "sort" "sync" "github.com/larvit/fejkdata/internal/datafiles" "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/invariant" ) // MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along @@ -57,13 +57,13 @@ func WithSeed(seed uint64) Option { // layer several; the last wins a name clash. func WithDataPath(dir string) Option { return func(c *config) { - c.sources = append(c.sources, datafiles.Source{FS: os.DirFS(dir), Label: dir, OnDisk: true}) + c.sources = append(c.sources, datafiles.Dir(dir)) } } // WithDataFS layers a data tree held in an [fs.FS], such as an embed.FS of your own. func WithDataFS(fsys fs.FS) Option { - return func(c *config) { c.sources = append(c.sources, datafiles.Source{FS: fsys}) } + return func(c *config) { c.sources = append(c.sources, datafiles.FS(fsys, "")) } } // WithoutShippedData leaves the shipped data set out, so only the sources given @@ -161,7 +161,7 @@ func paths(n node) []string { } return out default: - panic(internalError("paths has no case for node %T", n)) + panic(invariant.Broken("paths has no case for node %T", n)) } } @@ -232,7 +232,3 @@ func (c config) drawSeed() (uint64, error) { } return binary.LittleEndian.Uint64(b[:]), nil } - -func internalError(format string, a ...any) string { - return "fejkdata: internal error: " + fmt.Sprintf(format, a...) -} diff --git a/graph.go b/graph.go index ebf47d7..5427927 100644 --- a/graph.go +++ b/graph.go @@ -4,6 +4,8 @@ import ( "fmt" "slices" "sort" + + "github.com/larvit/fejkdata/internal/invariant" ) // eachNode visits n and every node contained within it once, passing the dot path @@ -65,7 +67,7 @@ func contained(n node) []namedNode { case *nullItem, *tableRow: return nil default: - panic(internalError("contained has no case for node %T", n)) + panic(invariant.Broken("contained has no case for node %T", n)) } } @@ -127,7 +129,7 @@ func renderEdges(n node) []renderEdge { case *folder, *nullItem: return nil default: - panic(internalError("renderEdges has no case for node %T", n)) + panic(invariant.Broken("renderEdges has no case for node %T", n)) } } diff --git a/internal/builtinfunc/builtinfunc.go b/internal/builtinfunc/builtinfunc.go index c513d32..ed0c716 100644 --- a/internal/builtinfunc/builtinfunc.go +++ b/internal/builtinfunc/builtinfunc.go @@ -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...) -} diff --git a/internal/builtinfunc/builtins.go b/internal/builtinfunc/builtins.go index 5146c76..83f3443 100644 --- a/internal/builtinfunc/builtins.go +++ b/internal/builtinfunc/builtins.go @@ -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 } diff --git a/internal/builtinfunc/calc.go b/internal/builtinfunc/calc.go index b4ea1b0..c5e2e83 100644 --- a/internal/builtinfunc/calc.go +++ b/internal/builtinfunc/calc.go @@ -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 } diff --git a/internal/builtinfunc/layout.go b/internal/builtinfunc/layout.go index 78c7cf5..317d939 100644 --- a/internal/builtinfunc/layout.go +++ b/internal/builtinfunc/layout.go @@ -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 { diff --git a/internal/builtinfunc/transform.go b/internal/builtinfunc/transform.go index cad92d6..5affb4b 100644 --- a/internal/builtinfunc/transform.go +++ b/internal/builtinfunc/transform.go @@ -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] diff --git a/internal/datafiles/datafiles.go b/internal/datafiles/datafiles.go index e1070cb..139a7d0 100644 --- a/internal/datafiles/datafiles.go +++ b/internal/datafiles/datafiles.go @@ -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) } diff --git a/internal/datatype/datatype.go b/internal/datatype/datatype.go index c646aa2..aa90ef5 100644 --- a/internal/datatype/datatype.go +++ b/internal/datatype/datatype.go @@ -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] diff --git a/internal/invariant/invariant.go b/internal/invariant/invariant.go new file mode 100644 index 0000000..86ebc03 --- /dev/null +++ b/internal/invariant/invariant.go @@ -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...) +} diff --git a/internal/proven/proven.go b/internal/proven/proven.go index 9ed15ff..d5fc48e 100644 --- a/internal/proven/proven.go +++ b/internal/proven/proven.go @@ -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 diff --git a/path.go b/path.go index 805d251..5e81b36 100644 --- a/path.go +++ b/path.go @@ -6,6 +6,7 @@ import ( "github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" ) // stepInto is what seg names under n: a folder's entry or a template's field. @@ -36,7 +37,7 @@ func stepInto(n node, seg string) (node, error) { return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", seg, n.t.header[n.i]) case *nullItem: default: - panic(internalError("stepInto has no case for node %T", n)) + panic(invariant.Broken("stepInto has no case for node %T", n)) } return nil, fmt.Errorf("no field %q", seg) } @@ -219,7 +220,7 @@ func routeSteps(steps []pathStep, pins *pinSet, t *table, r tableRoute, at int) case *tableColumn: steps = append(steps, pathStep{kind: stepColumn, at: at, name: t.header[next.i]}) default: - panic(internalError("%s: a route onto %T", t.segment, r.next)) + panic(invariant.Broken("%s: a route onto %T", t.segment, r.next)) } return steps, nil } @@ -302,13 +303,13 @@ func drawSteps(s *drawstate.State, n node, steps []pathStep, pins *pinSet, memo if st.kind == stepField { var err error if n, err = stepInto(n, st.name); err != nil { - panic(internalError("step %d: %v; a path's steps should have been compiled from the node they are drawn from", st.at, err)) + panic(invariant.Broken("step %d: %v; a path's steps should have been compiled from the node they are drawn from", st.at, err)) } continue } t, ok := n.(*table) if !ok { - panic(internalError("step %d: a table's step of kind %d on %T", st.at, st.kind, n)) + panic(invariant.Broken("step %d: a table's step of kind %d on %T", st.at, st.kind, n)) } switch { case st.kind == stepParent: @@ -325,7 +326,7 @@ func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node { switch st.kind { case stepSelect: if err := pins.pinRow(t, st.row); err != nil { - panic(internalError("%s[%s]: %v", t.segment, st.name, err)) + panic(invariant.Broken("%s[%s]: %v", t.segment, st.name, err)) } return t case stepDraw: @@ -340,7 +341,7 @@ func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node { case stepParent: return t.parentT } - panic(internalError("drawStep has no case for step kind %d", st.kind)) + panic(invariant.Broken("drawStep has no case for step kind %d", st.kind)) } func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []string, at int) node { @@ -403,7 +404,7 @@ func checkPathResolves(n node, tail []string, level string) error { func compilePath(n node, tail []string) pathCheck { w := pathCheck{tail: tail} if _, err := w.run(n); err != nil { - panic(internalError("%s: %v; the path should have been proved before it was compiled", grammar.JoinSegments(tail), err)) + panic(invariant.Broken("%s: %v; the path should have been proved before it was compiled", grammar.JoinSegments(tail), err)) } return w } diff --git a/pick.go b/pick.go index 7f2fa14..25ff73d 100644 --- a/pick.go +++ b/pick.go @@ -1,6 +1,9 @@ package fejkdata -import "github.com/larvit/fejkdata/internal/drawstate" +import ( + "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/invariant" +) // namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the // value each read there produced, keyed by the path from the name, and the table rows they @@ -140,7 +143,7 @@ func (sc renderScope) frameOf(scope *nameScope) *pickFrame { func readName(s *drawstate.State, sc renderScope, a arm) readValue { f := sc.frameOf(a.named.scope) if f == nil { - panic(internalError("name %q is read where no frame of its scope renders", a.named.name)) + panic(invariant.Broken("name %q is read where no frame of its scope renders", a.named.name)) } p := &f.picks[a.named.index] if r, done := p.memo.value[a.path]; done { diff --git a/pins.go b/pins.go index a4fd603..d59e5fc 100644 --- a/pins.go +++ b/pins.go @@ -1,6 +1,10 @@ package fejkdata -import "fmt" +import ( + "fmt" + + "github.com/larvit/fejkdata/internal/invariant" +) type tablePin struct { t *table @@ -28,7 +32,7 @@ func (p *pinSet) pinned(t *table) (int, bool) { func (p *pinSet) mustRow(t *table) int { r, ok := p.pinned(t) if !ok { - panic(internalError("a column of %s is rendered with no row pinned", t.segment)) + panic(invariant.Broken("a column of %s is rendered with no row pinned", t.segment)) } return r } diff --git a/reference.go b/reference.go index f9b882f..922076e 100644 --- a/reference.go +++ b/reference.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" ) // refBinding is what a reference resolves to: the head its category is held @@ -83,7 +84,7 @@ func compileArms(t *template) { func compileArm(t *template, a *arm) { head := t.head(a.head) if head == nil { - panic(internalError("{%s} reads a head nothing bound", a.spelling)) + panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling)) } w := compilePath(head, a.tail) a.steps, a.leaves = w.steps, w.leaves diff --git a/render.go b/render.go index c78af41..3f35f56 100644 --- a/render.go +++ b/render.go @@ -6,6 +6,7 @@ import ( "github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" ) // Fake generates a value for a dot path. Each segment descends one level: folder @@ -83,7 +84,7 @@ func render(s *drawstate.State, n node, sc renderScope) string { } return renderRepeat(s, n, sc) default: - panic(internalError("uncompiled node %T", n)) + panic(invariant.Broken("uncompiled node %T", n)) } } diff --git a/scope.go b/scope.go index 0420b85..5411c58 100644 --- a/scope.go +++ b/scope.go @@ -1,5 +1,7 @@ package fejkdata +import "github.com/larvit/fejkdata/internal/invariant" + // readValue is what one read drew: its text, and whether it landed on a null. type readValue struct { text string @@ -42,7 +44,7 @@ func (sc renderScope) at(n node, pins *pinSet) renderScope { // rowOf is the row of t its columns render from. func (sc renderScope) rowOf(t *table) int { if sc.row.t != t { - panic(internalError("a column of %s renders in a scope holding no row of it", t.segment)) + panic(invariant.Broken("a column of %s renders in a scope holding no row of it", t.segment)) } return sc.row.index } diff --git a/shipped.go b/shipped.go index f363510..38792d0 100644 --- a/shipped.go +++ b/shipped.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/larvit/fejkdata/internal/datafiles" + "github.com/larvit/fejkdata/internal/invariant" ) //go:generate env REGENERATE=1 go test -run ^TestShippedIndexIsCurrent$ . @@ -12,7 +13,7 @@ import ( //go:embed data var shippedFS embed.FS -var shippedSource = datafiles.Source{FS: shippedFS, BaseDir: "data"} +var shippedSource = datafiles.FS(shippedFS, "data") // shippedEntry is a shipped category's entry in shippedindex.go: its parent table, "" for // none, and the paths List advertises below it. After changing it, empty @@ -100,7 +101,7 @@ func loadShipped(root *folder, wanted []unloadedCategory) { } site, err := u.load(root) if err != nil { - panic(internalError("shipped %s: %v; after a change under data/, regenerate shippedindex.go", join(strings.Join(u.dir, "."), u.name), err)) + panic(invariant.Broken("shipped %s: %v; after a change under data/, regenerate shippedindex.go", join(strings.Join(u.dir, "."), u.name), err)) } sites = append(sites, site) queue = append(queue, unloadedReads(root, u.dir, sitesScope([]categorySite{site}))...) @@ -110,7 +111,7 @@ func loadShipped(root *folder, wanted []unloadedCategory) { return } if err := categoryBinding(sites, root.children).bind(); err != nil { - panic(internalError("binding shipped categories: %v; after a change under data/, regenerate shippedindex.go", err)) + panic(invariant.Broken("binding shipped categories: %v; after a change under data/, regenerate shippedindex.go", err)) } } diff --git a/struct.go b/struct.go index 013bc8f..d1f1c49 100644 --- a/struct.go +++ b/struct.go @@ -11,6 +11,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" ) @@ -393,6 +394,6 @@ func setText(field reflect.Value, text string) { field.SetUint(n) } if err != nil { - panic(internalError("%q reached a %s field unproven: %v", text, field.Type(), err)) + panic(invariant.Broken("%q reached a %s field unproven: %v", text, field.Type(), err)) } } diff --git a/todo.md b/todo.md index 166419d..2d0344e 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 112` +`Next ID = 114` | Goal | W | |---|---| @@ -83,6 +83,7 @@ | 79 | 0.1.0 | decision | **Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.** | 6 | 6 | 7 | 8 | 13 | 12.9 | | 105 | 0.1.0 | principle | **Link each template in steps that each return what they build — its references, its bindings' heads and addressed keys, its compiled arms, its column read — so no step writes into what an earlier one built.** | 6 | 7 | 2 | 9 | 3.2 | 12.4 | | 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 | +| 113 | 0.2.0 | defect | **Draw `{iban(CC)}` with its country's national check digits, and SE's with a real bank's id, so a validator checking more than mod 97 accepts it.** | 3 | 5 | 7 | 9 | 4 | 25.0 | | 26 | 0.2.0 | defect | **Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives.** | 4 | 6 | 7 | 8 | 4 | 22.1 | | 81 | 0.2.0 | decision | **Refuse at load every `calc` operand not proven numeric and every divisor not proven nonzero, in a string column too.** | 4 | 4 | 6 | 8 | 6.1, 4 | 20.6 | | 36 | 0.2.0 | | **Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states.** | 5 | 6 | 7 | 8 | 6 | 19.5 | @@ -108,6 +109,7 @@ | 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 7 | 8 | 16 | 14.0 | | 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 10 | 14.0 | | 107 | 0.2.0 | decision | **Move the template engine into `internal/`, leaving the root the public API and its options.** | 6 | 9 | 3 | 8 | 3.2, 3.4 | 12.2 | +| 112 | 0.2.0 | | **Check a calc's field operands where its template links, beside its name operands, once item 36 reports every mistake at once.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 | | 94 | 0.2.0 | | **Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`.** | 3 | 3 | 3 | 5 | 5.7 | 10.2 | | 108 | 0.2.0 | defect | **Name item 26 in the decision "No builtin reads the clock, so a date is bounded by days, never by an age", as the item that ends its conflict with goal 4.** | 1 | 1 | 1 | 4 | 3.4 | 8.0 | | 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 | @@ -285,6 +287,10 @@ Needs item 104. `linkBindings` writes `nameBinding.head` and `tail` in `linkRefs GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged. +### 113. Draw `{iban(CC)}` with its country's national check digits, and SE's with a real bank's id, so a validator checking more than mod 97 accepts it. + +`iban` in `internal/builtinfunc/checksum.go` draws a random numeric BBAN and sets only the mod-97 digits. BE's last two digits are the first ten mod 97, NO carries a mod-11 account check digit, FI a Luhn digit, ES two control digits, and SE's three-digit bank id must be a real bank's (`docs/research/research-sources-se.md`). Goal 4 asks for the check the real system applies; the README's `{iban(CC)}` row promises only length and mod 97, and changes with it. + ### 26. Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives. The format hard-codes `-`, and its earliest birthdate, 1930-01-01, makes the oldest draws invalid from 2030. A system holding Swedish personnummer often breaks on the `+`, so a fixture carrying one is a valuable test. Whether a number takes `+` depends on the date it is read, and the decision "No builtin reads the clock, so a date is bounded by days, never by an age" forbids a builtin from reading that date off the clock. This item revises it: once the caller gives the date, an age is a bound, and the CLI's default date makes output change with the day it runs unless the flag is given. Do the first step before the rest: @@ -297,7 +303,7 @@ The format hard-codes `-`, and its earliest birthdate, 1930-01-01, makes the old ### 81. Refuse at load every `calc` operand not proven numeric and every divisor not proven nonzero, in a string column too. -The decision "A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`" lets `{calc(a/(b*c))}` with `b` fixed at `"0"` write `Inf` into every row. An operand that is sometimes not a number prints `NaN`. Both work against goals 6.1 and 4. `valueproof.go` already proves a typed column's divisor nonzero. +The decision "A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`" lets `{calc(a/(b*c))}` with `b` fixed at `"0"` write `Inf` into every row. An operand that is sometimes not a number prints `NaN`. Both work against goals 6.1 and 4. `valueproof.go` already proves a typed column's divisor nonzero. Once every operand is proven, `neverNumeric` and `constantValue` in `calc.go` go, and with them their reading of `NaN` and `Inf` as numbers, which `proven.Literal` refuses: calc reads text as a number one way. ### 36. Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states. @@ -374,12 +380,16 @@ Require the path step to reach a sibling category. ### 46. Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files. -`readTableOptions` and `loadDir` return on the first in Go's map order. +`readTableOptions` and `walkDir` in `internal/datafiles` return on the first in Go's map order. ### 107. Move the template engine into `internal/`, leaving the root the public API and its options. Needs item 5. The engine returns column values and the root builds `Record`. Revises the decision "The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and items 101 and 102 move a table's rows there", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc". +### 112. Check a calc's field operands where its template links, beside its name operands, once item 36 reports every mistake at once. + +Needs item 36. `checkCalcFields` runs at compile and `checkCalcNames` at link, two passes over one rule. Moving the field check to link before item 36 would change which of two mistakes in two category files a load reports, since every category compiles before any links. + ### 94. Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`. Both spellings load and draw alike, against goal 5.7. Widen the read-once refusal in `namefence.go` to the longest prefix every read of a name shares. @@ -442,7 +452,7 @@ Both phone formats carry the same 21-code `area` list, so an edit to one copy ca ### 47. Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there. -`loadDir` indexes hidden TSVs and skips them in two places, so one loads where its comment says a hidden file is never data. +`internal/datafiles` indexes hidden TSVs in `newRowsFiles` and skips them in `walkDir`, so one loads where its comment says a hidden file is never data. ### 72. Fail the merge gate when test coverage falls below the last recorded figure. @@ -482,7 +492,7 @@ The check covers `data` and `testdata/shipped_shape.txt` alone, where `AGENTS.md ### 66. Cut the README's Layout block to the lines that say what a file name cannot. -`calc.go`, `doc.go` and `cmd/fejkdata/` restate their names. +`doc.go` and `cmd/fejkdata/` restate their names. ### 67. Cut `AGENTS.md` to the rules only it states, and explain every term its comprehension rule uses. diff --git a/valueproof.go b/valueproof.go index fc82ed2..7bc59cd 100644 --- a/valueproof.go +++ b/valueproof.go @@ -6,6 +6,7 @@ import ( "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" "github.com/larvit/fejkdata/internal/proven" ) @@ -116,7 +117,7 @@ func (p *valueProof) prove(n node) proven.Value { case *nullItem: v = proven.Unproven(`it reads a null, which renders "" outside its own column`) default: - panic(internalError("prove has no case for node %T", n)) + panic(invariant.Broken("prove has no case for node %T", n)) } p.memo[n] = v return v @@ -193,7 +194,7 @@ func (p *valueProof) proveCalc(o op) proven.Value { return proven.PrintedNumber(body, v, builtinfunc.CalcDecimals(args)) } -var typedCalls, operandCalls = builtinfunc.NumberCalls(false), builtinfunc.NumberCalls(true) +var typedCalls, operandCalls = builtinfunc.TypedCalls(), builtinfunc.OperandCalls() func notOneValue(format, calls string) string { return fmt.Sprintf("%q is not one value; write one literal or one %s, or read one", format, calls)