Answer architecture round 2: one folder walk and one sorted-keys helper, REGENERATE for the index, and the held decisions naming the items that end them
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Rules
|
||||
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo>`; the merge gate is `docker build .` plus CI's changelog check.
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate>`; the merge gate is `docker build .` plus CI's changelog check.
|
||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
||||
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table.
|
||||
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||
|
||||
@@ -816,8 +816,8 @@ The target is about a microsecond per value (goal 12).
|
||||
Each file is parsed, validated and weight-indexed once, bar a table's name and parent
|
||||
columns, which are mapped on the first `Fake` that selects by name or descends through
|
||||
it. With the shipped set alone, a category loads on the first call reaching it, with
|
||||
every category it reads, and `List` reads an index; beside a `--data-path`, every
|
||||
category loads in `New`. Proving the draw fences adds a pass over the loaded tree per
|
||||
every category it reads and every table linked to it, and `List` reads an index for what
|
||||
has not loaded yet; beside a `--data-path`, every category loads in `New`. Proving the draw fences adds a pass over the loaded tree per
|
||||
fence, and walks what a render reads only where data holds a reference, so a set that
|
||||
holds none pays for the passes alone. A `Fake` call then costs about what its output
|
||||
costs: an unweighted pick is O(1) whatever the list's length, a weighted one
|
||||
|
||||
+7
-4
@@ -171,7 +171,7 @@ release instead. Before `v1.0.0` a minor carries what a major would.
|
||||
|
||||
Any edit to a category shifts its stream and everything drawn after it, so a promise
|
||||
across versions would freeze every shipped list; a fixture is re-pinned on a bump, as
|
||||
this repo's own are.
|
||||
this repo's own are. Goal 9 reads wider than this until `todo.md` item 85 rewords it.
|
||||
|
||||
## An error is a contract by what it names, not its bytes
|
||||
|
||||
@@ -259,7 +259,8 @@ assumes a 64-bit int; on a 32-bit target it could overflow and panic.
|
||||
load error; the fold stops where an operand varies, so `a/(b*c)` with `b` fixed at `0`
|
||||
and `c` varying loads and prints `Inf` every draw — catching it needs zero-absorbing
|
||||
algebra for a shape nobody writes. A [typed column](../README.md#datatype) bounds its operands
|
||||
instead and refuses a divisor it cannot keep from zero.
|
||||
instead and refuses a divisor it cannot keep from zero. `Inf` in a string column works
|
||||
against goals 4 and 6; `todo.md` item 81 asks the maintainer whether to refuse it.
|
||||
|
||||
## In data, a default written out and a constant spelled as a sample are load errors
|
||||
|
||||
@@ -466,7 +467,8 @@ The benchmark suite (the README's Development) reports time for a human, not as
|
||||
objects would cost it a second; a TSV is one allocation whose cells are substrings, and
|
||||
the JSON says only how a row is composed. The TSV sits beside its category file, named
|
||||
by `rows`, so a data directory stays a directory of categories, and one nothing names is
|
||||
a load error rather than a file silently ignored.
|
||||
a load error rather than a file silently ignored. A TSV works against goal 8's "Data is
|
||||
JSON files" until `todo.md` item 84 rewords the goal or ends this decision.
|
||||
|
||||
## A selector is bracketed, and a dot inside it is literal
|
||||
|
||||
@@ -909,7 +911,8 @@ mode field, leaked the check's leaves and errors with the draw's pins, arm and m
|
||||
since Go tracks a struct's fields as one, and a repeat of a reference path rose from 66
|
||||
to 106 allocations. `drawSteps` takes its state as parameters and a step holds no node,
|
||||
so no benchmark gained an allocation. Valid while Go's escape analysis tracks
|
||||
a struct's fields as one.
|
||||
a struct's fields as one. The two loops have drifted, against DRY, until `todo.md` item 80
|
||||
walks both through one per-step function.
|
||||
|
||||
## A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
|
||||
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ func paths(n node) []string {
|
||||
out = append(out, join(name, p))
|
||||
}
|
||||
}
|
||||
for _, name := range sortedEntries(n.unloaded) {
|
||||
for _, name := range sortedNames(n.unloaded) {
|
||||
for _, p := range n.unloaded[name].paths {
|
||||
out = append(out, join(name, p))
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func namedNodes(m map[string]node) []namedNode {
|
||||
return out
|
||||
}
|
||||
|
||||
func sortedNames(m map[string]node) []string {
|
||||
func sortedNames[V any](m map[string]V) []string {
|
||||
names := make([]string, 0, len(m))
|
||||
for name := range m {
|
||||
names = append(names, name)
|
||||
|
||||
@@ -406,12 +406,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
|
||||
// compileFields compiles every non-option key of a template object.
|
||||
func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
||||
fields := make(map[string]node, len(m))
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
for _, k := range sortedNames(m) {
|
||||
if isOption(k) {
|
||||
continue
|
||||
}
|
||||
|
||||
+24
-18
@@ -214,26 +214,32 @@ func eachTemplate(sites []categorySite, fn func(s categorySite, path string, t *
|
||||
// resolveCategory walks a dotted path through the folders to the category it
|
||||
// names, returning that category's segments, the node, and the tail left to read into it.
|
||||
func resolveCategory(root map[string]node, segments []string) (categorySegs []string, target node, tail []string, err error) {
|
||||
var n node = &folder{children: root}
|
||||
i := 0
|
||||
for ; i < len(segments); i++ {
|
||||
g, ok := n.(*folder)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if isSelector(segments[i]) {
|
||||
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
|
||||
}
|
||||
child, ok := g.children[segments[i]]
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("no entry %q", segments[i])
|
||||
}
|
||||
n = child
|
||||
}
|
||||
if _, ok := n.(*folder); ok {
|
||||
g, i := folderAt(&folder{children: root}, segments)
|
||||
switch {
|
||||
case i == len(segments):
|
||||
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
||||
case isSelector(segments[i]):
|
||||
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
|
||||
}
|
||||
return segments[:i], n, segments[i:], nil
|
||||
n, ok := g.children[segments[i]]
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("no entry %q", segments[i])
|
||||
}
|
||||
return segments[:i+1], n, segments[i+1:], nil
|
||||
}
|
||||
|
||||
// folderAt is the deepest folder a path from root walks into, and the index of the first
|
||||
// segment naming no folder in it, len(segs) where every segment does.
|
||||
func folderAt(root *folder, segs []string) (*folder, int) {
|
||||
g := root
|
||||
for i, seg := range segs {
|
||||
sub, isFolder := g.children[seg].(*folder)
|
||||
if !isFolder {
|
||||
return g, i
|
||||
}
|
||||
g = sub
|
||||
}
|
||||
return g, len(segs)
|
||||
}
|
||||
|
||||
// refTokens returns the reference names a format reads, as tokens or as operands, or binds.
|
||||
|
||||
+12
-26
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate env REPIN=1 go test -run ^TestShippedIndexIsCurrent$ .
|
||||
//go:generate env REGENERATE=1 go test -run ^TestShippedIndexIsCurrent$ .
|
||||
|
||||
//go:embed data
|
||||
var shippedFS embed.FS
|
||||
@@ -17,7 +17,8 @@ var shippedFS embed.FS
|
||||
var shippedSource = dataSource{fsys: shippedFS, baseDir: "data"}
|
||||
|
||||
// shippedEntry is a shipped category's entry in shippedindex.go: the table beside it that
|
||||
// it links to, "" for none, and the paths List advertises below it.
|
||||
// it links to, "" for none, and the paths List advertises below it. After changing it, empty
|
||||
// shippedIndex's literal by hand so the package builds, then run generate.
|
||||
type shippedEntry struct {
|
||||
parent string
|
||||
paths []string
|
||||
@@ -45,15 +46,6 @@ func unloadedTree() folder {
|
||||
return root
|
||||
}
|
||||
|
||||
func sortedEntries(m map[string]shippedEntry) []string {
|
||||
names := make([]string, 0, len(m))
|
||||
for name := range m {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// unloadedCategory is a shipped category not loaded yet: the folder holding it, that
|
||||
// folder's path, and its name there.
|
||||
type unloadedCategory struct {
|
||||
@@ -64,25 +56,19 @@ type unloadedCategory struct {
|
||||
|
||||
// unloadedAt is the unloaded shipped category a path from root names, or descends into.
|
||||
func unloadedAt(root *folder, segs []string) (unloadedCategory, bool) {
|
||||
g := root
|
||||
for i, seg := range segs {
|
||||
switch n := g.children[seg].(type) {
|
||||
case *folder:
|
||||
g = n
|
||||
continue
|
||||
case nil:
|
||||
if _, unloaded := g.unloaded[seg]; unloaded {
|
||||
return unloadedCategory{dir: segs[:i:i], in: g, name: seg}, true
|
||||
}
|
||||
}
|
||||
g, i := folderAt(root, segs)
|
||||
if i == len(segs) {
|
||||
return unloadedCategory{}, false
|
||||
}
|
||||
return unloadedCategory{}, false
|
||||
if _, unloaded := g.unloaded[segs[i]]; !unloaded {
|
||||
return unloadedCategory{}, false
|
||||
}
|
||||
return unloadedCategory{dir: segs[:i:i], in: g, name: segs[i]}, true
|
||||
}
|
||||
|
||||
// loadShippedAt loads the shipped category a caller's path names or descends into, unless
|
||||
// it is loaded already. An entry point walking root calls it first: a walk that loaded
|
||||
// would load mid-render.
|
||||
// it is loaded already. An entry point calls it before walking root: a walk is a query and
|
||||
// loads nothing.
|
||||
func (f *Generator) loadShippedAt(segs []string) {
|
||||
if u, unloaded := unloadedAt(&f.root, segs); unloaded {
|
||||
loadShipped(&f.root, []unloadedCategory{u})
|
||||
@@ -163,7 +149,7 @@ func (u unloadedCategory) linkedTables(e shippedEntry) []unloadedCategory {
|
||||
if e.parent != "" {
|
||||
out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: e.parent})
|
||||
}
|
||||
for _, name := range sortedEntries(u.in.unloaded) {
|
||||
for _, name := range sortedNames(u.in.unloaded) {
|
||||
if u.in.unloaded[name].parent == u.name {
|
||||
out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: name})
|
||||
}
|
||||
|
||||
+2
-2
@@ -39,13 +39,13 @@ func loadedCategories(f *Generator) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// REPIN=1 rewrites the index.
|
||||
// REGENERATE=1, which go generate sets, rewrites the index.
|
||||
func TestShippedIndexIsCurrent(t *testing.T) {
|
||||
got, err := shippedIndexSource(newShippedWhole(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if os.Getenv("REPIN") == "1" {
|
||||
if os.Getenv("REGENERATE") == "1" {
|
||||
if err := os.WriteFile(shippedIndexFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user