Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
20 changed files with 202 additions and 227 deletions
Showing only changes of commit 0002efb9d4 - Show all commits
-1
View File
@@ -3,7 +3,6 @@ FROM golang:${GO_VERSION} AS portable
WORKDIR /app
# Module layer cached separately from source.
COPY go.mod ./
RUN go mod download
+5 -5
View File
@@ -987,11 +987,11 @@ shipped.go the embedded data set, and loading a shipped category on the fir
shippedindex.go the index of shipped categories, generated from data/
node.go the node model and JSON -> node compilation
table.go tables: their options, format and cells, the link to the parent beside them, and how a path passes and draws a table
name.go names: {x as n} bindings, the scopes they live in, the reads they answer, and each binding's target and the keys its reads address
name.go names: {x as n} bindings, the scopes they live in, the reads they answer, and each binding's target
namefence.go the load fences over names
pick.go a render's named picks: the frame of each name scope rendering, the memo a pick keeps, and the reads kept under it
pick.go a render's named picks: the key of each level a pick keeps a draw at, built at load and read at render, the frame of each name scope rendering, the memo a pick keeps, and the reads kept under it
env.go renderEnv: what a render reads names and rows through, the stack of frames it renders in, and the frames a read opens and finds
path.go the dotted-path walks, and proving a path reaches a node
path.go the dotted-path walks, proving a path reaches a node, and the sub-paths every variant of a choice carries
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype
@@ -1001,9 +1001,9 @@ arm.go the compiled read of one {a|b} alternative or operand: its head,
reference.go one reference's resolution: its sigil, its category, its tail
resolve.go resolving templates against the assembled tree in steps, each over every template before the next
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
sorted.go sortedNames: a map's keys, sorted
sorted.go sortedNames, a map's keys sorted, and join, two dotted paths joined
calc.go the {calc()} operand checks: never a number, and a constant zero divisor
datatype.go column datatypes: DataType, a template's declared datatype, a column's datatype
datatype.go column datatypes: DataType, a template's declared datatype, a column's datatype, and a format that only reads a column
valueproof.go the value proof: what a typed column, calc operand or struct field holds, checked at load
data.go the load path: the categories internal/datafiles hands over, compiled into one merged tree, and the load pipeline every entry point runs
cmd/fejkdata/ the fejkdata CLI
-9
View File
@@ -54,15 +54,6 @@ func pathArm(name, head string, segs []string) arm {
return arm{spelling: name, head: head, tail: segs, levels: levelKeys(append([]string{head}, segs...), 1)}
}
// levelKeys is the key of each prefix of path holding at least from segments, shortest first.
func levelKeys(path []string, from int) []pickKey {
levels := make([]pickKey, len(path)-from+1)
for i := range levels {
levels[i] = pickKey(grammar.JoinSegments(path[:from+i]))
}
return levels
}
// key is the key of the leaf a lands on, the one key every way of writing this read shares.
func (a arm) key() pickKey { return a.levels[len(a.levels)-1] }
-1
View File
@@ -47,7 +47,6 @@ services:
<<: *go
command: go mod tidy
# Interactive shell for ad-hoc work: docker compose run --rm dev
dev:
<<: *go
command: sh
+42
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/larvit/fejkdata/internal/datatype"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
)
@@ -212,3 +213,44 @@ func typedAs(t *template, d DataType) string {
}
return fmt.Sprintf(`give %q "datatype": %q`, t.format, d)
}
// columnRead is a record's column read by a format that only reads one reference or name,
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not or t declares a string, which reads the column as text.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.datatype != nil && *t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
var start node
var category string
var tail []string
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
// {f as n}{n}: column f of the record binding n, in t's own category.
start, tail = b.binder, append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
// {/c as n}{n.x} or {/c.x as n}{n}: column x of category c.
start, category = targets[b].start, categoryOf(b.binder.refs.byName[b.ref].head)
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case grammar.IsRef(a.head):
// {/c.x}: column x of category c.
start, category, tail = t.startOf(a.head), categoryOf(a.head), a.tail
default:
return nil
}
if target, isTemplate := start.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+3 -14
View File
@@ -29,20 +29,9 @@ package fejkdata
// `namedPick`, `pickFrame`.
// - memo — what a named pick keeps its draws in, the variant drawn at each level
// and the value each path read: `drawMemo`.
// - level — a prefix of a read's path, from where it starts to its leaf; a choice and the
// variant drawn from it, or a table and its row, share one. `arm.levels` holds one pick
// key per level.
// - pick key — the key a memo keeps a level's draw under, the level's path of segments:
// `pickKey`, `levelKeys`.
// - addressed key — a pick key some read of a name lands on or passes, from the level the
// name's binding lands on onward: `addressedKeys`. A fresh read rendering in a pick is
// kept in it only where its first level is addressed: `renderEnv.keeps`, `readUnder`.
// In data/geo/SE/address.json, {.locality as l} binds l, and {l.street.name} passes the
// levels "", "street" and "street.name", the last its key. With {l.name} and
// {l.postal-code.code}, l's addressed keys are those three, "name", "postal-code" and
// "postal-code.code". Where p binds a category whose format reads {first}, and both {p}
// and {p.first} are read, the {first} that {p} renders has the key "first" under {p}'s
// key "". {p.first} addresses "first", so both read one draw.
// - level — a prefix of a read's path, from where it starts to its leaf: `pickKey`.
// - pick key — the key a memo keeps a level's draw under: `pickKey`.
// - addressed key — a pick key some read of a name lands on or passes: `pickKey`.
// - pin — fixing which row of a table one path, or one named pick, uses:
// `pinSet`, `namedPick.pins`, `table.followStep`.
// - link — a table's tie to its parent table, each row to its parent row:
-6
View File
@@ -111,9 +111,3 @@ func (env renderEnv) drawRowOf(s *drawstate.State, t *table) int {
}
return t.rows.Draw(s)
}
// keeps reports whether a read of env's name addresses the level a starts at.
func (env renderEnv) keeps(a arm) bool {
_, kept := env.pick.named.addressed[env.pickAt.under(a.levels[0])]
return kept
}
+4 -2
View File
@@ -17,7 +17,8 @@ import (
// any one path; the CLI's --repeat shares it.
const MaxRepeat = 1 << 20
var _ [^uint(0)>>63 - 1]struct{} // docs/decisions.md#64-bit-targets-only
// Fails to compile where uint is narrower than 64 bits: docs/decisions.md#64-bit-targets-only
var _ [^uint(0)>>63 - 1]struct{}
// Generator generates fake data from a namespace tree. Create one with [New].
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn
@@ -88,7 +89,8 @@ func New(opts ...Option) (*Generator, error) {
return &Generator{drawState: drawstate.New(seed), root: root}, nil
}
// docs/decisions.md#with-only-the-shipped-set-a-category-loads-on-the-first-call-reaching-it-beside-a---data-path-every-category-loads-in-new
// load builds the tree New starts from: with only the shipped set, every category unloaded; else
// every source loaded and merged. docs/decisions.md#with-only-the-shipped-set-a-category-loads-on-the-first-call-reaching-it-beside-a---data-path-every-category-loads-in-new
func (c config) load() (folder, error) {
if c.shipped && len(c.sources) == 0 {
return unloadedTree(), nil
+15 -16
View File
@@ -32,43 +32,43 @@ func eachNode(n node, label string, fn func(label string, n node) error) error {
return visit(label, n)
}
// namedNode is a contained child and the segment reaching it; a choice's items carry
// containedNode is a contained child and the segment reaching it; a choice's items carry
// no segment, matching how a dot path steps over a choice.
type namedNode struct {
type containedNode struct {
name string
node node
line int // a table cell's line in its rows file, else 0
}
func (c namedNode) labelIn(label string) string {
func (c containedNode) labelIn(label string) string {
if c.line > 0 {
return fmt.Sprintf("%s, line %d", label, c.line)
}
return join(label, c.name)
}
func contained(n node) []namedNode {
func contained(n node) []containedNode {
switch n := n.(type) {
case *folder:
return namedNodes(n.children)
return containedByName(n.children)
case *choice:
out := make([]namedNode, len(n.items))
out := make([]containedNode, len(n.items))
for i, it := range n.items {
out[i] = namedNode{node: it}
out[i] = containedNode{node: it}
}
return out
case *template:
return namedNodes(n.fields)
return containedByName(n.fields)
case *table:
return append([]namedNode{{node: n.formatTemplate}}, namedNodes(n.formatTemplate.fields)...)
return append([]containedNode{{node: n.formatTemplate}}, containedByName(n.formatTemplate.fields)...)
case *tableColumn:
if len(n.t.cellTemplates) == 0 {
return nil
}
var out []namedNode
var out []containedNode
for r := 0; r < n.t.rows.Len(); r++ {
if cell := n.t.cellTemplate(r, n.i); cell != nil {
out = append(out, namedNode{node: cell, line: r + 2})
out = append(out, containedNode{node: cell, line: r + 2})
}
}
return out
@@ -79,10 +79,10 @@ func contained(n node) []namedNode {
}
}
func namedNodes(m map[string]node) []namedNode {
out := make([]namedNode, 0, len(m))
func containedByName(m map[string]node) []containedNode {
out := make([]containedNode, 0, len(m))
for _, name := range sortedNames(m) {
out = append(out, namedNode{name: name, node: m[name]})
out = append(out, containedNode{name: name, node: m[name]})
}
return out
}
@@ -95,8 +95,7 @@ type renderEdge struct {
}
// renderEdges lists the children rendering n recurses into, mirroring render: a
// choice's items, and a template's field/reference tokens plus its operands. A
// folder renders nothing, so it has no edges.
// choice's items, and a template's field/reference tokens plus its operands.
func renderEdges(n node) []renderEdge {
switch n := n.(type) {
case *choice:
-1
View File
@@ -330,7 +330,6 @@ func (t *Table[O]) checkAncestors(p *Table[O], sibling func(name string) *Table[
return nil
}
// Children is the tables linked to t, sorted by segment.
func (t *Table[O]) Children() []*Table[O] {
out := make([]*Table[O], 0, len(t.children))
for _, c := range t.children {
-40
View File
@@ -62,43 +62,3 @@ func tablePaths(t *table, intoRepeats bool) []string {
}
return out
}
// sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] {
if len(shared) == 0 {
return nil
}
next := subPaths(it, intoRepeats)
for p := range shared {
if !next[p] {
delete(shared, p)
}
}
}
return shared
}
// subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{}
for _, p := range paths(n, intoRepeats) {
if p != "" {
out[p] = true
}
}
return out
}
func join(prefix, name string) string {
switch {
case prefix == "":
return name
case name == "":
return prefix
}
return prefix + "." + name
}
-21
View File
@@ -109,7 +109,6 @@ func (sc *nameScope) bindAll(t *template, where string) error {
return nil
}
// spelled names the scope in an error.
func (sc *nameScope) spelled() string {
if k := sc.kind(); k != scopeCategory {
return "in one " + k.String()
@@ -269,23 +268,3 @@ func nameTargets(ts []templateSite) map[*nameBinding]nameTarget {
}
return targets
}
// addressedKeys is every key the reads of each name in ts land on or pass, from the name's own
// level, with the spelling of the first read reaching it.
func addressedKeys(ts []templateSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[pickKey]string {
keys := map[*nameBinding]map[pickKey]string{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
if keys[b] == nil {
keys[b] = map[pickKey]string{}
}
for _, key := range r.a.levels[len(targets[b].tail):] {
if _, seen := keys[b][key]; !seen {
keys[b][key] = r.a.spelling
}
}
}
}
return keys
}
+5 -3
View File
@@ -65,9 +65,11 @@ type template struct {
nameScope *nameScope // where its tokens look a name up
// Filled by `resolveTemplates`, from the assembled tree:
refs templateRefs
compiled formatOps
readsColumn *columnRead // set when the format only reads one reference or name, and that read is a record's column
refs templateRefs
compiled formatOps
// readsColumn is set when the format only reads one reference or name, and that read is a
// record's column. renderLeaf, proveColumnItem and itemDatatype each read it: change them together.
readsColumn *columnRead
// Filled by `settleRecords`, once the load refused every cycle:
columns []recordColumn // a record's, where it has fields, in name order
+38 -8
View File
@@ -68,10 +68,10 @@ const (
// compiledPath walks a path and proves it reaches a node whichever way the draws go: every
// variant of a choice carries the rest of it, and is walked, a selector names a row inside the
// rows selected before it, and no level read carries a repeat. Once run, it holds the steps a
// draw takes, and every leaf the path may render; level names the head in its errors.
// draw takes, and every leaf the path may render; label names the head in its errors.
type compiledPath struct {
pins pinSet
level string
label string
tail []string
steps []pathStep
leaves []node
@@ -88,7 +88,7 @@ func (w *compiledPath) walk(n node, tail []string) (node, error) {
return w.walkEvery(c, at.tail)
}
var err error
if w.steps, err = compileStep(&at, w.tail, w.level, w.steps, &w.pins); err != nil {
if w.steps, err = compileStep(&at, w.tail, w.label, w.steps, &w.pins); err != nil {
return nil, err
}
}
@@ -112,8 +112,8 @@ func (p pathPos) more() bool { return len(p.tail) > 0 || p.fromRow }
// compileStep takes the first step of at.tail from at.n, which must not be a choice, moves at past
// it, and appends the step to steps: a field, or the route through a table. whole is the full
// path; level is the prefix an error puts before it.
func compileStep(at *pathPos, whole []string, level string, steps []pathStep, pins *pinSet) ([]pathStep, error) {
// path; label is the prefix an error puts before it.
func compileStep(at *pathPos, whole []string, label string, steps []pathStep, pins *pinSet) ([]pathStep, error) {
i := len(whole) - len(at.tail)
switch x := at.n.(type) {
case *table:
@@ -125,7 +125,7 @@ func compileStep(at *pathPos, whole []string, level string, steps []pathStep, pi
return routeSteps(steps, pins, x, r, i)
case *template:
if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) {
return steps, repeatLevelError(join(level, strings.Join(whole[:i], ".")))
return steps, repeatLevelError(join(label, strings.Join(whole[:i], ".")))
}
}
next, err := childNamed(at.n, at.tail[0])
@@ -222,6 +222,36 @@ func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []pickKey
return memo.variantOf(s, c, levels[at])
}
// sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] {
if len(shared) == 0 {
return nil
}
next := subPaths(it, intoRepeats)
for p := range shared {
if !next[p] {
delete(shared, p)
}
}
}
return shared
}
// subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{}
for _, p := range paths(n, intoRepeats) {
if p != "" {
out[p] = true
}
}
return out
}
// carriedByAll is the choice rule a path that must reach a node on every call obeys:
// the rest of the tail must be one every variant carries.
func carriedByAll(c *choice, rest []string) error {
@@ -241,8 +271,8 @@ func unreachableInChoice(c *choice, want string) error {
return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered)
}
func provePath(n node, tail []string, level string) error {
_, err := (&compiledPath{level: level, tail: tail}).run(n)
func provePath(n node, tail []string, label string) error {
_, err := (&compiledPath{label: label, tail: tail}).run(n)
return err
}
+72 -19
View File
@@ -2,12 +2,28 @@ package fejkdata
import (
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
)
// pickKey is a level's path of segments from a root keyed "": for a named read, the node its
// name's target starts at; for a fresh read, the template it sits in. under moves a fresh read's
// key into the pick it renders in.
// pickKey is the key a named pick's memo keeps one level's draw under.
//
// A level is a prefix of a read's path, from where it starts to its leaf; a choice and the
// variant drawn from it, or a table and its row, share one. A level's key is its path of
// segments from a root keyed "": for a named read, the node its name's target starts at; for a
// fresh read, the template it sits in. Load gives each read one key per level, in levelKeys.
//
// A name's addressed keys are the keys some read of it lands on or passes, from the level its
// binding lands on onward; load gathers them in addressedKeys. A fresh read rendering in a pick
// is kept in it only where its first level, moved under the key of what renders, is addressed:
// keptInPick, then readUnder.
//
// In data/geo/SE/address.json, {.locality as l} binds l, and {l.street.name} passes the levels
// "", "street" and "street.name", the last its key. With {l.name} and {l.postal-code.code},
// l's addressed keys are those three, "name", "postal-code" and "postal-code.code". Where p
// binds a category whose format reads {first}, and both {p} and {p.first} are read, the {first}
// that {p} renders has the key "first" under {p}'s key "". {p.first} addresses "first", so both
// read one draw.
type pickKey string
// under is rel, a key from a fresh read's levels, moved under k, the key of what renders. It never
@@ -19,6 +35,59 @@ func (k pickKey) under(rel pickKey) pickKey {
return k + "." + rel
}
// levelKeys is the key of each prefix of path holding at least from segments, shortest first.
func levelKeys(path []string, from int) []pickKey {
levels := make([]pickKey, len(path)-from+1)
for i := range levels {
levels[i] = pickKey(grammar.JoinSegments(path[:from+i]))
}
return levels
}
// addressedKeys is every key the reads of each name in ts land on or pass, from the name's own
// level, with the spelling of the first read reaching it.
func addressedKeys(ts []templateSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[pickKey]string {
keys := map[*nameBinding]map[pickKey]string{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
if keys[b] == nil {
keys[b] = map[pickKey]string{}
}
for _, key := range r.a.levels[len(targets[b].tail):] {
if _, seen := keys[b][key]; !seen {
keys[b][key] = r.a.spelling
}
}
}
}
return keys
}
// keptInPick reports whether a, a fresh read rendering in env.pick, is kept in it: a reads a
// field, and a read of the pick's name addresses the level a starts at.
func keptInPick(env renderEnv, a arm) bool {
if env.pick == nil || grammar.IsRef(a.head) {
return false
}
_, kept := env.pick.named.addressed[env.pickAt.under(a.levels[0])]
return kept
}
// readUnder reads a, an arm of t kept in env.pick, once per pick, keyed by a's key under env.pickAt.
func readUnder(s *drawstate.State, t *template, env renderEnv, a arm) readValue {
p, key := env.pick, env.pickAt.under(a.key())
if r, done := p.memo.value[key]; done {
return r
}
levels := make([]pickKey, len(a.levels))
for i, l := range a.levels {
levels[i] = env.pickAt.under(l)
}
leaf, pins := p.draw(s, t.startOf(a.head), a.steps, levels)
return p.renderAt(s, leaf, pins, key, env)
}
// namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the
// value each read there produced, by its pickKey, and the table rows they pinned.
type namedPick struct {
@@ -124,22 +193,6 @@ func readName(s *drawstate.State, env renderEnv, a arm) readValue {
return r
}
// readUnder reads a, an arm of t, once per pick: t renders in env.pick at env.pickAt, and a read of
// the name addresses the level a starts at. It keys the value by a's key under env.pickAt.
func readUnder(s *drawstate.State, t *template, env renderEnv, a arm) readValue {
p, key := env.pick, env.pickAt.under(a.key())
if r, done := p.memo.value[key]; done {
return r
}
levels := make([]pickKey, len(a.levels))
for i, l := range a.levels[:len(levels)-1] {
levels[i] = env.pickAt.under(l)
}
levels[len(levels)-1] = key
leaf, pins := p.draw(s, t.startOf(a.head), a.steps, levels)
return p.renderAt(s, leaf, pins, key, env)
}
// draw draws the path from start under p, keeping the variant drawn at each of levels, whose last
// is the leaf's key, and returns the leaf and the pins its row is in.
func (p *namedPick) draw(s *drawstate.State, start node, steps []pathStep, levels []pickKey) (node, *pinSet) {
+1 -1
View File
@@ -152,7 +152,7 @@ func readField(s *drawstate.State, t *template, env renderEnv, a arm) readValue
switch {
case a.kind == namedRead:
return readName(s, env, a)
case env.pick != nil && !grammar.IsRef(a.head) && env.keeps(a):
case keptInPick(env, a):
return readUnder(s, t, env, a)
}
env.pick = nil
-43
View File
@@ -2,8 +2,6 @@ package fejkdata
import (
"fmt"
"github.com/larvit/fejkdata/internal/grammar"
)
// templateSite is a template to resolve, with its folder and category, both empty for an inline
@@ -89,44 +87,3 @@ func resolveTemplates(ts []templateSite, root map[string]node) error {
}
return nil
}
// columnRead is a record's column read by a format that only reads one reference or name,
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not or t declares a string, which reads the column as text.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.datatype != nil && *t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
var start node
var category string
var tail []string
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
// {f as n}{n}: column f of the record binding n, in t's own category.
start, tail = b.binder, append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
// {/c as n}{n.x} or {/c.x as n}{n}: column x of category c.
start, category = targets[b].start, categoryOf(b.binder.refs.byName[b.ref].head)
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case grammar.IsRef(a.head):
// {/c.x}: column x of category c.
start, category, tail = t.startOf(a.head), categoryOf(a.head), a.tail
default:
return nil
}
if target, isTemplate := start.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+10
View File
@@ -10,3 +10,13 @@ func sortedNames[V any](m map[string]V) []string {
sort.Strings(names)
return names
}
func join(prefix, name string) string {
switch {
case prefix == "":
return name
case name == "":
return prefix
}
return prefix + "." + name
}
+3 -3
View File
@@ -74,7 +74,7 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil
if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.options); err != nil {
return nil, err
}
if err := t.checkCells(); err != nil {
if err := t.compileCells(); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err)
}
if err := t.compileRowFormat(o.format); err != nil {
@@ -118,8 +118,8 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) {
return o, nil
}
// checkCells compiles every cell carrying a token.
func (t *table) checkCells() error {
// compileCells compiles every cell carrying a token into cellTemplates.
func (t *table) compileCells() error {
header := t.rows.Header()
for row := 0; row < t.rows.Len(); row++ {
for col := range header {
+4 -34
View File
@@ -73,23 +73,17 @@
| 129 | 0.1.0 | decision | **Take a template wherever the library renders, so `fake:"{/sv_SE.person.first}"` and `Fake("{/sv_SE.person}")` render, and text with no `{…}` prints as written, as the CLI already does.** | 5 | 8 | 8 | 10 | 5.9, 7 | 25.0 |
| 163 | 0.1.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 |
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 10, 148 and 151 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 164 | 0.1.0 | | **Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.** | 2 | 2 | 5 | 7 | 4.1, 5.3 | 19.4 |
| 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 145 | 0.1.0 | | **Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.** | 2 | 3 | 1 | 9 | 3.4, 3.2 | 15.4 |
| 165 | 0.1.0 | | **Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 |
| 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 |
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
| 166 | 0.1.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 |
| 10 | 0.1.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 |
| 148 | 0.1.0 | | **State in `geo.py` the locality row each country's script hands it, and meet the fence that every parent row has a child row in one shared step.** | 2 | 3 | 2 | 6 | 2.1, 3.2 | 12.6 |
| 146 | 0.1.0 | | **Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.** | 1 | 2 | 1 | 6 | 3.1, 3.2 | 11.3 |
| 147 | 0.1.0 | | **Rename `namedNode`, `compiledPath.level` and `checkCells`, so "named" means bound to a name, "level" means only a prefix of a read's path, and `checkCells`' new name says it compiles.** | 1 | 2 | 1 | 6 | 3.3 | 11.3 |
| 151 | 0.1.0 | | **Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 |
| 149 | 0.1.0 | | **Say in one line what `config.load` does and what the 64-bit assertion asserts, beside their decision links.** | 1 | 1 | 1 | 5 | 3.4 | 9.8 |
| 150 | 0.1.0 | | **Move `columnRead` and `columnReadOf` from `resolve.go` into `datatype.go`, and state at `template.readsColumn` that `renderLeaf`, `proveColumnItem` and `itemDatatype` change with it.** | 1 | 2 | 1 | 5 | 3.1, 3.2 | 9.5 |
| 152 | 0.1.0 | | **Delete the doc lines that restate their code.** | 1 | 2 | 1 | 5 | 3.4 | 9.5 |
| 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 |
| 154 | 0.2.0 | | **Add `en_US.routing`, an ABA routing number over the Fed's prefix ranges with its check digit drawn by `{aba()}`.** | 2 | 2 | 6 | 7 | 2.2, 15 | 22.8 |
@@ -270,7 +264,7 @@ The README's Audience names a bulk fixture author writing millions of rows, and
Nothing has shipped, so "no longer paths", "where it used to fail" and "where it used to be an empty string" describe versions no reader can have installed.
### 153. Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.
### 153. Run the nine-seat comprehension panel and the scoring run again once items 10, 148 and 151 have merged, and file what the panel names.
The decision "comprehension floor: every dimension and the overall at 7.0 or above" holds the scores this run is measured against.
@@ -286,10 +280,6 @@ Split from item 129. cmd.exe's `echo` keeps its quotes and a trailing space, so
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs.
### 145. Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.
All 13 seats of the 2026-10-07 panel and scoring run on ad59967 again named the named-pick read path hardest, and twelve kept `doc.go`'s vocabulary open beside `pick.go`, `env.go` and `namefence.go` to follow it. Load builds a pick key in `levelKeys` (`arm.go`) and `addressedKeys` (`name.go`), and render builds the same key in `keeps` (`env.go`) and `readUnder` (`pick.go`); five seats named the two sides agreeing by hand as the unit they least want to change. `readUnder`'s loop stops one level short and its next line builds that level's key the same way, which one seat searched for a difference. Update the README's Layout block, and leave `doc.go` a one-line entry per term naming `pickKey`.
### 165. Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.
The maintainer ruled so on 2026-10-09. Today `bindOptions` in `internal/rows` refuses such a table, since `ambiguous`, which reports a name matching several rows, lists those rows by key. Afterwards `ambiguous` lists them by line, the header being line 1, and ends with "make the names unique, or add a key column".
@@ -314,30 +304,10 @@ Today nine, from `render` to `columnItems`, must agree, and only `render` and `r
Needed by item 61. The extender seat of the 2026-10-07 panel on ad59967, reading for item 61, rebuilt the row's keys, `name`, `municipality`, `population`, `lat`, `lon` and `codes`, from `geo-se.py` and `geo-us.py` side by side. It found the fence met two ways: `geo-se.py`'s `main` exits on a kommun with no locality, while `geo-us.py`'s `main` prunes counties and regions left with none.
### 146. Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.
Three architect seats of the 2026-10-07 panel and scoring run on ad59967 found that `sharedPaths` fills `choice.shared` in `node.go`, which `carriedByAll` reads to accept or refuse every path through a choice, so `List`'s file decides which paths compile; `join` serves `list.go`, `data.go`, `graph.go` and `path.go`.
### 147. Rename `namedNode`, `compiledPath.level` and `checkCells`, so "named" means bound to a name, "level" means only a prefix of a read's path, and `checkCells`' new name says it compiles.
`namedNode` in `graph.go` is a child and its segment, beside `namedRead` and `namedPick`, which go through a bound name; `compiledPath.level` is an error's label, where a level is a prefix of a read's path; `checkCells` compiles each cell and stores it in `cellTemplates`. Three seats of the 2026-10-07 panel and scoring run on ad59967 named the first two, and four named `checkCells`.
### 151. Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded.
`Fake` and `FakeRecord` each call `loadShippedAt` before they walk, and its doc asks the next entry point to do the same. The greenfield architect of the 2026-10-07 panel on ad59967 named it the one corner a new senior would break without warning. A test passing `WithDataPath` would not catch it, because then `New` loads every category.
### 149. Say in one line what `config.load` does and what the 64-bit assertion asserts, beside their decision links.
Five seats of the 2026-10-07 panel and scoring run on ad59967 met a bare decision link where a definition belonged: `config.load` in `fejkdata.go` has only the link as its doc, and `var _ [^uint(0)>>63 - 1]struct{}` only a link beside it.
### 150. Move `columnRead` and `columnReadOf` from `resolve.go` into `datatype.go`, and state at `template.readsColumn` that `renderLeaf`, `proveColumnItem` and `itemDatatype` change with it.
Two seats of the 2026-10-07 scoring run on ad59967 found `readsColumn` changing what `renderLeaf`, `proveColumnItem` and `itemDatatype` do, and found that by grep; `columnReadOf`, which sets it, sits in `resolve.go`, apart from all three.
### 152. Delete the doc lines that restate their code.
Named by seats of the 2026-10-07 panel and scoring run on ad59967: `Children` in `internal/rows/rows.go`, `spelled` in `name.go`, the line in `graph.go` saying a folder has no edges, the Dockerfile's "Module layer cached separately from source." and `compose.yaml`'s "Interactive shell for ad-hoc work".
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
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.
@@ -509,7 +479,7 @@ In `internal/rows`, `Pins.PinRow` names `t.path` and `Pins.MustRow` panics with
### 115. Spell a rows file's line one way in every error, naming the file.
Today a cell reads `g.t.b, line 3` (`namedNode.labelIn`), a cell check `t.tsv: line 3, b` (`checkCells`) and a link `t.tsv line 3` (`rows.Table.Link`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name.
Today a cell reads `g.t.b, line 3` (`containedNode.labelIn`), a cell check `t.tsv: line 3, b` (`compileCells`) and a link `t.tsv line 3` (`rows.Table.Link`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name.
### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`.
@@ -545,7 +515,7 @@ Both phone formats carry the same 21-code `area` list, so an edit to one copy ca
### 116. Turn a row index into its rows file's line in one function.
Today `namedNode`'s cell line in `graph.go`, `table.checkCells` and each site in `internal/rows` add the header line and the one-based count.
Today `containedNode`'s cell line in `graph.go`, `table.compileCells` and each site in `internal/rows` add the header line and the one-based count.
### 56. Build on a manual run of `test.yml`.