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
|
||||
- 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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user