Set a table's path when it compiles, link a table family in one function, and label a cell from the walk
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
before paraphrasing it, and name the register you read before claiming none
|
before paraphrasing it, and name the register you read before claiming none
|
||||||
publishes a spelling.
|
publishes a spelling.
|
||||||
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||||
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`linkParent`, `compileTemplate`, `renderEdges`) sit at it and stay whole; a function that would pass it is decomposed.
|
- Cyclomatic complexity is gated at 14. A function that would pass it is decomposed; a table-shaped dispatch at 14, one case per kind, stays whole.
|
||||||
|
|
||||||
# Decisions
|
# Decisions
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ In [docs/decisions.md](docs/decisions.md):
|
|||||||
- `List` advertises direct descents only
|
- `List` advertises direct descents only
|
||||||
- A path draws through its compiled steps
|
- A path draws through its compiled steps
|
||||||
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
|
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
|
||||||
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, and items 101, 102 and 104 to 106
|
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106
|
||||||
- The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and items 101 and 102 move a table's rows there
|
- The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and item 102 moves a table's rows there
|
||||||
- Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
|
- Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
|
||||||
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
|
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func checkCalcNames(path string, t *template) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil {
|
if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func loadData(sources []datafiles.Source) (map[string]node, error) {
|
|||||||
// compileInto compiles each category it is handed into the folder place returns for its Folders.
|
// compileInto compiles each category it is handed into the folder place returns for its Folders.
|
||||||
func compileInto(place func(dir []string) *folder) func(datafiles.Category) error {
|
func compileInto(place func(dir []string) *folder) func(datafiles.Category) error {
|
||||||
return func(c datafiles.Category) error {
|
return func(c datafiles.Category) error {
|
||||||
n, err := compileCategory(c.JSON, c.Name, c.ReadRows)
|
n, err := compileCategory(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,6 @@ func categoryBinding(sites []categorySite, root map[string]node) binding {
|
|||||||
return binding{
|
return binding{
|
||||||
scope: sitesScope(sites),
|
scope: sitesScope(sites),
|
||||||
link: func() error {
|
link: func() error {
|
||||||
setTablePaths(sites)
|
|
||||||
if err := linkTables(sites); err != nil {
|
if err := linkTables(sites); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -825,22 +825,22 @@ category's frames, since the field sits inside it.
|
|||||||
|
|
||||||
Valid while names are read only inside the category binding them.
|
Valid while names are read only inside the category binding them.
|
||||||
|
|
||||||
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, and items 101, 102 and 104 to 106
|
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106
|
||||||
|
|
||||||
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and `todo.md` items 101, 102 and 104 to 106. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before item 106 merges would score code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
|
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and the untangling of the engine's own structs: the change that set a table's path where it compiles, linked a table family in one function and labelled a cell from the walk, and `todo.md` items 102 and 104 to 106. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before item 106 merges would score code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
|
||||||
|
|
||||||
## The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and items 101 and 102 move a table's rows there
|
## The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and item 102 moves a table's rows there
|
||||||
|
|
||||||
2026-10-05, Lilleman auf Larv. Serves goals 3.2 and 3.4. Two architect reviews found the
|
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
|
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
|
because several passes each write part of the same structs; the template language's
|
||||||
recursion reads few of those types. Moving the engine before items 101 and 104 to 106 give
|
recursion reads few of those types. Moving the engine before items 104 to 106 give
|
||||||
each value one writer would spread that knot across packages, behind forwarding calls from
|
each value one writer would spread that knot across packages, behind forwarding calls from
|
||||||
every public method. The draw state, the grammar, `DataType`, what a proof knows of a
|
every public method. The draw state, the grammar, `DataType`, what a proof knows of a
|
||||||
value, the builtins and the reading of data files read no engine type, so each sits in a
|
value, the builtins and the reading of data files read no engine type, so each sits in a
|
||||||
package under `internal/`, behind the import list `imports_test.go` checks. A
|
package under `internal/`, behind the import list `imports_test.go` checks. Item
|
||||||
table's rows stop reading any engine type once items 101 and 102 remove those reads; item
|
102 removes a table's rows' reads of engine types, moves the rows into `internal/rows` and
|
||||||
102 then moves them into `internal/rows` and adds it to that list. Valid until `todo.md` item 107 moves the engine.
|
adds it to that list. Valid until `todo.md` item 107 moves the engine.
|
||||||
|
|
||||||
## Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
|
## Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// eachNode visits n and every node contained within it once, passing the dot path
|
// eachNode visits n and every node contained within it once, passing the dot path
|
||||||
// that reaches each. It never crosses a reference edge — a {/path} reference is
|
// that reaches each, followed by a table cell's line. It never crosses a reference edge — a {/path} reference is
|
||||||
// skipped — so a single inline node is walked on its own.
|
// skipped — so a single inline node is walked on its own.
|
||||||
func eachNode(n node, path string, fn func(path string, n node) error) error {
|
func eachNode(n node, path string, fn func(path string, n node) error) error {
|
||||||
seen := map[node]bool{}
|
seen := map[node]bool{}
|
||||||
@@ -23,7 +23,7 @@ func eachNode(n node, path string, fn func(path string, n node) error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, c := range contained(m) {
|
for _, c := range contained(m) {
|
||||||
if err := visit(join(path, c.name), c.node); err != nil {
|
if err := visit(c.labelUnder(path), c.node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,15 @@ func eachNode(n node, path string, fn func(path string, n node) error) error {
|
|||||||
type namedNode struct {
|
type namedNode struct {
|
||||||
name string
|
name string
|
||||||
node node
|
node node
|
||||||
|
line int // a table cell's line in its rows file, else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// labelUnder is the path naming c in an error, a cell by its line.
|
||||||
|
func (c namedNode) labelUnder(path string) string {
|
||||||
|
if c.line > 0 {
|
||||||
|
return fmt.Sprintf("%s, line %d", path, c.line)
|
||||||
|
}
|
||||||
|
return join(path, c.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contained(n node) []namedNode {
|
func contained(n node) []namedNode {
|
||||||
@@ -60,7 +69,7 @@ func contained(n node) []namedNode {
|
|||||||
var out []namedNode
|
var out []namedNode
|
||||||
for r := 0; r < n.t.rowCount(); r++ {
|
for r := 0; r < n.t.rowCount(); r++ {
|
||||||
if cell := n.t.cellTemplate(r, n.i); cell != nil {
|
if cell := n.t.cellTemplate(r, n.i); cell != nil {
|
||||||
out = append(out, namedNode{node: cell})
|
out = append(out, namedNode{node: cell, line: r + 2})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRea
|
|||||||
func linkNames(path string, t *template) error {
|
func linkNames(path string, t *template) error {
|
||||||
return namedReads(t, func(o *op, a *arm) error {
|
return namedReads(t, func(o *op, a *arm) error {
|
||||||
if err := linkName(t, a); err != nil {
|
if err := linkName(t, a); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-3
@@ -17,16 +17,16 @@ func checkNameReads(path string, t *template) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil {
|
if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), tok.Body, err)
|
return fmt.Errorf("%s: token {%s}: %w", path, tok.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return namedReads(t, func(o *op, a *arm) error {
|
return namedReads(t, func(o *op, a *arm) error {
|
||||||
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
|
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.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", t.site.label(path), o.Body, a.named.name, a.named.ref)
|
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", path, o.Body, a.named.name, a.named.ref)
|
||||||
}
|
}
|
||||||
for _, leaf := range a.leaves {
|
for _, leaf := range a.leaves {
|
||||||
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
|
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/datafiles"
|
||||||
"github.com/larvit/fejkdata/internal/grammar"
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,9 +56,8 @@ type template struct {
|
|||||||
repeat int
|
repeat int
|
||||||
separator string
|
separator string
|
||||||
datatype DataType
|
datatype DataType
|
||||||
fromString bool // written as a JSON string rather than an object
|
fromString bool // written as a JSON string rather than an object
|
||||||
isRecord bool // compiled at the top without a repeat, so its fields are record columns
|
isRecord bool // compiled at the top without a repeat, so its fields are record columns
|
||||||
site tableSite
|
|
||||||
unbound []unboundRead // the heads its tokens read that no field holds
|
unbound []unboundRead // the heads its tokens read that no field holds
|
||||||
|
|
||||||
// Filled by `bindNames`, from the compiled category:
|
// Filled by `bindNames`, from the compiled category:
|
||||||
@@ -69,27 +69,6 @@ type template struct {
|
|||||||
compiled formatOps
|
compiled formatOps
|
||||||
}
|
}
|
||||||
|
|
||||||
// tableSite is the table a template belongs to, as its format or one of its cells; table is nil
|
|
||||||
// for any other template.
|
|
||||||
type tableSite struct {
|
|
||||||
table *table
|
|
||||||
row int // the cell's row, or formatRow for the format
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatRow = -1
|
|
||||||
|
|
||||||
func (s tableSite) isCell() bool {
|
|
||||||
return s.table != nil && s.row != formatRow
|
|
||||||
}
|
|
||||||
|
|
||||||
// label names the template at path in an error, a cell by its line in the rows file.
|
|
||||||
func (s tableSite) label(path string) string {
|
|
||||||
if s.isCell() {
|
|
||||||
return fmt.Sprintf("%s, line %d", path, s.row+2)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// templateLink is what a template resolves to in the assembled tree.
|
// templateLink is what a template resolves to in the assembled tree.
|
||||||
type templateLink struct {
|
type templateLink struct {
|
||||||
refs map[string]refBinding // each reference the format reads -> what it resolves to
|
refs map[string]refBinding // each reference the format reads -> what it resolves to
|
||||||
@@ -119,18 +98,18 @@ func compile(v any) (node, error) {
|
|||||||
|
|
||||||
// compileCategory compiles a data file's value, which may be a table over a rows
|
// compileCategory compiles a data file's value, which may be a table over a rows
|
||||||
// file beside it, and refuses a choice that is a table written as templates.
|
// file beside it, and refuses a choice that is a table written as templates.
|
||||||
func compileCategory(v any, name string, readRows func(file string) (string, error)) (node, error) {
|
func compileCategory(c datafiles.Category) (node, error) {
|
||||||
if m, ok := v.(map[string]any); ok {
|
if m, ok := c.JSON.(map[string]any); ok {
|
||||||
if _, isTable := m["rows"]; isTable {
|
if _, isTable := m["rows"]; isTable {
|
||||||
return compileTable(m, name, readRows)
|
return compileTable(m, c.Name, join(strings.Join(c.Folders, "."), c.Name), c.ReadRows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if items, ok := v.([]any); ok {
|
if items, ok := c.JSON.([]any); ok {
|
||||||
if err := checkNotRows(items, name); err != nil {
|
if err := checkNotRows(items, c.Name); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return compile(v)
|
return compile(c.JSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNotRows refuses a choice of templates sharing one format and one set of
|
// checkNotRows refuses a choice of templates sharing one format and one set of
|
||||||
@@ -197,7 +176,7 @@ func compileAt(v any, pos position) (node, error) {
|
|||||||
func compileItem(v any, pos position) (node, error) {
|
func compileItem(v any, pos position) (node, error) {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
t, err := compileString(v, tableSite{})
|
t, err := compileString(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -228,20 +207,12 @@ func jsonKind(v any) string {
|
|||||||
return fmt.Sprintf("%T", v)
|
return fmt.Sprintf("%T", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileString(s string, site tableSite) (*template, error) {
|
func compileString(s string) (*template, error) {
|
||||||
toks, unbound, err := parseChecked(s, nil)
|
toks, unbound, err := parseChecked(s, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if site.table != nil {
|
return &template{format: s, tokens: toks, repeat: 1, fromString: true, unbound: unbound}, nil
|
||||||
if err := refuseTableBinding(toks); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(unbound) > 0 {
|
|
||||||
return nil, unbound[0].err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &template{format: s, tokens: toks, repeat: 1, fromString: true, site: site, unbound: unbound}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixedText is one render's output when the format holds no token; repeat is the caller's.
|
// fixedText is one render's output when the format holds no token; repeat is the caller's.
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ func linkRefs(sites []categorySite, root map[string]node) error {
|
|||||||
// linkTemplate resolves t's references in category, "" for an inline template, and
|
// linkTemplate resolves t's references in category, "" for an inline template, and
|
||||||
// compiles its format against them.
|
// compiles its format against them.
|
||||||
func linkTemplate(folder []string, path, category string, t *template, root map[string]node) error {
|
func linkTemplate(folder []string, path, category string, t *template, root map[string]node) error {
|
||||||
link, err := t.resolveLink(folder, t.site.label(path), category, root)
|
link, err := t.resolveLink(folder, path, category, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func isTableOption(name string) bool {
|
|||||||
// token arm.
|
// token arm.
|
||||||
const inSelector = `[]{}"|`
|
const inSelector = `[]{}"|`
|
||||||
|
|
||||||
func compileTable(m map[string]any, segment string, readRows func(file string) (string, error)) (*table, error) {
|
func compileTable(m map[string]any, segment, path string, readRows func(file string) (string, error)) (*table, error) {
|
||||||
o, err := readTableOptions(m)
|
o, err := readTableOptions(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -94,7 +94,7 @@ func compileTable(m map[string]any, segment string, readRows func(file string) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t := &table{segment: segment, file: o.rows, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1}
|
t := &table{segment: segment, path: path, file: o.rows, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1}
|
||||||
if err := t.parseRows(data); err != nil {
|
if err := t.parseRows(data); err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", o.rows, err)
|
return nil, fmt.Errorf("%s: %w", o.rows, err)
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ func (t *table) checkCells() error {
|
|||||||
if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 {
|
if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
n, err := compileString(cell, tableSite{t, row})
|
n, err := compileCell(cell)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("line %d, %s: %w", row+2, t.header[col], err)
|
return fmt.Errorf("line %d, %s: %w", row+2, t.header[col], err)
|
||||||
}
|
}
|
||||||
@@ -319,6 +319,22 @@ func (t *table) checkCells() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compileCell compiles a cell carrying a token. A cell sits in no name scope, so it binds no
|
||||||
|
// name and reads only references.
|
||||||
|
func compileCell(cell string) (*template, error) {
|
||||||
|
n, err := compileString(cell)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := refuseTableBinding(n.tokens); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(n.unbound) > 0 {
|
||||||
|
return nil, n.unbound[0].err
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
// compileRowFormat compiles the format a row renders through, over the columns as its
|
// compileRowFormat compiles the format a row renders through, over the columns as its
|
||||||
// fields.
|
// fields.
|
||||||
func (t *table) compileRowFormat(format string) error {
|
func (t *table) compileRowFormat(format string) error {
|
||||||
@@ -351,7 +367,7 @@ func (t *table) compileRowFormat(format string) error {
|
|||||||
if len(unbound) > 0 {
|
if len(unbound) > 0 {
|
||||||
return unbound[0].err
|
return unbound[0].err
|
||||||
}
|
}
|
||||||
t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true, site: tableSite{t, formatRow}}
|
t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true}
|
||||||
t.rowNode = &tableRow{t}
|
t.rowNode = &tableRow{t}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -366,48 +382,46 @@ func refuseTableBinding(toks []grammar.Token) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTablePaths gives every table the path a selector on it is written at, before a
|
// linkTables links every table to its parent beside it, the one writer of parentT and
|
||||||
// link or a draw can name one.
|
// children.
|
||||||
func setTablePaths(sites []categorySite) {
|
|
||||||
for _, s := range sites {
|
|
||||||
if t, isTable := s.n.(*table); isTable {
|
|
||||||
t.path = s.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// linkTables binds every table's parent to the table beside it, and proves the
|
|
||||||
// links: a parent has a key, every link cell is one, every parent row is linked
|
|
||||||
// to, no chain of parents closes, and no child is named like a parent's column.
|
|
||||||
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
|
|
||||||
func linkTables(sites []categorySite) error {
|
func linkTables(sites []categorySite) error {
|
||||||
for _, s := range sites {
|
for _, s := range sites {
|
||||||
t, isTable := s.n.(*table)
|
t, isTable := s.n.(*table)
|
||||||
if !isTable || t.parentIndex < 0 {
|
if !isTable || t.parentIndex < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := t.linkParent(s.path, s.in.children); err != nil {
|
p, err := t.proveParent(s.in.children)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", s.path, err)
|
return fmt.Errorf("%s: %w", s.path, err)
|
||||||
}
|
}
|
||||||
|
t.parentT = p
|
||||||
|
if p.children == nil {
|
||||||
|
p.children = map[string]*table{}
|
||||||
|
}
|
||||||
|
p.children[t.segment] = t
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *table) linkParent(path string, siblings map[string]node) error {
|
// proveParent is the table among siblings that t's link column names, proved: it has a key,
|
||||||
|
// every link cell is one, every row of it is linked to, no chain of parents closes, and no
|
||||||
|
// ancestor has a column named like t.
|
||||||
|
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
|
||||||
|
func (t *table) proveParent(siblings map[string]node) (*table, error) {
|
||||||
name := t.header[t.parentIndex]
|
name := t.header[t.parentIndex]
|
||||||
p, ok := siblings[name].(*table)
|
p, ok := siblings[name].(*table)
|
||||||
switch {
|
switch {
|
||||||
case siblings[name] == nil:
|
case siblings[name] == nil:
|
||||||
return fmt.Errorf("parent %q names no table beside it", name)
|
return nil, fmt.Errorf("parent %q names no table beside it", name)
|
||||||
case !ok:
|
case !ok:
|
||||||
return fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
|
return nil, fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
|
||||||
case p.keyIndex < 0:
|
case p.keyIndex < 0:
|
||||||
return fmt.Errorf("parent %q has no key column to link to", name)
|
return nil, fmt.Errorf("parent %q has no key column to link to", name)
|
||||||
}
|
}
|
||||||
var ancestors []*table
|
var ancestors []*table
|
||||||
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.header[q.parentIndex]].(*table) {
|
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.header[q.parentIndex]].(*table) {
|
||||||
if seen[q] {
|
if seen[q] {
|
||||||
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
|
return nil, fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
|
||||||
}
|
}
|
||||||
seen[q] = true
|
seen[q] = true
|
||||||
ancestors = append(ancestors, q)
|
ancestors = append(ancestors, q)
|
||||||
@@ -417,28 +431,23 @@ func (t *table) linkParent(path string, siblings map[string]node) error {
|
|||||||
}
|
}
|
||||||
for _, q := range ancestors {
|
for _, q := range ancestors {
|
||||||
if _, clash := q.col[t.segment]; clash {
|
if _, clash := q.col[t.segment]; clash {
|
||||||
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
|
return nil, fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
linked := make(map[string]bool, p.rowCount())
|
linked := make(map[string]bool, p.rowCount())
|
||||||
for r := 0; r < t.rowCount(); r++ {
|
for r := 0; r < t.rowCount(); r++ {
|
||||||
k := t.cell(r, t.parentIndex)
|
k := t.cell(r, t.parentIndex)
|
||||||
if _, ok := p.byKey[k]; !ok {
|
if _, ok := p.byKey[k]; !ok {
|
||||||
return fmt.Errorf("%s line %d: %s %q is no key of %s", t.file, r+2, name, k, p.file)
|
return nil, fmt.Errorf("%s line %d: %s %q is no key of %s", t.file, r+2, name, k, p.file)
|
||||||
}
|
}
|
||||||
linked[k] = true
|
linked[k] = true
|
||||||
}
|
}
|
||||||
for r := 0; r < p.rowCount(); r++ {
|
for r := 0; r < p.rowCount(); r++ {
|
||||||
if k := p.cell(r, p.keyIndex); !linked[k] {
|
if k := p.cell(r, p.keyIndex); !linked[k] {
|
||||||
return fmt.Errorf("%s links no row to %s %q; every %s row needs one, or drop line %d of %s", t.file, name, k, name, r+2, p.file)
|
return nil, fmt.Errorf("%s links no row to %s %q; every %s row needs one, or drop line %d of %s", t.file, name, k, name, r+2, p.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.parentT = p
|
return p, nil
|
||||||
if p.children == nil {
|
|
||||||
p.children = map[string]*table{}
|
|
||||||
}
|
|
||||||
p.children[t.segment] = t
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *table) builtLookup() *rowLookup {
|
func (t *table) builtLookup() *rowLookup {
|
||||||
|
|||||||
@@ -72,13 +72,12 @@
|
|||||||
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
|
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
|
||||||
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
|
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
|
||||||
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
|
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
|
||||||
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run once items 101, 102 and 104 to 106 have restructured the code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
|
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run once items 102 and 104 to 106 have restructured the code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
|
||||||
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
|
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
|
||||||
| 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 |
|
| 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 |
|
||||||
| 102 | 0.1.0 | defect | **Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.** | 5 | 6 | 4 | 8 | 3.2, 3.4, 5 | 15.9 |
|
| 102 | 0.1.0 | defect | **Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.** | 5 | 6 | 4 | 8 | 3.2, 3.4, 5 | 15.9 |
|
||||||
| 106 | 0.1.0 | | **Settle each record's shape when its template links, so `Generator.records` goes.** | 2 | 2 | 2 | 7 | 3.2 | 14.1 |
|
| 106 | 0.1.0 | | **Settle each record's shape when its template links, so `Generator.records` goes.** | 2 | 2 | 2 | 7 | 3.2 | 14.1 |
|
||||||
| 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 |
|
| 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 |
|
||||||
| 101 | 0.1.0 | decision | **Set a table's path when it compiles, link a table family in one function, and label a cell's template from the walk, so no template points back at its table.** | 4 | 4 | 2 | 8 | 3.2 | 13.4 |
|
|
||||||
| 104 | 0.1.0 | decision | **Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.** | 4 | 4 | 2 | 8 | 3.2 | 13.4 |
|
| 104 | 0.1.0 | decision | **Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.** | 4 | 4 | 2 | 8 | 3.2 | 13.4 |
|
||||||
| 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 |
|
| 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 |
|
||||||
| 105 | 0.1.0 | principle | **Link each template in steps that each return what they build — its references, its bindings' heads and addressed keys, its compiled arms, its column read — so no step writes into what an earlier one built.** | 6 | 7 | 2 | 9 | 3.2 | 12.4 |
|
| 105 | 0.1.0 | principle | **Link each template in steps that each return what they build — its references, its bindings' heads and addressed keys, its compiled arms, its column read — so no step writes into what an earlier one built.** | 6 | 7 | 2 | 9 | 3.2 | 12.4 |
|
||||||
@@ -241,9 +240,9 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
|
|||||||
|
|
||||||
Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. The maintainer answered yes on 2026-10-05, for reads inside one category or one struct; reads across categories, `{/city.name}, {/country.name}`, keep loading.
|
Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. The maintainer answered yes on 2026-10-05, for reads inside one category or one struct; reads across categories, `{/city.name}, {/country.name}`, keep loading.
|
||||||
|
|
||||||
### 5. Run the nine-seat comprehension panel and larv-review's scoring run once items 101, 102 and 104 to 106 have restructured the code, and file what the panel names.
|
### 5. Run the nine-seat comprehension panel and larv-review's scoring run once items 102 and 104 to 106 have restructured the code, and file what the panel names.
|
||||||
|
|
||||||
Until a panel scores 7.0 or above, only items 101, 102 and 104 to 106 and the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, and items 101, 102 and 104 to 106" suspends every scoring run and panel until this item runs.
|
Until a panel scores 7.0 or above, only items 102 and 104 to 106 and the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106" suspends every scoring run and panel until this item runs.
|
||||||
|
|
||||||
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
|
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
|
||||||
|
|
||||||
@@ -255,7 +254,7 @@ Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` see
|
|||||||
|
|
||||||
### 102. Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.
|
### 102. Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.
|
||||||
|
|
||||||
Needs item 101. `internal/rows` takes the rows, their key and parent proofs, the pins, the row draws and the lookups, and owns the parent and child links. Each rows table holds the engine table that owns it as a payload of a type parameter, set when the rows table is built, so a step to a relative reaches its format and cells with no side map, the links have one writer, and `internal/rows` imports no engine type. The rows code reads `CheckName` and the selector spelling from `internal/grammar`. The lookups reuse the weights `sumWeights` parsed; today `builtLookup` parses them again. Today `pathCheck.walk` and `probePath` are two loops over one rule for passing a choice, a table or a field, and they have drifted: `probePath` skips `pathCheck.enter`, so `Fake("x.a")` under `"repeat":3` renders one draw where the README's Correlated fields section refuses it; the refusal owes a `CHANGELOG.md` entry. The shared step takes its state as parameters, as `drawSteps` does: one loop switching on a mode raised a repeat of a reference path from 66 to 106 allocations (decision "A path draws through its compiled steps"). Technical principle: DRY.
|
`internal/rows` takes the rows, their key and parent proofs, the pins, the row draws and the lookups, and owns the parent and child links. Each rows table holds the engine table that owns it as a payload of a type parameter, set when the rows table is built, so a step to a relative reaches its format and cells with no side map, the links have one writer, and `internal/rows` imports no engine type. The rows code reads `CheckName` and the selector spelling from `internal/grammar`. The lookups reuse the weights `sumWeights` parsed; today `builtLookup` parses them again. Today `pathCheck.walk` and `probePath` are two loops over one rule for passing a choice, a table or a field, and they have drifted: `probePath` skips `pathCheck.enter`, so `Fake("x.a")` under `"repeat":3` renders one draw where the README's Correlated fields section refuses it; the refusal owes a `CHANGELOG.md` entry. The shared step takes its state as parameters, as `drawSteps` does: one loop switching on a mode raised a repeat of a reference path from 66 to 106 allocations (decision "A path draws through its compiled steps"). Technical principle: DRY.
|
||||||
|
|
||||||
### 106. Settle each record's shape when its template links, so `Generator.records` goes.
|
### 106. Settle each record's shape when its template links, so `Generator.records` goes.
|
||||||
|
|
||||||
@@ -265,10 +264,6 @@ Needs item 105. Settling all 84 shipped record shapes took 0.09 ms on 2026-10-05
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### 101. Set a table's path when it compiles, link a table family in one function, and label a cell's template from the walk, so no template points back at its table.
|
|
||||||
|
|
||||||
`template.site` serves only labels and the table-cell refusals at compile, so it goes and the walk names the label. `setTablePaths` and the write `linkParent` makes into another table's `children` become one builder; `AGENTS.md`'s complexity rule, which names `linkParent` among the dispatches that stay whole, changes with it.
|
|
||||||
|
|
||||||
### 104. Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.
|
### 104. Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.
|
||||||
|
|
||||||
The two-way link `nameScope.owner` ↔ `template.ownNameScope` is written by a third party, `settle`; `resolveReads` appends `uses` to a binding that only the load checks read.
|
The two-way link `nameScope.owner` ↔ `template.ownNameScope` is written by a third party, `settle`; `resolveReads` appends `uses` to a binding that only the load checks read.
|
||||||
@@ -384,7 +379,7 @@ Require the path step to reach a sibling category.
|
|||||||
|
|
||||||
### 107. Move the template engine into `internal/`, leaving the root the public API and its options.
|
### 107. Move the template engine into `internal/`, leaving the root the public API and its options.
|
||||||
|
|
||||||
Needs item 5. 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 sits in `internal/`, and items 101 and 102 move a table's rows there", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc".
|
Needs item 5. 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 sits in `internal/`, and item 102 moves a table's rows there", 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 links, beside its name operands, once item 36 reports every mistake at once.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user