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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ services:
|
||||
|
||||
cover:
|
||||
<<: *go
|
||||
command: go test -cover ./...
|
||||
command: go test -cover -coverpkg=./... ./...
|
||||
|
||||
bench:
|
||||
<<: *go
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
+2
-3
@@ -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.
|
||||
|
||||
+4
-8
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+4
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+3
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user