Give scope, binding, link, pick and head one meaning each: renderEnv, nodeSet, pipeline, resolvedRef, resolveTemplates, drawItem and a read's start

This commit is contained in:
2026-10-07 00:52:28 +02:00
committed by Lillem4n
parent dfa7271850
commit 98bb73f096
30 changed files with 301 additions and 300 deletions
+4 -4
View File
@@ -961,20 +961,20 @@ table.go tables: their options, format and cells, and the link to the par
name.go names: {x as n} bindings, the scopes they live in, and the reads they answer
namefence.go the load fences over names
pick.go a render's named picks: the frames of the name scopes rendering, the memo a pick keeps, and the reads kept under it
scope.go the scope a render reads names and rows through
env.go the env a render reads names and rows through
path.go the dotted-path walks, and proving a path resolves
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
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and bind
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and resolve
format.go a format's tokens checked against their fields, and compiled
reference.go resolving references across the tree
link.go linking templates to the assembled tree in steps, each over every template before the next
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
calc.go the {calc()} operand checks: never a number, and a constant zero divisor
datatype.go column datatypes: DataType, where datatype and null may sit, a column's datatype
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 bind pipeline every entry point runs
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
internal/builtinfunc/ the {name()} functions: their checks and draws, checksums, date and time layouts, transforms and calc arithmetic
internal/datafiles/ the walk of a data tree: its folders, its category files and the rows files beside them
+2 -2
View File
@@ -12,7 +12,7 @@ import (
)
// checkCalcFields runs checkOperands over the calc operands fields hold. An operand no field
// holds is left for a name, which checkCalcNames checks once names link.
// holds is left for a name, which checkCalcNames checks once names resolve.
func checkCalcFields(args []string, fields map[string]node) error {
return checkOperands(args[0], builtinfunc.ParsedCalc(args[0]), func(name string) []node {
if n, ok := fields[name]; ok {
@@ -35,7 +35,7 @@ func checkCalcNames(label string, t *template) error {
return nil
}
// operandNodes lists every node each operand of o may render, once its template and names link.
// operandNodes lists every node each operand of o may render, once its template and names resolve.
func operandNodes(o op) func(name string) []node {
return func(name string) []node {
a := o.operands[slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })]
+28 -28
View File
@@ -19,7 +19,7 @@ func loadData(sources []datafiles.Source) (map[string]node, error) {
if len(root) == 0 {
return nil, fmt.Errorf("no .json data found")
}
if err := categoryBinding(categorySites(&folder{children: root}), root).bind(); err != nil {
if err := categoryPipeline(categorySites(&folder{children: root}), root).run(); err != nil {
return nil, err
}
return root, nil
@@ -66,9 +66,9 @@ func mergeChildren(dst, src map[string]node) {
}
}
// nodeScope is the set of nodes one validation pass covers: the categories one load
// nodeSet is the set of nodes one validation pass covers: the categories one load
// binds, or a single inline node.
type nodeScope func(fn func(label string, n node) error) error
type nodeSet func(fn func(label string, n node) error) error
// categorySite is a loaded category and where it sits: the folder holding it, that
// folder's path, and its own dot path.
@@ -103,7 +103,7 @@ func siteIn(dir []string, in *folder, name string) categorySite {
// categoryPath is the dot path of the category name in the folder dir.
func categoryPath(dir []string, name string) string { return join(strings.Join(dir, "."), name) }
func sitesScope(sites []categorySite) nodeScope {
func siteNodes(sites []categorySite) nodeSet {
return func(fn func(label string, n node) error) error {
for _, s := range sites {
if err := eachNode(s.n, s.path, fn); err != nil {
@@ -114,54 +114,54 @@ func sitesScope(sites []categorySite) nodeScope {
}
}
func inlineScope(n node, label string) nodeScope {
func inlineNodes(n node, label string) nodeSet {
return func(fn func(label string, m node) error) error { return eachNode(n, label, fn) }
}
// binding is one scope's way through bind, the one load pipeline; its fields are where
// scopes differ.
type binding struct {
scope nodeScope
link func() error
// pipeline is the one load pipeline, run over one nodeSet; its fields are where
// entry points differ.
type pipeline struct {
nodes nodeSet
resolve func() error
// Set by a caller that proves the columns against their Go types itself.
typedByGo bool
}
// categoryBinding binds the categories of one load, their references resolving
// against root.
func categoryBinding(sites []categorySite, root map[string]node) binding {
return binding{
scope: sitesScope(sites),
link: func() error {
// categoryPipeline is the pipeline over the categories of one load, their references
// resolving against root.
func categoryPipeline(sites []categorySite, root map[string]node) pipeline {
return pipeline{
nodes: siteNodes(sites),
resolve: func() error {
if err := linkTables(sites); err != nil {
return err
}
return linkCategories(sites, root)
return resolveCategoryTemplates(sites, root)
},
}
}
func (b binding) bind() error {
if err := b.link(); err != nil {
func (p pipeline) run() error {
if err := p.resolve(); err != nil {
return err
}
if err := checkNoCycles(b.scope); err != nil {
if err := checkNoCycles(p.nodes); err != nil {
return err
}
settleRecords(b.scope)
if !b.typedByGo {
if err := checkColumns(b.scope); err != nil {
settleRecords(p.nodes)
if !p.typedByGo {
if err := checkColumns(p.nodes); err != nil {
return err
}
}
return checkNodeFences(b.scope)
return checkNodeFences(p.nodes)
}
// checkNodeFences runs the per-node fences every binding needs over a scope, each
// over the whole scope before the next, so which of several broken nodes is reported
// does not depend on the walk. Its walks recurse unguarded, so bind refuses the scope's cycles
// checkNodeFences runs the per-node fences every pipeline runs over its nodes, each
// over all of them before the next, so which of several broken nodes is reported
// does not depend on the walk. Its walks recurse unguarded, so run refuses their cycles
// first.
func checkNodeFences(s nodeScope) error {
func checkNodeFences(s nodeSet) error {
mem := renderCounts{}
if err := s(func(label string, n node) error { return repeatCheck(label, n, mem) }); err != nil {
return err
+1 -1
View File
@@ -57,7 +57,7 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) {
// checkColumns rejects a record column whose items hold different datatypes, checking a column
// after the columns its items read, so a column read is named before its readers.
func checkColumns(s nodeScope) error {
func checkColumns(s nodeSet) error {
checked := map[node]bool{}
var check func(label, name string, column node) error
check = func(label, name string, column node) error {
+9 -3
View File
@@ -16,20 +16,26 @@ package fejkdata
// Vocabulary
//
// - draw — one pick from a choice, or one row taken from a table: `pick`,
// `resolveChoice`, `renderScope.drawRowOf`. Every {…} draws afresh, bar a read of a name.
// - draw — one item taken from a choice, or one row from a table: `drawItem`,
// `drawThroughChoices`, `renderEnv.drawRowOf`. Every {…} draws afresh, bar a read of a name.
// - expansion — one render of one format: `expand`.
// - render — one walk of the tree from an entry point, over one `frameStack`:
// `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake`
// start one each, and `FakeStruct` one per record.
// - env — what a render reads names and rows through: `renderEnv`.
// - named pick — the draw a token such as {/person as p} binds to a name, drawn
// on its first read and kept while its scope renders: a category, or one
// repeat iteration: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`.
// A scope is only ever a name's.
// - memo — what a named pick keeps its draws in, the variant drawn at each level
// and the value each path read: `drawMemo`.
// - pin — fixing which row of a table one path, or one named pick, uses:
// `pinSet`, `namedPick.pins`, `table.drawStep`.
// - family — a table and every table reaching it through a chain of parent
// links (`table.linkParent`).
// links (`table.linkParent`). Only a table links.
// - resolve — tying each template's references and names to the merged tree:
// `resolveTemplates`.
// - head — a read's first segment, a string: `arm.head`. The node it names is
// the read's start: `template.startOf`, `nameTarget.start`.
// - fence — a load-time check, run over each category, template or struct as it
// loads or compiles, so rendering a compiled tree cannot fail.
+2 -2
View File
@@ -795,7 +795,7 @@ the direct chain is the one a reader can predict from the tables' parents.
2026-09-30, larv-review on the comprehension round the maintainer approved on 2026-09-29;
replaces "The path walks are separate loops".
Goals 3 and 13: `drawSteps` draws every path from its steps, compiled at link by
Goals 3 and 13: `drawSteps` draws every path from its steps, compiled at resolve by
`pathCheck` or, for a caller's path, by `probePath` into a stack buffer. Both take each
step through `takeStep`, and differ only at a choice: the check walks every variant, the
probe the first. On 2026-09-29 one loop over all three walks, switching on a mode field,
@@ -839,7 +839,7 @@ Last scoring run, 2026-10-05, on commit bcc6858: Navigation 7.00, Locality 5.75,
2026-10-05, Lilleman auf Larv. Serves goals 3.2 and 3.4. Two architect reviews found the
engine's types, `template`, `table`, `arm`, `op` and `nameBinding`, bound in one cycle
because several passes each write part of the same structs; the template language's
recursion reads few of those types. Moving the engine while several link passes still
recursion reads few of those types. Moving the engine while several resolve passes still
wrote each value would have spread that knot across packages, behind forwarding calls from
every public method. The draw state, the grammar, `DataType`, what a proof knows of a
value, the builtins, the reading of data files and a table's rows read no engine type, so
+12 -12
View File
@@ -8,11 +8,11 @@ type readValue struct {
null bool
}
// renderScope is where a render reads: the frames of the name scopes rendering; and row, the row
// renderEnv is where a render reads: the frames of the name scopes rendering; and row, the row
// of the table rendering, which its columns read. It passes by value, and what is read from it
// reaches a map key, an interface or a func value only as a copy; else sc, and with it every
// reaches a map key, an interface or a func value only as a copy; else env, and with it every
// render's frames, moves to the heap.
type renderScope struct {
type renderEnv struct {
frames *frameStack
row renderedRow
// base is the depth of the frame stack when the read rendering the category entered it,
@@ -28,22 +28,22 @@ type renderedRow struct {
index int
}
// at returns the scope n renders in, n being the leaf of a path whose rows sit in pins; where n
// is a table's row or column, the scope carries the row pins holds for that table.
func (sc renderScope) at(n node, pins *pinSet) renderScope {
// at returns the env n renders in, n being the leaf of a path whose rows sit in pins; where n
// is a table's row or column, the env carries the row pins holds for that table.
func (env renderEnv) at(n node, pins *pinSet) renderEnv {
switch n := n.(type) {
case *tableRow:
sc.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
env.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
case *tableColumn:
sc.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
env.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
}
return sc
return env
}
// rowOf is the row of t its columns render from.
func (sc renderScope) rowOf(t *table) int {
if sc.row.t != t {
func (env renderEnv) rowOf(t *table) int {
if env.row.t != t {
panic(invariant.Broken("a column of %s renders in a scope holding no row of it", t.rows.Segment()))
}
return sc.row.index
return env.row.index
}
+1 -1
View File
@@ -118,7 +118,7 @@ func linked(t *testing.T, s string) (node, error) {
if err != nil {
return nil, err
}
return n, linkInline(inlineScope(n, "template"), nil)
return n, resolveInlineTemplates(inlineNodes(n, "template"), nil)
}
// compiled parses, compiles and links a JSON fragment into a node.
+17 -17
View File
@@ -83,7 +83,7 @@ func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unbou
if _, _, err := grammar.RefShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.Body, err)
}
continue // resolveLink checks its target once the tree is assembled
continue // resolveRefs checks its target once the tree is assembled
}
missing, err := checkArm(name, fields, !operands && len(names) == 1)
if err == nil {
@@ -120,7 +120,7 @@ func checkBind(t grammar.Token, fields map[string]node) error {
return nil
}
// checkBound proves what a binding picks is a reference or a path into a field of the binding
// checkBound proves what a binding names is a reference or a path into a field of the binding
// template.
func checkBound(ref string, fields map[string]node) error {
if grammar.IsRef(ref) {
@@ -180,9 +180,9 @@ func tokenReads(t grammar.Token) []string {
return t.Arms
}
// checkNoRepeatedArm rejects {a|a|b}: an alternation picks its arms evenly, so a
// checkNoRepeatedArm rejects {a|a|b}: an alternation draws its arms evenly, so a
// repeated one is a second spelling of weight. The error names the spelling that
// does skew a pick.
// does skew a draw.
func checkNoRepeatedArm(body string, names []string) error {
if len(names) < 2 {
return nil
@@ -197,8 +197,8 @@ func checkNoRepeatedArm(body string, names []string) error {
return nil
}
// arm is one alternative of a {a|b} token or one operand, split into the head
// `template.head` resolves and the tail of a dotted path into it.
// arm is one alternative of a {a|b} token or one operand, split into the head,
// whose node `template.startOf` finds, and the tail of a dotted path into it.
type arm struct {
spelling string // as written, for messages
head string
@@ -220,8 +220,8 @@ const (
)
// splitArm splits one name into head and tail. refs maps a reference to what
// resolveLink resolved it to; before linking, a reference is whole.
func splitArm(name string, refs map[string]refBinding) arm {
// resolveRefs resolved it to; before that, a reference is whole.
func splitArm(name string, refs map[string]resolvedRef) arm {
if grammar.IsRef(name) {
b, linked := refs[name]
if !linked || len(b.tail) == 0 {
@@ -231,7 +231,7 @@ func splitArm(name string, refs map[string]refBinding) arm {
}
return arm{spelling: name, head: head, levels: []pickKey{pickKey(head)}}
}
sigil, rest, _ := grammar.RefShape(name) // resolveLink proved it, and took b.tail as a suffix of its segments
sigil, rest, _ := grammar.RefShape(name) // resolveRefs proved it, and took b.tail as a suffix of its segments
written, _ := grammar.SplitPath(rest)
return pathArm(name, b.head, sigil+grammar.JoinSegments(written[:len(written)-len(b.tail)]), b.tail)
}
@@ -282,8 +282,8 @@ type formatOps struct {
grow int
}
// compileFormat compiles t's parsed format against its link and its names' targets; a {ref as n}
// token compiles to no op. checkTokens proved every token valid.
// compileFormat compiles t's parsed format against its resolved references and its names'
// targets; a {ref as n} token compiles to no op. checkTokens proved every token valid.
func compileFormat(t *template, targets map[*nameBinding]nameTarget) (formatOps, error) {
var c formatOps
for _, tok := range t.tokens {
@@ -326,13 +326,13 @@ func (t *template) compileArms(names []string, targets map[*nameBinding]nameTarg
// compileArm compiles one read into a path: from the head it names, or, for a read through a
// name, from what that name binds.
func (t *template) compileArm(name string, targets map[*nameBinding]nameTarget) (arm, error) {
a := splitArm(name, t.link.refs)
a := splitArm(name, t.refs.byName)
if !t.isName(a.head) {
head := t.head(a.head)
if head == nil {
start := t.startOf(a.head)
if start == nil {
panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling))
}
w := compilePath(head, a.tail)
w := compilePath(start, a.tail)
a.steps, a.leaves = w.steps, w.leaves
return a, nil
}
@@ -342,10 +342,10 @@ func (t *template) compileArm(name string, targets map[*nameBinding]nameTarget)
}
target := targets[b]
full := append(target.tail[:len(target.tail):len(target.tail)], a.tail...)
if err := checkPathResolves(target.head, full, a.head); err != nil {
if err := checkPathResolves(target.start, full, a.head); err != nil {
return a, err
}
w := compilePath(target.head, full)
w := compilePath(target.start, full)
a.kind, a.named, a.steps, a.leaves = namedRead, b, w.steps, w.leaves
a.levels = levelKeys(full, 0)
return a, nil
+1 -1
View File
@@ -62,7 +62,7 @@ func TestTokenSubstitution(t *testing.T) {
}
}
func TestAlternationPicksOneField(t *testing.T) {
func TestAlternationDrawsOneField(t *testing.T) {
f := engine(3)
seen := map[string]bool{}
for i := 0; i < 100; i++ {
+2 -2
View File
@@ -178,7 +178,7 @@ func repeatCheck(label string, n node, mem renderCounts) error {
// graph (renderEdges). Every node is a root: a field its parent's format never
// renders is still reachable by dot path, so a cycle in one would otherwise reach
// render and be fatal there.
func checkNoCycles(s nodeScope) error {
func checkNoCycles(s nodeSet) error {
const (
grey = 1
black = 2
@@ -195,7 +195,7 @@ func checkNoCycles(s nodeScope) error {
var visit func(n node, label string) error
visit = func(n node, label string) error {
if !inScope[n] {
return nil // loaded before this scope and proven then, so it never reaches back into it
return nil // loaded before these nodes and proven then, so it never reaches back into it
}
switch color[n] {
case grey:
+14 -14
View File
@@ -26,7 +26,7 @@ func (t *Template) Fake() string {
}
// NewTemplate compiles an inline template — a format string or a JSON value — and
// binds its references against the loaded tree, so repeated renders pay the
// resolves its references against the loaded tree, so repeated renders pay the
// compile and validation once. It shares [New]'s guarantees: a bad template errors
// here, and rendering cannot fail.
func (f *Generator) NewTemplate(input string) (*Template, error) {
@@ -36,7 +36,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
}
f.mu.Lock()
defer f.mu.Unlock()
if err := bindInline(&f.root, n, "template", false); err != nil {
if err := loadInline(&f.root, n, "template", false); err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
return &Template{g: f, n: n}, nil
@@ -87,24 +87,24 @@ func inputValue(input string) (any, error) {
return raw, nil
}
// bindInline loads the shipped categories n reads, then binds n against root.
func bindInline(root *folder, n node, label string, typedByGo bool) error {
scope := inlineScope(n, label)
if err := refuseFolderRefs(scope); err != nil {
// loadInline loads the shipped categories n reads, then runs n through the pipeline against root.
func loadInline(root *folder, n node, label string, typedByGo bool) error {
nodes := inlineNodes(n, label)
if err := refuseFolderRefs(nodes); err != nil {
return err
}
loadShipped(root, unloadedReads(root, nil, scope))
return binding{
scope: scope,
link: func() error { return linkInline(scope, root.children) },
loadShipped(root, unloadedReads(root, nil, nodes))
return pipeline{
nodes: nodes,
resolve: func() error { return resolveInlineTemplates(nodes, root.children) },
typedByGo: typedByGo,
}.bind()
}.run()
}
// refuseFolderRefs refuses each reference in scope not written from the root, {/x}: an
// refuseFolderRefs refuses each reference in nodes not written from the root, {/x}: an
// inline node sits in no folder.
func refuseFolderRefs(scope nodeScope) error {
return scope(func(label string, m node) error {
func refuseFolderRefs(nodes nodeSet) error {
return nodes(func(label string, m node) error {
t, ok := m.(*template)
if !ok {
return nil
+1 -1
View File
@@ -400,7 +400,7 @@ func (t *Table[O]) Draw(s *drawstate.State) int {
return s.Weighted(t.cum)
}
// drawUnder picks a row among those linked to parent row pr.
// drawUnder draws a row among those linked to parent row pr.
func (t *Table[O]) drawUnder(s *drawstate.State, pr int) int {
lookup := t.builtLookup()
k := t.parent.Cell(pr, t.parent.keyIndex)
+1 -1
View File
@@ -29,7 +29,7 @@ type nameBinding struct {
index int
binder *template
// Filled by `linkTemplates`, once every check of the link passed:
// Filled by `resolveTemplates`, once every check it runs passed:
nameTarget
// addressed is every key a read of the name lands on or passes, the spelling of the
// first read reaching it beside it; a pick keeps the draws at these keys, and only these.
+1 -1
View File
@@ -428,7 +428,7 @@ func TestAFieldBindingReadOutsideWhatItBindsLoads(t *testing.T) {
}
}
func TestANameReadBeforeItsBinderLinks(t *testing.T) {
func TestANameReadBeforeItsBinderKeepsOnePick(t *testing.T) {
dir := writeData(t, map[string]string{
"person": `{"format":"{first} {last}","first":["Ada","Bo","Cy"],"last":["Byron","Ek","Lind"]}`,
"row": `{"format":"{a}|{b}","a":"{p}","b":"{/person as p}{p.first} {p.last}"}`,
+3 -3
View File
@@ -12,7 +12,7 @@ import (
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
// field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one
// draw of w.
func checkNameReads(label string, t *template, names linkedNames) error {
func checkNameReads(label string, t *template, names resolvedNames) error {
for _, tok := range t.tokens {
if tok.Kind != grammar.NameBind {
continue
@@ -24,7 +24,7 @@ func checkNameReads(label string, t *template, names linkedNames) error {
}
for _, r := range namedReads(t) {
b, target := r.a.named, names.targets[r.a.named]
if b.bindsField() && rendersInside(compilePath(target.head, target.tail).leaves, t) {
if b.bindsField() && rendersInside(compilePath(target.start, target.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", label, r.o.Body, b.name, b.ref)
}
for _, leaf := range r.a.leaves {
@@ -69,7 +69,7 @@ func rendersInside(nodes []node, t *template) bool {
// or tag of one reference alone, that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses(uses []nameUse) error {
if len(uses) == 0 {
panic(invariant.Broken("name %q has no read at link, though its compile found one", b.name))
panic(invariant.Broken("name %q has no read at resolve, though its compile found one", b.name))
}
r := uses[0]
if len(uses) > 1 || r.nested {
+15 -15
View File
@@ -26,8 +26,8 @@ type folder struct {
func (*folder) isNode() {}
// choice picks one of its items. cum holds cumulative weights for a weighted
// pick; when nil the choice is uniform and selection is O(1). shared is the set of
// choice draws one of its items. cum holds cumulative weights for a weighted
// draw; when nil the choice is uniform and selection is O(1). shared is the set of
// relative dot paths every item can address, so carriedByAll and List both read the one
// answer to what a path may reach through this choice.
type choice struct {
@@ -47,7 +47,7 @@ func (*nullItem) isNode() {}
// template renders a format string, substituting {tokens} from fields. A bare
// JSON string is a template with no fields. repeat (default 1) renders that format
// that many times and joins the results with separator (default ""), each render
// an independent pick. Every format compiles in `linkTemplates`.
// an independent pick. Every format compiles in `resolveTemplates`.
type template struct {
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
format string
@@ -63,8 +63,8 @@ type template struct {
// Filled by `bindNames`, from the compiled category:
nameScope *nameScope // where its tokens look a name up
// Filled by `linkTemplates`, from the assembled tree:
link templateLink
// 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
@@ -72,18 +72,18 @@ type template struct {
columns []recordColumn // a record's, where it has fields, in name order
}
// templateLink is what a template resolves to in the assembled tree.
type templateLink struct {
refs map[string]refBinding // each reference the format reads -> what it resolves to
refHeads map[string]node // each refBinding.head -> the category it names
// templateRefs is what a template resolves to in the assembled tree.
type templateRefs struct {
byName map[string]resolvedRef // each reference the format reads -> what it resolves to
categories map[string]node // each resolvedRef.head -> the category it names
}
func (*template) isNode() {}
// head is the node an arm's head names: a sibling field, or a reference's category.
func (t *template) head(name string) node {
// startOf is the node an arm's startOf names: a sibling field, or a reference's category.
func (t *template) startOf(name string) node {
if grammar.IsRef(name) {
return t.link.refHeads[name]
return t.refs.categories[name]
}
return t.fields[name]
}
@@ -262,7 +262,7 @@ func compileChoice(items []any, pos position) (node, error) {
}
c.items[i] = n
}
if weighted { // uniform choices skip the weight table and pick in O(1)
if weighted { // uniform choices skip the weight table and draw in O(1)
if math.IsInf(total, 1) {
return nil, fmt.Errorf("choice weights must sum to a finite number, got %v", total)
}
@@ -272,9 +272,9 @@ func compileChoice(items []any, pos position) (node, error) {
return c, nil
}
// checkNoRepeatedItem rejects a choice that lists one item twice: a pick is even
// checkNoRepeatedItem rejects a choice that lists one item twice: a draw is even
// over the items, so a repeat is a second spelling of weight. The error names the
// spelling that does skew a pick.
// spelling that does skew a draw.
func checkNoRepeatedItem(items []any) error {
seen := make(map[string]int, len(items))
for i, raw := range items {
+4 -4
View File
@@ -81,7 +81,7 @@ func TestNodeCompileErrors(t *testing.T) {
}
}
func TestEveryFormatCompilesAtLink(t *testing.T) {
func TestEveryFormatCompilesAtResolve(t *testing.T) {
n, err := compile(parse(t, `{"format":"{a} {b}","a":{"format":"x{c}","c":["1","2"]},"b":"{/w}"}`))
if err != nil {
t.Fatal(err)
@@ -104,15 +104,15 @@ func TestEveryFormatCompilesAtLink(t *testing.T) {
}
for format, has := range compiledByPath() {
if has {
t.Errorf("%s compiled before link", format)
t.Errorf("%s compiled before resolve", format)
}
}
if err := linkInline(inlineScope(n, "t"), map[string]node{"w": w}); err != nil {
if err := resolveInlineTemplates(inlineNodes(n, "t"), map[string]node{"w": w}); err != nil {
t.Fatal(err)
}
for format, has := range compiledByPath() {
if !has {
t.Errorf("%s not compiled at link", format)
t.Errorf("%s not compiled at resolve", format)
}
}
}
+3 -3
View File
@@ -73,7 +73,7 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
return tableRoute{}, err
}
// A selector further down pins this table by ancestry, so the walk draws only
// where none follows; drawing first could pick a row the selector is not inside.
// where none follows; drawing first could draw a row the selector is not inside.
r := tableRoute{sel: sel, draw: sel == "" && (descended || len(tail) > 0) && !grammar.HasSelector(tail)}
switch {
case len(tail) == 0 && sel == "" && !descended:
@@ -89,7 +89,7 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
}
// pathStep is one step of a compiled path, taken at the node the steps before it
// reached, a choice there resolved first; at indexes the tail where it is taken, and
// reached, a choice there drawn first; at indexes the tail where it is taken, and
// only a table's step sits past the tail's end. A step holds no node, so a
// caller's steps stay on its stack.
type pathStep struct {
@@ -315,7 +315,7 @@ func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node {
func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []pickKey, at int) node {
if memo == nil {
return resolveChoice(s, c)
return drawThroughChoices(s, c)
}
return memo.variantOf(s, c, levels[at])
}
+46 -46
View File
@@ -11,7 +11,7 @@ import (
type pickKey string
// under is rel, a key from a fresh read's levels, moved under k, the key of what renders. It never
// returns k itself: on a render k is sc.pickAt, and a memo keeping it would move every render's scope to the heap.
// returns k itself: on a render k is env.pickAt, and a memo keeping it would move every render's env to the heap.
func (k pickKey) under(rel pickKey) pickKey {
if k == "" {
return rel
@@ -69,9 +69,9 @@ func newPickFrame(scope *nameScope) *pickFrame {
return f
}
// frameStack is the frames of the name scopes rendering, innermost last. A render's scope points
// at it, and a repeat iteration's shares it: holding the frames in a renderScope would move every
// render's scope to the heap.
// frameStack is the frames of the name scopes rendering, innermost last. A render's env points
// at it, and a repeat iteration's shares it: holding the frames in a renderEnv would move every
// render's env to the heap.
type frameStack struct {
frames []*pickFrame
}
@@ -88,20 +88,20 @@ func (st *frameStack) pop(mark int) { st.frames = st.frames[:mark] }
// enter starts a read landing on n: the read sees no frame opened before it, and gets a frame for
// each name scope around n, from memo where there is one, so reads sharing memo read one pick of
// each name. It returns the mark that closes those frames.
func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) {
sc = sc.entering()
func (env renderEnv) enter(n node, memo *drawMemo) (renderEnv, int) {
env = env.entering()
for scope := scopeAround(n); scope != nil; scope = scope.up {
if len(scope.order) > 0 {
sc.frames.push(memo.enteredFrame(scope))
env.frames.push(memo.enteredFrame(scope))
}
}
return sc, sc.base
return env, env.base
}
// entering is sc as a read entering a category sees it: no frame opened before it, and no pick.
func (sc renderScope) entering() renderScope {
sc.base, sc.pick = len(sc.frames.frames), nil
return sc
// entering is env as a read entering a category sees it: no frame opened before it, and no pick.
func (env renderEnv) entering() renderEnv {
env.base, env.pick = len(env.frames.frames), nil
return env
}
// scopeAround is the innermost name scope a render of n reads names in, short of the frames n
@@ -136,18 +136,18 @@ func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame {
// renderFrame opens a fresh frame of t's scope, where t binds names and no read entering the
// category opened one, returning the mark that closes it, or -1.
func (sc renderScope) renderFrame(t *template) int {
func (env renderEnv) renderFrame(t *template) int {
own := t.ownScope()
if own == nil || sc.frameOf(own) != nil {
if own == nil || env.frameOf(own) != nil {
return -1
}
return sc.frames.push(newPickFrame(own))
return env.frames.push(newPickFrame(own))
}
// frameOf is the frame of scope rendering since the read entering the category, nil where none is.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
for i := len(sc.frames.frames) - 1; i >= sc.base; i-- {
if f := sc.frames.frames[i]; f.scope == scope {
func (env renderEnv) frameOf(scope *nameScope) *pickFrame {
for i := len(env.frames.frames) - 1; i >= env.base; i-- {
if f := env.frames.frames[i]; f.scope == scope {
return f
}
}
@@ -160,8 +160,8 @@ func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
// So every read through one pick sees one pick of each name the other category binds:
// {a.street} and {a.postal-code} in data/sv_SE/address.json read one pick of the name l
// that data/geo/SE/address.json binds.
func readName(s *drawstate.State, sc renderScope, a arm) readValue {
f := sc.frameOf(a.named.scope)
func readName(s *drawstate.State, env renderEnv, a arm) readValue {
f := env.frameOf(a.named.scope)
if f == nil {
panic(invariant.Broken("name %q is read where no frame of its scope renders", a.named.name))
}
@@ -169,50 +169,50 @@ func readName(s *drawstate.State, sc renderScope, a arm) readValue {
if r, done := p.memo.value[a.key()]; done {
return r
}
leaf, pins := p.draw(s, a.named.head, a.steps, a.levels)
leaf, pins := p.draw(s, a.named.start, a.steps, a.levels)
if a.named.bindsField() {
return p.renderAt(s, leaf, pins, a.key(), sc)
return p.renderAt(s, leaf, pins, a.key(), env)
}
sc, mark := sc.enter(leaf, &p.memo)
r := p.renderAt(s, leaf, pins, a.key(), sc)
sc.frames.pop(mark)
env, mark := env.enter(leaf, &p.memo)
r := p.renderAt(s, leaf, pins, a.key(), env)
env.frames.pop(mark)
return r
}
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
func (sc renderScope) drawRowOf(s *drawstate.State, t *table) int {
if sc.pick != nil {
return t.rows.DrawIn(s, &sc.pick.pins)
func (env renderEnv) drawRowOf(s *drawstate.State, t *table) int {
if env.pick != nil {
return t.rows.DrawIn(s, &env.pick.pins)
}
return t.rows.Draw(s)
}
// keeps reports whether a read of sc's name addresses the level a starts at.
func (sc renderScope) keeps(a arm) bool {
_, kept := sc.pick.named.addressed[sc.pickAt.under(a.levels[0])]
// 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
}
// readUnder reads a, an arm of t, once per pick: t renders in sc.pick at sc.pickAt, and a read of
// the name addresses the level a starts at. It keys the value by a's key under sc.pickAt.
func readUnder(s *drawstate.State, t *template, sc renderScope, a arm) readValue {
p, key := sc.pick, sc.pickAt.under(a.key())
// 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] = sc.pickAt.under(l)
levels[i] = env.pickAt.under(l)
}
levels[len(levels)-1] = key
leaf, pins := p.draw(s, t.head(a.head), a.steps, levels)
return p.renderAt(s, leaf, pins, key, sc)
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 head under p, keeping the variant drawn at each of levels, whose last
// 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, head node, steps []pathStep, levels []pickKey) (node, *pinSet) {
leaf, pins := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
func (p *namedPick) draw(s *drawstate.State, start node, steps []pathStep, levels []pickKey) (node, *pinSet) {
leaf, pins := drawSteps(s, start, steps, &p.pins, &p.memo, levels)
if c, isChoice := leaf.(*choice); isChoice {
leaf = p.memo.variantOf(s, c, levels[len(levels)-1])
}
@@ -220,10 +220,10 @@ func (p *namedPick) draw(s *drawstate.State, head node, steps []pathStep, levels
}
// renderAt renders leaf, drawn at key into pins, as part of p, and keeps what it rendered.
func (p *namedPick) renderAt(s *drawstate.State, leaf node, pins *pinSet, key pickKey, sc renderScope) readValue {
sc = sc.at(leaf, pins)
sc.pick, sc.pickAt = p, key
r := renderLeaf(s, leaf, sc)
func (p *namedPick) renderAt(s *drawstate.State, leaf node, pins *pinSet, key pickKey, env renderEnv) readValue {
env = env.at(leaf, pins)
env.pick, env.pickAt = p, key
r := renderLeaf(s, leaf, env)
if p.memo.value == nil {
p.memo.value = map[pickKey]readValue{}
}
@@ -235,7 +235,7 @@ func (p *namedPick) renderAt(s *drawstate.State, leaf node, pins *pinSet, key pi
func (m *drawMemo) variantOf(s *drawstate.State, c *choice, level pickKey) node {
n, drew := m.variant[level]
if !drew {
n = resolveChoice(s, c)
n = drawThroughChoices(s, c)
if m.variant == nil {
m.variant = map[pickKey]node{}
}
+16 -16
View File
@@ -135,9 +135,9 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
var frames frameStack
sc := renderScope{frames: &frames}
env := renderEnv{frames: &frames}
if t, isTable := n.(*table); isTable {
if n, sc.row, err = tableRecord(f.drawState, t, tail); err != nil {
if n, env.row, err = tableRecord(f.drawState, t, tail); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
} else if len(tail) > 0 {
@@ -151,7 +151,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, err)
}
return renderRecord(f.drawState, record, sc), nil
return renderRecord(f.drawState, record, env), nil
}
// tableRecord walks a path's tail from a table to the table whose row is the record, and that
@@ -184,11 +184,11 @@ func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock()
defer t.g.mu.Unlock()
var frames frameStack
return renderRecord(t.g.drawState, t.template, renderScope{frames: &frames})
return renderRecord(t.g.drawState, t.template, renderEnv{frames: &frames})
}
// NewRecordTemplate compiles an inline record — a JSON object with a format and
// fields — and binds its references against the loaded tree.
// fields — and resolves its references against the loaded tree.
func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) {
t, err := f.NewTemplate(input)
if err != nil {
@@ -246,10 +246,10 @@ type recordColumn struct {
boundWhole *nameBinding
}
// settleRecords fixes the columns of every record in scope. bind runs it after checkNoCycles: a
// settleRecords fixes the columns of every record in nodes. The pipeline runs it after checkNoCycles: a
// column's datatype walks the column, and a cycle would walk forever.
func settleRecords(scope nodeScope) {
_ = scope(func(_ string, n node) error {
func settleRecords(nodes nodeSet) {
_ = nodes(func(_ string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
t.columns = recordColumns(t)
}
@@ -267,8 +267,8 @@ func recordColumns(t *template) []recordColumn {
if tok.Kind != grammar.NameBind {
continue
}
if b := t.nameScope.bindings[tok.Bound]; b.bindsField() && len(b.tail) == 0 && whole[b.head] == nil {
whole[b.head] = b
if b := t.nameScope.bindings[tok.Bound]; b.bindsField() && len(b.tail) == 0 && whole[b.start] == nil {
whole[b.start] = b
}
}
names := sortedNames(t.fields)
@@ -281,18 +281,18 @@ func recordColumns(t *template) []recordColumn {
}
// renderRecord draws each column of t once, in name order, as one render, so the columns read one
// pick of each name; a table's columns read sc's row.
func renderRecord(s *drawstate.State, t *template, sc renderScope) *Record {
if mark := sc.renderFrame(t); mark >= 0 {
defer sc.frames.pop(mark)
// pick of each name; a table's columns read env's row.
func renderRecord(s *drawstate.State, t *template, env renderEnv) *Record {
if mark := env.renderFrame(t); mark >= 0 {
defer env.frames.pop(mark)
}
r := &Record{columns: make([]Column, len(t.columns))}
for i, c := range t.columns {
var column readValue
if c.boundWhole != nil {
column = readName(s, sc, arm{kind: namedRead, named: c.boundWhole, levels: wholeLevels})
column = readName(s, env, arm{kind: namedRead, named: c.boundWhole, levels: wholeLevels})
} else {
column = renderLeaf(s, c.field, sc)
column = renderLeaf(s, c.field, env)
}
r.columns[i] = Column{Name: c.name, DataType: c.datatype, Value: column.text, Null: column.null}
}
+15 -15
View File
@@ -7,9 +7,9 @@ import (
"github.com/larvit/fejkdata/internal/grammar"
)
// refBinding is what a reference resolves to: the head its category is held
// resolvedRef is what a reference resolves to: the head its category is held
// under, and the tail read into it.
type refBinding struct {
type resolvedRef struct {
head string
tail []string
}
@@ -34,36 +34,36 @@ func refSegments(name string, folder []string) ([]string, error) {
return append(append([]string{}, base...), segs...), nil
}
// resolveLink binds every reference t reads, refusing one to t's own category:
// resolveRefs resolves every reference t reads, refusing one to t's own category:
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (t *template) resolveLink(folder []string, label, category string, root map[string]node) (templateLink, error) {
var link templateLink
func (t *template) resolveRefs(folder []string, label, category string, root map[string]node) (templateRefs, error) {
var refs templateRefs
names := refTokens(t.tokens)
if len(names) == 0 {
return link, nil
return refs, nil
}
link.refs = make(map[string]refBinding, len(names))
link.refHeads = make(map[string]node, len(names))
refs.byName = make(map[string]resolvedRef, len(names))
refs.categories = make(map[string]node, len(names))
for _, name := range names {
segments, err := refSegments(name, folder)
if err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
}
categorySegs, target, tail, err := resolveCategory(root, segments)
if err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
}
head := "/" + strings.Join(categorySegs, ".")
if category != "" && head == "/"+category {
return link, fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", label, name)
return refs, fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", label, name)
}
if err := checkPathResolves(target, tail, head); err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
}
link.refHeads[head] = target
link.refs[name] = refBinding{head, tail}
refs.categories[head] = target
refs.byName[name] = resolvedRef{head, tail}
}
return link, nil
return refs, nil
}
// resolveCategory walks a dotted path through the folders to the category it
+46 -46
View File
@@ -12,7 +12,7 @@ import (
// Fake generates a value for a dot path. Each segment descends one level: folder
// names and the category (JSON file) come first, then named fields within it,
// e.g. "sv_SE.address" or "sv_SE.address.street". Choices along the way are
// resolved at random. A path naming a folder (no value of its own) is an error.
// drawn at random. A path naming a folder (no value of its own) is an error.
func (f *Generator) Fake(path string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -29,8 +29,8 @@ func (f *Generator) Fake(path string) (string, error) {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
}
var frames frameStack
sc, _ := renderScope{frames: &frames}.at(n, &pins).enter(n, nil)
return render(f.drawState, n, sc), nil
env, _ := renderEnv{frames: &frames}.at(n, &pins).enter(n, nil)
return render(f.drawState, n, env), nil
}
// descend walks a caller's path to the node it names, returning the rows its leaf renders in.
@@ -49,40 +49,40 @@ func descend(s *drawstate.State, root node, segments []string) (node, pinSet, er
// renderOnce renders n as one render, over frames of its own.
func renderOnce(s *drawstate.State, n node) string {
var frames frameStack
return render(s, n, renderScope{frames: &frames})
return render(s, n, renderEnv{frames: &frames})
}
// render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail.
// A child this switch renders is one renderEdges must list too, or the fences miss it.
func render(s *drawstate.State, n node, sc renderScope) string {
func render(s *drawstate.State, n node, env renderEnv) string {
switch n := n.(type) {
case *choice:
return render(s, pick(s, n), sc)
return render(s, drawItem(s, n), env)
case *nullItem:
return ""
case *table:
sc.row = renderedRow{n, sc.drawRowOf(s, n)}
return expand(s, n.formatTemplate, sc)
env.row = renderedRow{n, env.drawRowOf(s, n)}
return expand(s, n.formatTemplate, env)
case *tableRow:
return expand(s, n.t.formatTemplate, sc)
return expand(s, n.t.formatTemplate, env)
case *tableColumn:
row := sc.rowOf(n.t)
row := env.rowOf(n.t)
if cell := n.t.cellTemplate(row, n.i); cell != nil {
return render(s, cell, sc)
return render(s, cell, env)
}
return n.t.rows.Cell(row, n.i)
case *template:
if n.repeat == 1 {
if mark := sc.renderFrame(n); mark >= 0 {
defer sc.frames.pop(mark)
if mark := env.renderFrame(n); mark >= 0 {
defer env.frames.pop(mark)
}
if lit, fixed := n.fixedText(); fixed {
return lit
}
return expand(s, n, sc)
return expand(s, n, env)
}
return renderRepeat(s, n, sc)
return renderRepeat(s, n, env)
default:
panic(invariant.Broken("uncompiled node %T", n))
}
@@ -90,7 +90,7 @@ func render(s *drawstate.State, n node, sc renderScope) string {
// renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound
// outside t keeps its pick, and a name t binds picks again.
func renderRepeat(s *drawstate.State, t *template, sc renderScope) string {
func renderRepeat(s *drawstate.State, t *template, env renderEnv) string {
var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
own := t.ownScope()
@@ -99,26 +99,26 @@ func renderRepeat(s *drawstate.State, t *template, sc renderScope) string {
b.WriteString(t.separator)
}
if own == nil {
b.WriteString(expand(s, t, sc))
b.WriteString(expand(s, t, env))
continue
}
mark := sc.frames.push(newPickFrame(own))
b.WriteString(expand(s, t, sc))
sc.frames.pop(mark)
mark := env.frames.push(newPickFrame(own))
b.WriteString(expand(s, t, env))
env.frames.pop(mark)
}
return b.String()
}
// pick selects one item. Uniform choices are O(1); weighted choices are an
// drawItem selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights.
func pick(s *drawstate.State, c *choice) node {
func drawItem(s *drawstate.State, c *choice) node {
if c.cum == nil {
return c.items[s.IntN(len(c.items))]
}
return c.items[s.Weighted(c.cum)]
}
func expand(s *drawstate.State, t *template, sc renderScope) string {
func expand(s *drawstate.State, t *template, env renderEnv) string {
var b strings.Builder
b.Grow(t.compiled.grow)
for i := range t.compiled.ops {
@@ -127,13 +127,13 @@ func expand(s *drawstate.State, t *template, sc renderScope) string {
case grammar.LiteralRun:
b.WriteString(o.Lit)
case grammar.NameRead:
b.WriteString(readField(s, t, sc, o.arms[s.IntN(len(o.arms))]).text)
b.WriteString(readField(s, t, env, o.arms[s.IntN(len(o.arms))]).text)
case grammar.BuiltinCall:
var operands []string
if len(o.operands) > 0 {
operands = make([]string, len(o.operands))
for j, a := range o.operands {
operands[j] = readField(s, t, sc, a).text
operands[j] = readField(s, t, env, a).text
}
}
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
@@ -145,48 +145,48 @@ func expand(s *drawstate.State, t *template, sc renderScope) string {
// readField renders one arm of a token. A read is kept in a name's pick when it reads the name, or
// a level that a read of the name being rendered addresses; every other read draws afresh, so
// {word} {word} draws twice and {p.a} {p.b} reads two draws of p.
// A fresh read clears sc.pick: were it kept, a read nested in the fresh one would key the pick's
// memo under sc.pickAt and share a value with another node at that key.
func readField(s *drawstate.State, t *template, sc renderScope, a arm) readValue {
// A fresh read clears env.pick: were it kept, a read nested in the fresh one would key the pick's
// memo under env.pickAt and share a value with another node at that key.
func readField(s *drawstate.State, t *template, env renderEnv, a arm) readValue {
switch {
case a.kind == namedRead:
return readName(s, sc, a)
case sc.pick != nil && !grammar.IsRef(a.head) && sc.keeps(a):
return readUnder(s, t, sc, a)
return readName(s, env, a)
case env.pick != nil && !grammar.IsRef(a.head) && env.keeps(a):
return readUnder(s, t, env, a)
}
sc.pick = nil
env.pick = nil
if !grammar.IsRef(a.head) {
leaf, _ := drawSteps(s, t.head(a.head), a.steps, nil, nil, a.levels)
return renderLeaf(s, leaf, sc)
leaf, _ := drawSteps(s, t.startOf(a.head), a.steps, nil, nil, a.levels)
return renderLeaf(s, leaf, env)
}
var pins pinSet
leaf, leafPins := drawSteps(s, t.head(a.head), a.steps, &pins, nil, a.levels)
sc, mark := sc.at(leaf, leafPins).enter(leaf, nil)
r := renderLeaf(s, leaf, sc)
sc.frames.pop(mark)
leaf, leafPins := drawSteps(s, t.startOf(a.head), a.steps, &pins, nil, a.levels)
env, mark := env.at(leaf, leafPins).enter(leaf, nil)
r := renderLeaf(s, leaf, env)
env.frames.pop(mark)
return r
}
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column that only
// reads one reference or name whose read drew null.
func renderLeaf(s *drawstate.State, n node, sc renderScope) readValue {
n = resolveChoice(s, n)
func renderLeaf(s *drawstate.State, n node, env renderEnv) readValue {
n = drawThroughChoices(s, n)
switch leaf := n.(type) {
case *nullItem:
return readValue{null: true}
case *template:
if leaf.readsColumn != nil {
return readField(s, leaf, sc, leaf.readsColumn.a)
return readField(s, leaf, env, leaf.readsColumn.a)
}
}
return readValue{text: render(s, n, sc)}
return readValue{text: render(s, n, env)}
}
// resolveChoice resolves a choice to one variant. Nested choices unwrap too: a draw is one value,
// not another set to pick from.
func resolveChoice(s *drawstate.State, n node) node {
// drawThroughChoices draws a choice's variant. Nested choices unwrap too: a draw is one value,
// not another set to draw from.
func drawThroughChoices(s *drawstate.State, n node) node {
for c, ok := n.(*choice); ok; c, ok = n.(*choice) {
n = pick(s, c)
n = drawItem(s, c)
}
return n
}
+2 -2
View File
@@ -43,7 +43,7 @@ func TestGrowIsALowerBound(t *testing.T) {
}
for i := 0; i < 50; i++ {
var frames frameStack
if got := len(expand(f.drawState, tmpl, renderScope{frames: &frames})); got < tmpl.compiled.grow {
if got := len(expand(f.drawState, tmpl, renderEnv{frames: &frames})); got < tmpl.compiled.grow {
t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.compiled.grow)
}
}
@@ -113,7 +113,7 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) {
"prove": func(n node) { (&valueProof{}).prove(n) },
"render": func(n node) {
var frames frameStack
render(engine(1).drawState, n, renderScope{frames: &frames, row: renderedRow{tbl, 0}})
render(engine(1).drawState, n, renderEnv{frames: &frames, row: renderedRow{tbl, 0}})
},
"renderEdges": func(n node) { renderEdges(n) },
"stepInto": func(n node) { _, _ = stepInto(n, "x") },
+35 -35
View File
@@ -7,71 +7,71 @@ import (
"github.com/larvit/fejkdata/internal/grammar"
)
// linkSite is a template to link, with its folder and category, both empty for an inline
// templateSite is a template to resolve, with its folder and category, both empty for an inline
// template, and eachNode's label for it.
type linkSite struct {
type templateSite struct {
t *template
folder []string
category, label string
}
// linkCategories links every template of the categories, once all data is merged, so a reference
// sees the override-resolved tree.
func linkCategories(sites []categorySite, root map[string]node) error {
var ts []linkSite
// resolveCategoryTemplates resolves every template of the categories, once all data is merged, so a
// reference sees the override-resolved tree.
func resolveCategoryTemplates(sites []categorySite, root map[string]node) error {
var ts []templateSite
for _, s := range sites {
if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, linkSite{t: t, folder: s.dir, category: s.path, label: label})
ts = append(ts, templateSite{t: t, folder: s.dir, category: s.path, label: label})
}
return nil
}); err != nil {
return err
}
}
return linkTemplates(ts, root)
return resolveTemplates(ts, root)
}
// linkInline links the templates of an inline node against the loaded tree.
func linkInline(scope nodeScope, root map[string]node) error {
var ts []linkSite
if err := scope(func(label string, n node) error {
// resolveInlineTemplates resolves the templates of an inline node against the loaded tree.
func resolveInlineTemplates(nodes nodeSet, root map[string]node) error {
var ts []templateSite
if err := nodes(func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, linkSite{t: t, label: label})
ts = append(ts, templateSite{t: t, label: label})
}
return nil
}); err != nil {
return err
}
return linkTemplates(ts, root)
return resolveTemplates(ts, root)
}
// nameTarget is what a binding resolves to in the assembled tree: the node its head names and the
// path it reads into that node.
// nameTarget is what a binding resolves to in the assembled tree: start, the node its head names, and
// the path it reads into that node.
type nameTarget struct {
head node
tail []string
start node
tail []string
}
// linkedNames is what the link resolved about the names of a set of templates: each binding's
// resolvedNames is what resolveTemplates resolved about the names of a set of templates: each binding's
// target, the keys its reads address, and its reads.
type linkedNames struct {
type resolvedNames struct {
targets map[*nameBinding]nameTarget
addressed map[*nameBinding]map[pickKey]string
uses map[*nameBinding][]nameUse
}
// linkTemplates links ts in steps, each over every template before the next starts. Each step
// resolveTemplates resolves ts in steps, each over every template before the next starts. Each step
// returns what it builds. The references, the compiled format and the column read are kept on each
// template. The names' targets, keys and reads pass to later steps as arguments, and the bindings
// take their targets and keys only once every check has passed.
func linkTemplates(ts []linkSite, root map[string]node) error {
func resolveTemplates(ts []templateSite, root map[string]node) error {
for _, s := range ts {
link, err := s.t.resolveLink(s.folder, s.label, s.category, root)
refs, err := s.t.resolveRefs(s.folder, s.label, s.category, root)
if err != nil {
return err
}
s.t.link = link
s.t.refs = refs
}
targets := nameTargets(ts)
for _, s := range ts {
@@ -81,7 +81,7 @@ func linkTemplates(ts []linkSite, root map[string]node) error {
}
s.t.compiled = compiled
}
names := linkedNames{targets: targets, addressed: addressedKeys(ts, targets), uses: nameUses(ts)}
names := resolvedNames{targets: targets, addressed: addressedKeys(ts, targets), uses: nameUses(ts)}
for _, s := range ts {
s.t.readsColumn = columnReadOf(s.t, targets)
}
@@ -101,14 +101,14 @@ func linkTemplates(ts []linkSite, root map[string]node) error {
return nil
}
// nameTargets resolves what each binding of ts binds, from its binder's link.
func nameTargets(ts []linkSite) map[*nameBinding]nameTarget {
// nameTargets resolves what each binding of ts binds, from its binder's references.
func nameTargets(ts []templateSite) map[*nameBinding]nameTarget {
targets := map[*nameBinding]nameTarget{}
for _, s := range ts {
for _, tok := range s.t.tokens {
if tok.Kind == grammar.NameBind {
a := splitArm(tok.BoundRef, s.t.link.refs)
targets[s.t.nameScope.bindings[tok.Bound]] = nameTarget{head: s.t.head(a.head), tail: a.tail}
a := splitArm(tok.BoundRef, s.t.refs.byName)
targets[s.t.nameScope.bindings[tok.Bound]] = nameTarget{start: s.t.startOf(a.head), tail: a.tail}
}
}
}
@@ -117,7 +117,7 @@ func nameTargets(ts []linkSite) map[*nameBinding]nameTarget {
// 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 []linkSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[pickKey]string {
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) {
@@ -136,7 +136,7 @@ func addressedKeys(ts []linkSite, targets map[*nameBinding]nameTarget) map[*name
}
// nameUses is every read of each name in ts.
func nameUses(ts []linkSite) map[*nameBinding][]nameUse {
func nameUses(ts []templateSite) map[*nameBinding][]nameUse {
uses := map[*nameBinding][]nameUse{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
@@ -171,18 +171,18 @@ func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead
return nil
}
a := ops[0].arms[0]
head, category, tail := t.head(a.head), a.head[min(1, len(a.head)):], a.tail
start, category, tail := t.startOf(a.head), a.head[min(1, len(a.head)):], a.tail
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
head, category = b.binder, ""
start, category = b.binder, ""
tail = append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
head, category = targets[b].head, b.binder.link.refs[b.ref].head[1:]
start, category = targets[b].start, b.binder.refs.byName[b.ref].head[1:]
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case !grammar.IsRef(a.head):
return nil
}
if target, isTemplate := head.(*template); isTemplate && target.isRecord && len(tail) == 1 {
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
+1 -1
View File
@@ -94,7 +94,7 @@ func readsFact(n node) string {
collect(cell)
}
case *template:
for _, b := range n.link.refs {
for _, b := range n.refs.byName {
set[strings.TrimPrefix(b.head, "/")] = true
}
for _, field := range n.fields {
+8 -8
View File
@@ -64,11 +64,11 @@ func (f *Generator) loadShippedAt(segs []string) {
}
}
// unloadedReads is every unloaded shipped category the templates of scope reference, each
// unloadedReads is every unloaded shipped category the templates of nodes reference, each
// reference read from the folder dir.
func unloadedReads(root *folder, dir []string, scope nodeScope) []unloadedCategory {
func unloadedReads(root *folder, dir []string, nodes nodeSet) []unloadedCategory {
var out []unloadedCategory
_ = scope(func(_ string, n node) error {
_ = nodes(func(_ string, n node) error {
t, isTemplate := n.(*template)
if !isTemplate {
return nil
@@ -76,7 +76,7 @@ func unloadedReads(root *folder, dir []string, scope nodeScope) []unloadedCatego
for _, name := range refTokens(t.tokens) {
segs, err := refSegments(name, dir)
if err != nil {
continue // the link reports it
continue // resolveRefs reports it
}
if u, unloaded := unloadedAt(root, segs); unloaded {
out = append(out, u)
@@ -88,7 +88,7 @@ func unloadedReads(root *folder, dir []string, scope nodeScope) []unloadedCatego
}
// loadShipped loads the shipped categories wanted and, for each category it loads, every
// category it reads and its table family, then binds all it loaded as a whole load binds.
// category it reads and its table family, then runs all it loaded through a whole load's pipeline.
// TestEveryShippedCategoryLoadsAlone reaches each category, so a failure here means a stale
// index.
func loadShipped(root *folder, wanted []unloadedCategory) {
@@ -104,14 +104,14 @@ func loadShipped(root *folder, wanted []unloadedCategory) {
panic(invariant.Broken("shipped %s: %v; after a change under data/, regenerate shippedindex.go", categoryPath(u.dir, u.name), err))
}
sites = append(sites, site)
queue = append(queue, unloadedReads(root, u.dir, sitesScope([]categorySite{site}))...)
queue = append(queue, unloadedReads(root, u.dir, siteNodes([]categorySite{site}))...)
queue = append(queue, u.linkedTables(e)...)
}
if len(sites) == 0 {
return
}
if err := categoryBinding(sites, root.children).bind(); err != nil {
panic(invariant.Broken("binding shipped categories: %v; after a change under data/, regenerate shippedindex.go", err))
if err := categoryPipeline(sites, root.children).run(); err != nil {
panic(invariant.Broken("loading shipped categories: %v; after a change under data/, regenerate shippedindex.go", err))
}
}
+2 -2
View File
@@ -262,7 +262,7 @@ func (s *structShape) compileRecord(root *folder, t reflect.Type, label string,
if err != nil {
return fmt.Errorf("%s: %w", label, err)
}
if err := bindInline(root, n, label, true); err != nil {
if err := loadInline(root, n, label, true); err != nil {
return err
}
record, err := recordOf(n)
@@ -323,7 +323,7 @@ func (k columnKind) holds(v proven.Facts) bool {
func (s *structShape) fill(state *drawstate.State, v reflect.Value) {
if s.record != nil {
var frames frameStack
for i, c := range renderRecord(state, s.record, renderScope{frames: &frames}).columns {
for i, c := range renderRecord(state, s.record, renderEnv{frames: &frames}).columns {
setColumn(fieldAt(v, s.fieldIndexes[i]), c)
}
}
+8 -13
View File
@@ -79,11 +79,10 @@
| 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 |
| 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`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
| 120 | 0.1.0 | | **Rename the engine's overloaded nouns, so `scope`, `binding`, `link`, `pick` and `head` each name one thing.** | 2 | 4 | 1 | 8 | 3.3 | 13.4 |
| 123 | 0.1.0 | | **Name the name fences by what they refuse, and state each rule at `checkOnce`, `checkTemplateOnce` and `checkUses`, so they read without the README's Names list.** | 1 | 2 | 1 | 7 | 3.4, 3.3 | 13.1 |
| 79 | 0.1.0 | decision | **Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.** | 6 | 6 | 7 | 8 | 13 | 12.9 |
| 121 | 0.1.0 | | **Move `renderScope`'s methods out of `pick.go` into `scope.go`, a table's route and draw steps out of `path.go` into `table.go`, and `arm`, `op` and `compileArm` out of `format.go` into a file named for the compiled read.** | 1 | 3 | 1 | 7 | 3.1 | 12.8 |
| 122 | 0.1.0 | | **Rewrite `columnReadOf` so each case says which column it reads, with a named step stripping the sigil, and say on `refBinding` that its head carries the `/`.** | 2 | 2 | 1 | 7 | 3.4, 3.3 | 12.1 |
| 121 | 0.1.0 | | **Move `renderEnv`'s methods out of `pick.go` into `env.go`, a table's route and draw steps out of `path.go` into `table.go`, and `arm`, `op` and `compileArm` out of `format.go` into a file named for the compiled read.** | 1 | 3 | 1 | 7 | 3.1 | 12.8 |
| 122 | 0.1.0 | | **Rewrite `columnReadOf` so each case says which column it reads, with a named step stripping the sigil, and say on `resolvedRef` that its head carries the `/`.** | 2 | 2 | 1 | 7 | 3.4, 3.3 | 12.1 |
| 124 | 0.1.0 | | **State the `..` step-down rule where `drawSteps` carries `climbed`, and split `route`'s draw condition into named parts.** | 2 | 2 | 1 | 6 | 3.4 | 10.3 |
| 126 | 0.1.0 | | **Name in `geo-se.py` the source of its delivery-digit rule and of its 200 floor.** | 1 | 1 | 1 | 5 | 3.4 | 9.8 |
| 125 | 0.1.0 | | **Delete the doc lines the panel read as restating their code.** | 1 | 2 | 1 | 5 | 3.4 | 9.5 |
@@ -115,7 +114,7 @@
| 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 7 | 8 | 16 | 14.0 |
| 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 10 | 14.0 |
| 107 | 0.2.0 | decision | **Move the template engine into `internal/`, leaving the root the public API and its options.** | 6 | 9 | 3 | 8 | 3.2, 3.4 | 12.2 |
| 112 | 0.2.0 | | **Check a calc's field operands where its template links, beside its name operands, once item 36 reports every mistake at once.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 |
| 112 | 0.2.0 | | **Check a calc's field operands where its template resolves, beside its name operands, once item 36 reports every mistake at once.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 |
| 94 | 0.2.0 | | **Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`.** | 3 | 3 | 3 | 5 | 5.7 | 10.2 |
| 108 | 0.2.0 | defect | **Name item 26 in the decision "No builtin reads the clock, so a date is bounded by days, never by an age", as the item that ends its conflict with goal 4.** | 1 | 1 | 1 | 4 | 3.4 | 8.0 |
| 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 |
@@ -285,10 +284,6 @@ Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` see
Today `fejkdata 'sv_SE.person.'` reports `no field ""`, while `{/sv_SE.person.}` reports `path has an empty segment`. `grammar.CheckSegments` is the check a reference, a sibling path and a step up `..` run.
### 120. Rename the engine's overloaded nouns, so `scope`, `binding`, `link`, `pick` and `head` each name one thing.
From the panel of 2026-10-05: `nodeScope`, `nameScope` and `renderScope`; `binding`, `nameBinding` and `refBinding`; `linkTemplates` beside `linkTables` and `rows.Link`; `pick` the function beside a named pick; `head` a string on `arm` and a node on `nameTarget`.
### 123. Name the name fences by what they refuse, and state each rule at `checkOnce`, `checkTemplateOnce` and `checkUses`, so they read without the README's Names list.
Seven of the nine seats in the panel of 2026-10-05 needed the README's Names list open to learn what these refuse.
@@ -299,11 +294,11 @@ Needs item 77, which makes an override fail unless it says it replaces.
The decision "With only the shipped set, a category loads on the first call reaching it; beside a `--data-path`, every category loads in `New`" parses the whole shipped set on every `--data-path` run, about 45 ms today and seconds once items 61 and 78 land. Goal 6 needs only the user's categories, the shipped ones they override, the shipped ones they read, and the shipped ones whose reads reach an overridden category, so the index would carry each category's reads.
### 121. Move `renderScope`'s methods out of `pick.go` into `scope.go`, a table's route and draw steps out of `path.go` into `table.go`, and `arm`, `op` and `compileArm` out of `format.go` into a file named for the compiled read.
### 121. Move `renderEnv`'s methods out of `pick.go` into `env.go`, a table's route and draw steps out of `path.go` into `table.go`, and `arm`, `op` and `compileArm` out of `format.go` into a file named for the compiled read.
Both architects in the panel of 2026-10-05 ranked this first. Revise the README's Layout block with it.
### 122. Rewrite `columnReadOf` so each case says which column it reads, with a named step stripping the sigil, and say on `refBinding` that its head carries the `/`.
### 122. Rewrite `columnReadOf` so each case says which column it reads, with a named step stripping the sigil, and say on `resolvedRef` that its head carries the `/`.
Seven of the nine seats in the panel of 2026-10-05 named it: `a.head[min(1, len(a.head)):]` and `refs[b.ref].head[1:]` strip a sigil no comment names.
@@ -317,7 +312,7 @@ Seven of the nine seats in the panel of 2026-10-05 named `route` and `drawSteps`
### 125. Delete the doc lines the panel read as restating their code.
Named in the panel of 2026-10-05: `Fake` in `inline.go`, `Record.Columns`, `pick` in `render.go`, `compileInto`, `mergeChildren`, `tableOptions`, `randomBytes`, `frameStack.push`, `checkCalcFields`, the callers `recordOf`'s doc lists, and "Each step returns what it builds" on `linkTemplates`.
Named in the panel of 2026-10-05: `Fake` in `inline.go`, `Record.Columns`, `drawItem` in `render.go`, `compileInto`, `mergeChildren`, `tableOptions`, `randomBytes`, `frameStack.push`, `checkCalcFields`, the callers `recordOf`'s doc lists, and "Each step returns what it builds" on `resolveTemplates`.
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
@@ -426,9 +421,9 @@ Require the path step to reach a sibling category.
The engine returns column values and the root builds `Record`. Revises the decision "The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc".
### 112. Check a calc's field operands where its template links, beside its name operands, once item 36 reports every mistake at once.
### 112. Check a calc's field operands where its template resolves, beside its name operands, once item 36 reports every mistake at once.
Needs item 36. `checkCalcFields` runs at compile and `checkCalcNames` at link, two passes over one rule. Moving the field check to link before item 36 would change which of two mistakes in two category files a load reports, since every category compiles before any links.
Needs item 36. `checkCalcFields` runs at compile and `checkCalcNames` in `resolveTemplates`, two passes over one rule. Moving the field check there before item 36 would change which of two mistakes in two category files a load reports, since every category compiles before any resolves.
### 94. Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/larvit/fejkdata/internal/proven"
)
// valueProof proves what typed columns and their calc operands hold, each node once per scope.
// valueProof proves what typed columns and their calc operands hold, each node once per nodeSet.
type valueProof struct {
memo map[node]proven.Facts
columnMemo map[node]proven.Facts