Gather the pick key's load and render sides in pick.go, and name each thing for what it is
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] }
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user