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
|
# 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.
|
- 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.
|
- 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.
|
- 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
|
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
|
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
|
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
|
every category it reads and every table linked to it, and `List` reads an index for what
|
||||||
category loads in `New`. Proving the draw fences adds a pass over the loaded tree per
|
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
|
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
|
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
|
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
|
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
|
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
|
## 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`
|
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
|
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
|
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
|
## 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
|
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
|
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
|
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
|
## 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
|
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,
|
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
|
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
|
## 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))
|
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 {
|
for _, p := range n.unloaded[name].paths {
|
||||||
out = append(out, join(name, p))
|
out = append(out, join(name, p))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func namedNodes(m map[string]node) []namedNode {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedNames(m map[string]node) []string {
|
func sortedNames[V any](m map[string]V) []string {
|
||||||
names := make([]string, 0, len(m))
|
names := make([]string, 0, len(m))
|
||||||
for name := range m {
|
for name := range m {
|
||||||
names = append(names, name)
|
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.
|
// compileFields compiles every non-option key of a template object.
|
||||||
func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
||||||
fields := make(map[string]node, len(m))
|
fields := make(map[string]node, len(m))
|
||||||
keys := make([]string, 0, len(m))
|
for _, k := range sortedNames(m) {
|
||||||
for k := range m {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
if isOption(k) {
|
if isOption(k) {
|
||||||
continue
|
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
|
// 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.
|
// 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) {
|
func resolveCategory(root map[string]node, segments []string) (categorySegs []string, target node, tail []string, err error) {
|
||||||
var n node = &folder{children: root}
|
g, i := folderAt(&folder{children: root}, segments)
|
||||||
i := 0
|
switch {
|
||||||
for ; i < len(segments); i++ {
|
case i == len(segments):
|
||||||
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 {
|
|
||||||
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
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.
|
// refTokens returns the reference names a format reads, as tokens or as operands, or binds.
|
||||||
|
|||||||
+12
-26
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate env REPIN=1 go test -run ^TestShippedIndexIsCurrent$ .
|
//go:generate env REGENERATE=1 go test -run ^TestShippedIndexIsCurrent$ .
|
||||||
|
|
||||||
//go:embed data
|
//go:embed data
|
||||||
var shippedFS embed.FS
|
var shippedFS embed.FS
|
||||||
@@ -17,7 +17,8 @@ var shippedFS embed.FS
|
|||||||
var shippedSource = dataSource{fsys: shippedFS, baseDir: "data"}
|
var shippedSource = dataSource{fsys: shippedFS, baseDir: "data"}
|
||||||
|
|
||||||
// shippedEntry is a shipped category's entry in shippedindex.go: the table beside it that
|
// 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 {
|
type shippedEntry struct {
|
||||||
parent string
|
parent string
|
||||||
paths []string
|
paths []string
|
||||||
@@ -45,15 +46,6 @@ func unloadedTree() folder {
|
|||||||
return root
|
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
|
// unloadedCategory is a shipped category not loaded yet: the folder holding it, that
|
||||||
// folder's path, and its name there.
|
// folder's path, and its name there.
|
||||||
type unloadedCategory struct {
|
type unloadedCategory struct {
|
||||||
@@ -64,25 +56,19 @@ type unloadedCategory struct {
|
|||||||
|
|
||||||
// unloadedAt is the unloaded shipped category a path from root names, or descends into.
|
// unloadedAt is the unloaded shipped category a path from root names, or descends into.
|
||||||
func unloadedAt(root *folder, segs []string) (unloadedCategory, bool) {
|
func unloadedAt(root *folder, segs []string) (unloadedCategory, bool) {
|
||||||
g := root
|
g, i := folderAt(root, segs)
|
||||||
for i, seg := range segs {
|
if i == len(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return unloadedCategory{}, false
|
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
|
// 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
|
// it is loaded already. An entry point calls it before walking root: a walk is a query and
|
||||||
// would load mid-render.
|
// loads nothing.
|
||||||
func (f *Generator) loadShippedAt(segs []string) {
|
func (f *Generator) loadShippedAt(segs []string) {
|
||||||
if u, unloaded := unloadedAt(&f.root, segs); unloaded {
|
if u, unloaded := unloadedAt(&f.root, segs); unloaded {
|
||||||
loadShipped(&f.root, []unloadedCategory{u})
|
loadShipped(&f.root, []unloadedCategory{u})
|
||||||
@@ -163,7 +149,7 @@ func (u unloadedCategory) linkedTables(e shippedEntry) []unloadedCategory {
|
|||||||
if e.parent != "" {
|
if e.parent != "" {
|
||||||
out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: 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 {
|
if u.in.unloaded[name].parent == u.name {
|
||||||
out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: 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
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// REPIN=1 rewrites the index.
|
// REGENERATE=1, which go generate sets, rewrites the index.
|
||||||
func TestShippedIndexIsCurrent(t *testing.T) {
|
func TestShippedIndexIsCurrent(t *testing.T) {
|
||||||
got, err := shippedIndexSource(newShippedWhole(t))
|
got, err := shippedIndexSource(newShippedWhole(t))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if os.Getenv("REPIN") == "1" {
|
if os.Getenv("REGENERATE") == "1" {
|
||||||
if err := os.WriteFile(shippedIndexFile, got, 0o644); err != nil {
|
if err := os.WriteFile(shippedIndexFile, got, 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user