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

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
10 changed files with 59 additions and 61 deletions
Showing only changes of commit 306b88ef56 - Show all commits
+1 -1
View File
@@ -40,7 +40,7 @@ In [docs/decisions.md](docs/decisions.md):
- A change to what exists is a major; a minor only adds
- Seeded output is promised within one version
- An error is a contract by what it names, not its bytes
- A format holding a reference is fenced at link, and its errors name its path
- Every format is fenced at link, and its errors name its path
- Raising the lowest supported Go is a major
- The format check runs on the latest Go only
- The changelog heading is the one spelling of a release; CI cuts the tag
+10 -5
View File
@@ -120,10 +120,14 @@ func constantValue(n calcNode, fields map[string]node) (float64, bool) {
return float64(n), true
case calcVar:
t, ok := fields[string(n)].(*template)
if !ok || !t.fixed || t.repeat > 1 {
if !ok || t.repeat > 1 {
return 0, false
}
v, err := strconv.ParseFloat(strings.TrimSpace(t.lit), 64)
lit, fixed := t.fixedText()
if !fixed {
return 0, false
}
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
return v, err == nil
case calcNeg:
v, ok := constantValue(n.x, fields)
@@ -161,11 +165,12 @@ func neverNumeric(n node) (text string, never bool) {
case *nullItem:
return "", true
case *template:
if !n.fixed || n.repeat > 1 {
lit, fixed := n.fixedText()
if !fixed || n.repeat > 1 {
return "", false
}
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
return n.lit, true
if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil {
return lit, true
}
case *choice:
for _, it := range n.items {
+5 -5
View File
@@ -121,12 +121,12 @@ this repo's own are.
A script branches on the exit code and reads the named path or spelling, so those hold;
wording improves in a minor.
## A format holding a reference is fenced at link, and its errors name its path
## Every format is fenced at link, and its errors name its path
2026-09-24, larv-review; approved 2026-09-25 by lilleman. Goal 2: a format compiles once, and one holding a reference
can compile only once bound, so its fences fire at link and name the category path as
every link error does, a cell's with its line; one without a reference still names its
file at compile. Valid while compilation needs the bound references.
2026-09-28, larv-review; approval pending. Goal 2: every format compiles in one phase,
at link, since one holding a reference can compile only once bound, so no check reads
compiled ops a format lacks yet. Its fences name the category path as every link error
does, a cell's with its line. Valid while compilation needs the bound references.
## Raising the lowest supported Go is a major
+5 -1
View File
@@ -144,7 +144,11 @@ func operandDraw(n node, into map[node]bool) {
// isFixed is a string that varies nothing: fixed text with no fields to read into.
func isFixed(n node) bool {
t, ok := n.(*template)
return ok && t.fixed && len(t.fields) == 0
if !ok {
return false
}
_, fixed := t.fixedText()
return fixed && len(t.fields) == 0
}
// renders reports whether rendering n can reach anything in want, following the
+11 -26
View File
@@ -41,7 +41,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. A format holding a reference compiles in `linkTemplateRefs`.
// an independent pick. Every format compiles in `linkTemplateRefs`.
type template struct {
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
format string
@@ -62,8 +62,6 @@ type template struct {
// Filled by `template.compileFormat`, from `template.format` and `template.refs`:
ops []op // what expand walks
grow int // minimum output size, to size the render buffer
fixed bool // no op varies, so every render is lit
lit string // the whole output when fixed
// bound maps each field the format addresses by dotted path to one path token
// reading it, which is the half of an overlap the fences name. nil when the
// format takes no path.
@@ -208,18 +206,18 @@ func compileString(s string) (node, error) {
if err != nil {
return nil, err
}
t := &template{format: s, tokens: toks, repeat: 1, fromString: true}
if err := t.compileRefFree(); err != nil {
return nil, err
}
return t, nil
return &template{format: s, tokens: toks, repeat: 1, fromString: true}, nil
}
func (t *template) compileRefFree() error {
if len(refTokens(t.tokens)) > 0 {
return nil
// fixedText is the whole output of a format holding no token, which every render is.
func (t *template) fixedText() (string, bool) {
switch {
case len(t.tokens) == 0:
return "", true
case len(t.tokens) == 1 && t.tokens[0].kind == 'l':
return t.tokens[0].lit, true
}
return t.compileFormat()
return "", false
}
// compileFormat compiles the format into ops, and applies the fences that need the
@@ -227,15 +225,6 @@ func (t *template) compileRefFree() error {
func (t *template) compileFormat() error {
c := compileOps(t.tokens, t.refs)
t.ops, t.grow, t.bound, t.held, t.heldLocal = c.ops, c.grow, c.bound, c.held, c.heldLocal
t.fixed = true
for _, o := range t.ops {
if o.kind != 'l' {
t.fixed = false
}
}
if t.fixed && len(t.ops) == 1 {
t.lit = t.ops[0].lit
}
if err := checkNoOverlap(t.ops, t.bound); err != nil {
return err
}
@@ -340,11 +329,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
if err := checkNestedDrawGroup(fields, o.group); err != nil {
return nil, err
}
t := &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, isRecord: fieldPos == inColumn}
if err := t.compileRefFree(); err != nil {
return nil, err
}
return t, nil
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, isRecord: fieldPos == inColumn}, nil
}
// templateOptions is what a template object's option keys say.
+19 -14
View File
@@ -67,9 +67,9 @@ func refSegments(name string, folder []string) ([]string, error) {
return append(append([]string{}, base...), segs...), nil
}
// linkRefs resolves every reference in the assembled tree. The head of the path —
// up to the category it names — is bound into the referring template's refHeads
// under its root path, and the rest reads into it the way a sibling path does, so
// linkRefs resolves every reference in the assembled tree and compiles every format.
// The head of the path — up to the category it names — is bound into the referring
// template's refHeads under its root path, and the rest reads into it the way a sibling path does, so
// a reference is held like a sibling and two spellings of one target are one
// draw. It runs once, after all data is merged, so a reference sees the final
// (override-resolved) tree. A path that is unknown, names a folder, or reads a
@@ -83,17 +83,26 @@ func linkRefs(root map[string]node) error {
})
}
// linkTemplateRefs binds one template's references against root, refusing one that names the
// category it sits in.
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
// linkTemplateRefs binds one template's references against root, then compiles its format.
func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error {
names := refTokens(t.tokens)
if len(names) == 0 {
return nil
}
if t.cellOf != nil {
path = fmt.Sprintf("%s, line %d", path, t.cellRow+2)
}
if names := refTokens(t.tokens); len(names) > 0 {
if err := t.bindRefs(folder, path, category, names, root); err != nil {
return err
}
}
if err := t.compileFormat(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
t.readsColumn = columnReadOf(t)
return nil
}
// bindRefs refuses a reference naming the category t sits in.
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (t *template) bindRefs(folder []string, path, category string, names []string, root map[string]node) error {
t.refs = make(map[string]refBinding, len(names))
t.refHeads = make(map[string]node, len(names))
for _, name := range names {
@@ -115,10 +124,6 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root
t.refHeads[key] = target
t.refs[name] = refBinding{key, tail}
}
if err := t.compileFormat(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
t.readsColumn = columnReadOf(t)
return nil
}
+2 -2
View File
@@ -71,8 +71,8 @@ func render(s *session, n node, sc renderScope) string {
case *template:
sc = sc.in(n)
if n.repeat == 1 {
if n.fixed {
return n.lit
if lit, fixed := n.fixedText(); fixed {
return lit
}
return expand(s, n, sc)
}
+1 -1
View File
@@ -344,7 +344,7 @@ func (t *table) compileRowFormat(format string) error {
}
t.formatTemplate = &template{format: format, tokens: toks, fields: t.fields, repeat: 1, isRecord: true, table: t}
t.pinnedRow = &tableRow{t}
return t.formatTemplate.compileRefFree()
return nil
}
// setTablePaths gives every table the path a selector on it is written at, before a
-2
View File
@@ -21,8 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming
This round, in order:
- Compile every format in one phase: a template holding a reference has nil `ops`
until `linkTemplateRefs`, so a check reading `ops` before link skips it silently.
- Run `New`, `NewTemplate` and `FakeStruct` through one bind pipeline that names
where they differ: each picks its own fences today.
- Make `disagreement` and `bothTyped` a table of cases: each swaps its operands and
+3 -2
View File
@@ -143,11 +143,12 @@ func (v proven) or(w proven) proven {
// proveTemplate proves a template that renders one value: fixed text, or a format that is
// one token alone.
func (p *valueProof) proveTemplate(t *template) proven {
lit, fixed := t.fixedText()
switch {
case t.repeat != 1:
return unproven(fmt.Sprintf("%q carries a repeat, which composes text rather than one value", t.format))
case t.fixed:
return literalValue(t.lit)
case fixed:
return literalValue(lit)
case len(t.ops) != 1:
v := unproven(notOneValue(t.format, typedCalls))
v.notOperand = notOneValue(t.format, operandCalls)