Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -40,7 +40,7 @@ In [docs/decisions.md](docs/decisions.md):
|
|||||||
- A change to what exists is a major; a minor only adds
|
- A change to what exists is a major; a minor only adds
|
||||||
- Seeded output is promised within one version
|
- Seeded output is promised within one version
|
||||||
- An error is a contract by what it names, not its bytes
|
- 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
|
- Raising the lowest supported Go is a major
|
||||||
- The format check runs on the latest Go only
|
- The format check runs on the latest Go only
|
||||||
- The changelog heading is the one spelling of a release; CI cuts the tag
|
- The changelog heading is the one spelling of a release; CI cuts the tag
|
||||||
|
|||||||
@@ -120,10 +120,14 @@ func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
|||||||
return float64(n), true
|
return float64(n), true
|
||||||
case calcVar:
|
case calcVar:
|
||||||
t, ok := fields[string(n)].(*template)
|
t, ok := fields[string(n)].(*template)
|
||||||
if !ok || !t.fixed || t.repeat > 1 {
|
if !ok || t.repeat > 1 {
|
||||||
return 0, false
|
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
|
return v, err == nil
|
||||||
case calcNeg:
|
case calcNeg:
|
||||||
v, ok := constantValue(n.x, fields)
|
v, ok := constantValue(n.x, fields)
|
||||||
@@ -161,11 +165,12 @@ func neverNumeric(n node) (text string, never bool) {
|
|||||||
case *nullItem:
|
case *nullItem:
|
||||||
return "", true
|
return "", true
|
||||||
case *template:
|
case *template:
|
||||||
if !n.fixed || n.repeat > 1 {
|
lit, fixed := n.fixedText()
|
||||||
|
if !fixed || n.repeat > 1 {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
|
if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil {
|
||||||
return n.lit, true
|
return lit, true
|
||||||
}
|
}
|
||||||
case *choice:
|
case *choice:
|
||||||
for _, it := range n.items {
|
for _, it := range n.items {
|
||||||
|
|||||||
+5
-5
@@ -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;
|
A script branches on the exit code and reads the named path or spelling, so those hold;
|
||||||
wording improves in a minor.
|
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
|
2026-09-28, larv-review; approval pending. Goal 2: every format compiles in one phase,
|
||||||
can compile only once bound, so its fences fire at link and name the category path as
|
at link, since one holding a reference can compile only once bound, so no check reads
|
||||||
every link error does, a cell's with its line; one without a reference still names its
|
compiled ops a format lacks yet. Its fences name the category path as every link error
|
||||||
file at compile. Valid while compilation needs the bound references.
|
does, a cell's with its line. Valid while compilation needs the bound references.
|
||||||
|
|
||||||
## Raising the lowest supported Go is a major
|
## Raising the lowest supported Go is a major
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -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.
|
// isFixed is a string that varies nothing: fixed text with no fields to read into.
|
||||||
func isFixed(n node) bool {
|
func isFixed(n node) bool {
|
||||||
t, ok := n.(*template)
|
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
|
// renders reports whether rendering n can reach anything in want, following the
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (*nullItem) isNode() {}
|
|||||||
// template renders a format string, substituting {tokens} from fields. A bare
|
// 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
|
// 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
|
// 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 {
|
type template struct {
|
||||||
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
|
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
|
||||||
format string
|
format string
|
||||||
@@ -60,10 +60,8 @@ type template struct {
|
|||||||
cellRow int // the row the cell sits in
|
cellRow int // the row the cell sits in
|
||||||
|
|
||||||
// Filled by `template.compileFormat`, from `template.format` and `template.refs`:
|
// Filled by `template.compileFormat`, from `template.format` and `template.refs`:
|
||||||
ops []op // what expand walks
|
ops []op // what expand walks
|
||||||
grow int // minimum output size, to size the render buffer
|
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
|
// 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
|
// reading it, which is the half of an overlap the fences name. nil when the
|
||||||
// format takes no path.
|
// format takes no path.
|
||||||
@@ -208,18 +206,18 @@ func compileString(s string) (node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t := &template{format: s, tokens: toks, repeat: 1, fromString: true}
|
return &template{format: s, tokens: toks, repeat: 1, fromString: true}, nil
|
||||||
if err := t.compileRefFree(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *template) compileRefFree() error {
|
// fixedText is the whole output of a format holding no token, which every render is.
|
||||||
if len(refTokens(t.tokens)) > 0 {
|
func (t *template) fixedText() (string, bool) {
|
||||||
return nil
|
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
|
// 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 {
|
func (t *template) compileFormat() error {
|
||||||
c := compileOps(t.tokens, t.refs)
|
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.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 {
|
if err := checkNoOverlap(t.ops, t.bound); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,11 +329,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
if err := checkNestedDrawGroup(fields, o.group); err != nil {
|
if err := checkNestedDrawGroup(fields, o.group); err != nil {
|
||||||
return nil, err
|
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}
|
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
|
||||||
if err := t.compileRefFree(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// templateOptions is what a template object's option keys say.
|
// templateOptions is what a template object's option keys say.
|
||||||
|
|||||||
+19
-14
@@ -67,9 +67,9 @@ func refSegments(name string, folder []string) ([]string, error) {
|
|||||||
return append(append([]string{}, base...), segs...), nil
|
return append(append([]string{}, base...), segs...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkRefs resolves every reference in the assembled tree. The head of the path —
|
// linkRefs resolves every reference in the assembled tree and compiles every format.
|
||||||
// up to the category it names — is bound into the referring template's refHeads
|
// The head of the path — up to the category it names — is bound into the referring
|
||||||
// under its root path, and the rest reads into it the way a sibling path does, so
|
// 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
|
// 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
|
// 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
|
// (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
|
// linkTemplateRefs binds one template's references against root, then compiles its format.
|
||||||
// category it sits in.
|
|
||||||
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
|
|
||||||
func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error {
|
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 {
|
if t.cellOf != nil {
|
||||||
path = fmt.Sprintf("%s, line %d", path, t.cellRow+2)
|
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.refs = make(map[string]refBinding, len(names))
|
||||||
t.refHeads = make(map[string]node, len(names))
|
t.refHeads = make(map[string]node, len(names))
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@@ -115,10 +124,6 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root
|
|||||||
t.refHeads[key] = target
|
t.refHeads[key] = target
|
||||||
t.refs[name] = refBinding{key, tail}
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ func render(s *session, n node, sc renderScope) string {
|
|||||||
case *template:
|
case *template:
|
||||||
sc = sc.in(n)
|
sc = sc.in(n)
|
||||||
if n.repeat == 1 {
|
if n.repeat == 1 {
|
||||||
if n.fixed {
|
if lit, fixed := n.fixedText(); fixed {
|
||||||
return n.lit
|
return lit
|
||||||
}
|
}
|
||||||
return expand(s, n, sc)
|
return expand(s, n, sc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.formatTemplate = &template{format: format, tokens: toks, fields: t.fields, repeat: 1, isRecord: true, table: t}
|
||||||
t.pinnedRow = &tableRow{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
|
// setTablePaths gives every table the path a selector on it is written at, before a
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming
|
|||||||
|
|
||||||
This round, in order:
|
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
|
- Run `New`, `NewTemplate` and `FakeStruct` through one bind pipeline that names
|
||||||
where they differ: each picks its own fences today.
|
where they differ: each picks its own fences today.
|
||||||
- Make `disagreement` and `bothTyped` a table of cases: each swaps its operands and
|
- Make `disagreement` and `bothTyped` a table of cases: each swaps its operands and
|
||||||
|
|||||||
@@ -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
|
// proveTemplate proves a template that renders one value: fixed text, or a format that is
|
||||||
// one token alone.
|
// one token alone.
|
||||||
func (p *valueProof) proveTemplate(t *template) proven {
|
func (p *valueProof) proveTemplate(t *template) proven {
|
||||||
|
lit, fixed := t.fixedText()
|
||||||
switch {
|
switch {
|
||||||
case t.repeat != 1:
|
case t.repeat != 1:
|
||||||
return unproven(fmt.Sprintf("%q carries a repeat, which composes text rather than one value", t.format))
|
return unproven(fmt.Sprintf("%q carries a repeat, which composes text rather than one value", t.format))
|
||||||
case t.fixed:
|
case fixed:
|
||||||
return literalValue(t.lit)
|
return literalValue(lit)
|
||||||
case len(t.ops) != 1:
|
case len(t.ops) != 1:
|
||||||
v := unproven(notOneValue(t.format, typedCalls))
|
v := unproven(notOneValue(t.format, typedCalls))
|
||||||
v.notOperand = notOneValue(t.format, operandCalls)
|
v.notOperand = notOneValue(t.format, operandCalls)
|
||||||
|
|||||||
Reference in New Issue
Block a user