Answer architecture round 1: calls carry no operands in the grammar, ParseCalc returns its operands, one empty-segment check, sorted token kinds, and file items 110 and 111

This commit is contained in:
2026-10-05 12:25:22 +02:00
committed by Lillem4n
parent 3479f9cf98
commit 6fd7da19e5
14 changed files with 103 additions and 103 deletions
+1 -1
View File
@@ -979,7 +979,7 @@ valueproof.go the value proof: what a typed column, calc operand or struct fie
data.go the load path: fs.FS folders/files -> merged namespace tree, and the bind pipeline every entry point runs data.go the load path: fs.FS folders/files -> merged namespace tree, and the bind pipeline every entry point runs
cmd/fejkdata/ the fejkdata CLI cmd/fejkdata/ the fejkdata CLI
internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax, and whether an argument is a template or a path
data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md) data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md)
docs/ the decision log, the register research behind the shipped data, and the survey of other libraries docs/ the decision log, the register research behind the shipped data, and the survey of other libraries
+11 -11
View File
@@ -51,11 +51,11 @@ func checkCalc(fields map[string]node, args []string) error {
if len(args) < 1 || len(args) > 2 { if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args)) return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
} }
expr, err := grammar.ParseCalc(args[0]) c, err := grammar.ParseCalc(args[0])
if err != nil { if err != nil {
return fmt.Errorf("calc(%q): %w", args[0], err) return fmt.Errorf("calc(%q): %w", args[0], err)
} }
if err := checkOperands(args[0], expr, func(name string) []node { if err := checkOperands(args[0], c, func(name string) []node {
if n, ok := fields[name]; ok { if n, ok := fields[name]; ok {
return []node{n} return []node{n}
} }
@@ -101,13 +101,13 @@ func operandNodes(o op) func(name string) []node {
// checkOperands refuses an operand that is never a number and a division by a constant zero. // checkOperands refuses an operand that is never a number and a division by a constant zero.
// operand lists every node an operand may render, nil while that is unknown. // operand lists every node an operand may render, nil while that is unknown.
func checkOperands(text string, expr grammar.CalcNode, operand func(name string) []node) error { func checkOperands(text string, c grammar.Calc, operand func(name string) []node) error {
for _, name := range grammar.CalcVars(expr) { for _, name := range c.Operands {
if rendered, never := allNeverNumeric(operand(name)); never { if rendered, never := allNeverNumeric(operand(name)); never {
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", text, name, rendered) return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", text, name, rendered)
} }
} }
if divisor, zero := constantZeroDivisor(expr, operand); zero { if divisor, zero := constantZeroDivisor(c.Expr, operand); zero {
return fmt.Errorf("calc(%q) divides by %s, which is always zero", text, divisor) return fmt.Errorf("calc(%q) divides by %s, which is always zero", text, divisor)
} }
return nil return nil
@@ -202,7 +202,7 @@ func allNeverNumeric(nodes []node) (text string, never bool) {
// calcPrep parses the expression and decimals once, at compile time. checkCalc proved // calcPrep parses the expression and decimals once, at compile time. checkCalc proved
// both args valid, so no step here can fail. // both args valid, so no step here can fail.
func calcPrep(args []string) callFn { func calcPrep(args []string) callFn {
expr := parsedCalc(args[0]) expr := parsedCalc(args[0]).Expr
dp := calcDecimals(args) dp := calcDecimals(args)
return func(_ *drawstate.State, _ string, operands []string) string { return func(_ *drawstate.State, _ string, operands []string) string {
return formatFloat(evalCalc(expr, operands), dp) return formatFloat(evalCalc(expr, operands), dp)
@@ -211,12 +211,12 @@ func calcPrep(args []string) callFn {
// parsedCalc parses an expression checkCalc accepted. A nil AST would dereference later, with // parsedCalc parses an expression checkCalc accepted. A nil AST would dereference later, with
// no message. // no message.
func parsedCalc(expr string) grammar.CalcNode { func parsedCalc(expr string) grammar.Calc {
n, err := grammar.ParseCalc(expr) c, err := grammar.ParseCalc(expr)
if err != nil { if err != nil {
panic(internalError("calc(%q) passed its check unparsed: %v", expr, err)) panic(internalError("calc(%q) passed its check unparsed: %v", expr, err))
} }
return n return c
} }
// calcDecimals is a calc's decimals count, or shortestDecimals where it names none. // calcDecimals is a calc's decimals count, or shortestDecimals where it names none.
@@ -233,9 +233,9 @@ func calcOperands(args []string) []string {
if len(args) == 0 { if len(args) == 0 {
return nil return nil
} }
expr, err := grammar.ParseCalc(args[0]) c, err := grammar.ParseCalc(args[0])
if err != nil { if err != nil {
return nil return nil
} }
return grammar.CalcVars(expr) return c.Operands
} }
+19 -19
View File
@@ -62,7 +62,7 @@ func checkFunc(tok grammar.Token, fields map[string]node) error {
} }
func parseChecked(format string, fields map[string]node) ([]grammar.Token, []unboundRead, error) { func parseChecked(format string, fields map[string]node) ([]grammar.Token, []unboundRead, error) {
toks, err := grammar.ParseFormat(format, builtinOperands) toks, err := grammar.ParseFormat(format)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -110,7 +110,7 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error)
} }
// Last, so an arm broken on its own terms is reported as that: a repeat is // Last, so an arm broken on its own terms is reported as that: a repeat is
// the consequence of such a mistake, not the mistake itself. // the consequence of such a mistake, not the mistake itself.
return unbound, checkNoRepeatedArm(t.Body, t.Names) return unbound, checkNoRepeatedArm(t.Body, t.Arms)
} }
return nil, nil return nil, nil
} }
@@ -119,14 +119,15 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error)
// field, returning those whose head no field holds. // field, returning those whose head no field holds.
func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unboundRead, error) { func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unboundRead, error) {
var unbound []unboundRead var unbound []unboundRead
for _, name := range t.Names { names := tokenReads(t)
for _, name := range names {
if grammar.IsRef(name) { if grammar.IsRef(name) {
if _, _, err := grammar.RefShape(name); err != nil { if _, _, err := grammar.RefShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.Body, err) return nil, fmt.Errorf("token {%s}: %w", t.Body, err)
} }
continue // its target is checked at New (see linkRefs) continue // its target is checked at New (see linkRefs)
} }
missing, err := checkArm(name, fields, !operands && len(t.Names) == 1) missing, err := checkArm(name, fields, !operands && len(names) == 1)
if err == nil { if err == nil {
continue continue
} }
@@ -214,6 +215,14 @@ func hintableRef(name string) bool {
return grammar.CheckPathNames(name) == nil return grammar.CheckPathNames(name) == nil
} }
// tokenReads lists the names t reads: a read's arms, or the operands a call's builtin reads.
func tokenReads(t grammar.Token) []string {
if t.Kind == grammar.BuiltinCall {
return builtinOperands(t.Fn, t.Args)
}
return t.Arms
}
// builtinOperands lists the fields a call reads as operands, empty for a builtin that // builtinOperands lists the fields a call reads as operands, empty for a builtin that
// reads none or is unknown. // reads none or is unknown.
func builtinOperands(name string, args []string) []string { func builtinOperands(name string, args []string) []string {
@@ -295,22 +304,13 @@ func pathArm(name, head, writtenHead string, segs []string) arm {
return arm{spelling: name, head: head, writtenHead: writtenHead, tail: segs, levels: levels, path: head + "." + strings.Join(segs, ".")} return arm{spelling: name, head: head, writtenHead: writtenHead, tail: segs, levels: levels, path: head + "." + strings.Join(segs, ".")}
} }
// checkSegments rejects an unfinished path: "{a.}" and "{a...b}" each have a // checkSegments refuses an empty segment in a path, which a field really named "" would
// segment naming nothing. A field really named "" would otherwise make them // otherwise resolve, so a typo would read as a path that worked.
// resolve, so a typo would read as a path that worked.
func checkSegments(a arm) error { func checkSegments(a arm) error {
if len(a.tail) == 0 { if len(a.tail) == 0 {
return nil return nil
} }
if a.head == "" { return grammar.CheckSegments(append([]string{a.head}, a.tail...))
return fmt.Errorf("path has an empty segment")
}
for _, seg := range a.tail {
if seg == "" {
return fmt.Errorf("path has an empty segment")
}
}
return nil
} }
// callFn is a builtin prepared for one call site: its args already parsed. It reads the // callFn is a builtin prepared for one call site: its args already parsed. It reads the
@@ -337,7 +337,7 @@ type formatOps struct {
func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) {
var operands []arm var operands []arm
for _, operand := range tok.Names { for _, operand := range builtinOperands(tok.Fn, tok.Args) {
a := splitArm(operand, refs) a := splitArm(operand, refs)
if isName(a.head) { if isName(a.head) {
a.kind = namedRead a.kind = namedRead
@@ -348,8 +348,8 @@ func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isNa
} }
func (c *formatOps) field(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { func (c *formatOps) field(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) {
arms := make([]arm, len(tok.Names)) arms := make([]arm, len(tok.Arms))
for i, name := range tok.Names { for i, name := range tok.Arms {
arms[i] = splitArm(name, refs) arms[i] = splitArm(name, refs)
if isName(arms[i].head) { if isName(arms[i].head) {
arms[i].kind = namedRead arms[i].kind = namedRead
+21 -36
View File
@@ -12,8 +12,14 @@ type CalcNode interface{ calcNode() }
type CalcNum float64 // a number literal type CalcNum float64 // a number literal
// CalcVar is an operand, a sibling field or a name. At is its position among the // Calc is a parsed calc expression and the distinct operands it reads, in the order it
// distinct operands in the order the expression first names each, which CalcVars lists. // first names each.
type Calc struct {
Expr CalcNode
Operands []string
}
// CalcVar is an operand, a sibling field or a name, at its position in Calc.Operands.
type CalcVar struct { type CalcVar struct {
Name string Name string
At int At int
@@ -31,27 +37,6 @@ func (CalcVar) calcNode() {}
func (CalcNeg) calcNode() {} func (CalcNeg) calcNode() {}
func (CalcBin) calcNode() {} func (CalcBin) calcNode() {}
// CalcVars lists the distinct operands an expression reads, each at its At.
func CalcVars(n CalcNode) []string {
var out []string
var walk func(CalcNode)
walk = func(n CalcNode) {
switch n := n.(type) {
case CalcVar:
if n.At == len(out) {
out = append(out, n.Name)
}
case CalcNeg:
walk(n.X)
case CalcBin:
walk(n.L)
walk(n.R)
}
}
walk(n)
return out
}
// CalcText spells an expression node the way an author would read it. // CalcText spells an expression node the way an author would read it.
func CalcText(n CalcNode) string { func CalcText(n CalcNode) string {
switch n := n.(type) { switch n := n.(type) {
@@ -70,25 +55,25 @@ func CalcText(n CalcNode) string {
// calcParser is a recursive-descent parser over the expression runes, threading // calcParser is a recursive-descent parser over the expression runes, threading
// expr -> term -> factor for the standard * / before + - precedence. // expr -> term -> factor for the standard * / before + - precedence.
type calcParser struct { type calcParser struct {
rs []rune rs []rune
pos int pos int
vars map[string]int operands []string
} }
// ParseCalc parses a whole expression, requiring it to consume all input. // ParseCalc parses a whole expression, requiring it to consume all input.
func ParseCalc(expr string) (CalcNode, error) { func ParseCalc(expr string) (Calc, error) {
p := &calcParser{rs: []rune(expr), vars: map[string]int{}} p := &calcParser{rs: []rune(expr)}
if p.space(); p.pos >= len(p.rs) { if p.space(); p.pos >= len(p.rs) {
return nil, fmt.Errorf("empty expression") return Calc{}, fmt.Errorf("empty expression")
} }
n, err := p.expr() n, err := p.expr()
if err != nil { if err != nil {
return nil, err return Calc{}, err
} }
if p.space(); p.pos != len(p.rs) { if p.space(); p.pos != len(p.rs) {
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:])) return Calc{}, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
} }
return n, nil return Calc{n, p.operands}, nil
} }
func (p *calcParser) space() { func (p *calcParser) space() {
@@ -185,10 +170,10 @@ func (p *calcParser) ident() (CalcNode, error) {
} }
} }
name := string(p.rs[start:p.pos]) name := string(p.rs[start:p.pos])
at, seen := p.vars[name] at := slices.Index(p.operands, name)
if !seen { if at < 0 {
at = len(p.vars) at = len(p.operands)
p.vars[name] = at p.operands = append(p.operands, name)
} }
return CalcVar{name, at}, nil return CalcVar{name, at}, nil
} }
+13 -16
View File
@@ -1,5 +1,6 @@
// Package grammar is how the template language is written: format tokens, paths and // Package grammar is how the template language is written: format tokens, paths and
// their selectors, reference sigils, names, and calc expressions. It reads strings only. // their selectors, reference sigils, names, calc expressions, and whether an argument is a
// template or a path. It reads strings only.
package grammar package grammar
import ( import (
@@ -60,12 +61,12 @@ func eachScanUnit(format string, fn func(scanUnit) error) error {
// Token is one parsed unit of a format. // Token is one parsed unit of a format.
type Token struct { type Token struct {
Kind TokenKind Kind TokenKind
Lit string // LiteralRun Lit string // LiteralRun
Body string // the braces' content, as written Body string // the braces' content, as written
Fn string // BuiltinCall Fn string // BuiltinCall
Args []string // BuiltinCall Args []string // BuiltinCall
Names []string // NameRead: the '|' arms; BuiltinCall: the operands its builtin reads Arms []string // NameRead: the '|' alternatives
// NameBind: the reference it binds, and the name. // NameBind: the reference it binds, and the name.
BoundRef, Bound string BoundRef, Bound string
} }
@@ -74,16 +75,16 @@ type TokenKind uint8
const ( const (
BuiltinCall TokenKind = iota + 1 BuiltinCall TokenKind = iota + 1
NameRead
LiteralRun LiteralRun
NameBind NameBind
NameRead
) )
const AsWord = " as " const AsWord = " as "
// ParseFormat is the one reading of a format's tokens: a '(' outside a selector makes // ParseFormat is the one reading of a format's tokens: a '(' outside a selector makes
// a token a call. operands lists the operands a call reads; nil reads none. // a token a call.
func ParseFormat(format string, operands func(fn string, args []string) []string) ([]Token, error) { func ParseFormat(format string) ([]Token, error) {
var toks []Token var toks []Token
var lit strings.Builder var lit strings.Builder
flush := func() { flush := func() {
@@ -103,18 +104,14 @@ func ParseFormat(format string, operands func(fn string, args []string) []string
return nil return nil
} }
if IndexOutside(u.body, '(') < 0 { if IndexOutside(u.body, '(') < 0 {
toks = append(toks, Token{Kind: NameRead, Body: u.body, Names: SplitOutside(u.body, '|')}) toks = append(toks, Token{Kind: NameRead, Body: u.body, Arms: SplitOutside(u.body, '|')})
return nil return nil
} }
name, args, ok := FuncCall(u.body) name, args, ok := FuncCall(u.body)
if !ok { if !ok {
return fmt.Errorf("malformed function token {%s}", u.body) return fmt.Errorf("malformed function token {%s}", u.body)
} }
tok := Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args} toks = append(toks, Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args})
if operands != nil {
tok.Names = operands(name, args)
}
toks = append(toks, tok)
return nil return nil
}) })
if err != nil { if err != nil {
+10
View File
@@ -2,6 +2,7 @@ package grammar
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
) )
@@ -160,3 +161,12 @@ func JoinSegments(segs []string) string {
} }
return b.String() return b.String()
} }
// CheckSegments refuses an unfinished path: "a." and "a...b" each have a segment naming
// nothing.
func CheckSegments(segs []string) error {
if slices.Contains(segs, "") {
return fmt.Errorf("path has an empty segment")
}
return nil
}
+2 -4
View File
@@ -32,10 +32,8 @@ func RefShape(name string) (sigil, rest string, err error) {
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
for _, seg := range segs { if err := CheckSegments(segs); err != nil {
if seg == "" { return "", "", err
return "", "", fmt.Errorf("path has an empty segment")
}
} }
return sigil, rest, nil return sigil, rest, nil
} }
+3 -5
View File
@@ -6,8 +6,6 @@ import (
"strings" "strings"
) )
// IsTemplate reports whether arg is an inline template rather than a path, by its shape;
// the root's IsTemplate documents the rule.
func IsTemplate(arg string) (bool, error) { func IsTemplate(arg string) (bool, error) {
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) { if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
if path, lone := loneReference(arg); lone { if path, lone := loneReference(arg); lone {
@@ -56,7 +54,7 @@ func loneReference(arg string) (string, bool) {
if !isString { if !isString {
return "", false return "", false
} }
toks, err := ParseFormat(format, nil) toks, err := ParseFormat(format)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -73,8 +71,8 @@ func loneReference(arg string) (string, bool) {
// loneRef is the reference a format of one reference token and nothing else reads. // loneRef is the reference a format of one reference token and nothing else reads.
func loneRef(toks []Token) (string, bool) { func loneRef(toks []Token) (string, bool) {
if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Names) != 1 || !IsRef(toks[0].Names[0]) { if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) {
return "", false return "", false
} }
return toks[0].Names[0], true return toks[0].Arms[0], true
} }
+2 -2
View File
@@ -109,8 +109,8 @@ func fieldPathTo(t, target *template) ([]string, bool) {
// calcReads reports whether a calc reads spelling as one operand. // calcReads reports whether a calc reads spelling as one operand.
func calcReads(spelling string) bool { func calcReads(spelling string) bool {
n, err := grammar.ParseCalc(spelling) c, err := grammar.ParseCalc(spelling)
v, isVar := n.(grammar.CalcVar) v, isVar := c.Expr.(grammar.CalcVar)
return err == nil && isVar && v.Name == spelling return err == nil && isVar && v.Name == spelling
} }
+6 -4
View File
@@ -90,11 +90,13 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
// stepUp proves a ".." after a row of t can step up: t has a parent, rest names it, and no // stepUp proves a ".." after a row of t can step up: t has a parent, rest names it, and no
// selector follows, since a row selected below the parent row could lie outside it. // selector follows, since a row selected below the parent row could lie outside it.
func (t *table) stepUp(rest []string) error { func (t *table) stepUp(rest []string) error {
switch { if t.parentT == nil {
case t.parentT == nil:
return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment) return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment)
case len(rest) > 0 && rest[0] == "": }
return fmt.Errorf("path has an empty segment") if err := grammar.CheckSegments(rest); err != nil {
return err
}
switch {
case len(rest) == 0 || rest[0] != t.parentT.segment: case len(rest) == 0 || rest[0] != t.parentT.segment:
return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: %s`, t.segment, t.climbTo(rest)) return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: %s`, t.segment, t.climbTo(rest))
case grammar.HasSelector(rest): case grammar.HasSelector(rest):
+1 -1
View File
@@ -210,7 +210,7 @@ func refTokens(toks []grammar.Token) []string {
var refs []string var refs []string
seen := map[string]bool{} seen := map[string]bool{}
for _, tok := range toks { for _, tok := range toks {
names := tok.Names names := tokenReads(tok)
if tok.Kind == grammar.NameBind { if tok.Kind == grammar.NameBind {
names = []string{tok.BoundRef} names = []string{tok.BoundRef}
} }
+2 -2
View File
@@ -322,7 +322,7 @@ func (t *table) checkCells() error {
// 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 {
toks, err := grammar.ParseFormat(format, builtinOperands) toks, err := grammar.ParseFormat(format)
if err != nil { if err != nil {
return err return err
} }
@@ -333,7 +333,7 @@ func (t *table) compileRowFormat(format string) error {
if tok.Kind != grammar.NameRead { if tok.Kind != grammar.NameRead {
continue continue
} }
for _, name := range tok.Names { for _, name := range tok.Arms {
a := splitArm(name, nil) a := splitArm(name, nil)
if _, ok := t.col[a.head]; !ok && !grammar.IsRef(a.head) && a.head != "" { if _, ok := t.col[a.head]; !ok && !grammar.IsRef(a.head) && a.head != "" {
return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.file, t.header) return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.file, t.header)
+11 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 110` `Next ID = 112`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -85,6 +85,7 @@
| 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 |
| 99 | 0.1.0 | decision | **Move `DataType` into `internal/datatype`, aliased by the root, and what a proof knows of a value, `proven`, into `internal/proven`.** | 3 | 4 | 2 | 7 | 3.2, 3.4 | 12.6 | | 99 | 0.1.0 | decision | **Move `DataType` into `internal/datatype`, aliased by the root, and what a proof knows of a value, `proven`, into `internal/proven`.** | 3 | 4 | 2 | 7 | 3.2, 3.4 | 12.6 |
| 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 |
| 111 | 0.1.0 | defect | **Settle goal 14.1 against the three decisions that drop source rows: amend the goal to admit a row dropped by a stated rule that serves a higher goal, or name the item that ends each decision.** | 1 | 2 | 3 | 7 | 14.1 | 9.4 |
| 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 | | 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 |
| 26 | 0.2.0 | defect | **Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives.** | 4 | 6 | 7 | 8 | 4 | 22.1 | | 26 | 0.2.0 | defect | **Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives.** | 4 | 6 | 7 | 8 | 4 | 22.1 |
| 81 | 0.2.0 | decision | **Refuse at load every `calc` operand not proven numeric and every divisor not proven nonzero, in a string column too.** | 4 | 4 | 6 | 8 | 6.1, 4 | 20.6 | | 81 | 0.2.0 | decision | **Refuse at load every `calc` operand not proven numeric and every divisor not proven nonzero, in a string column too.** | 4 | 4 | 6 | 8 | 6.1, 4 | 20.6 |
@@ -110,6 +111,7 @@
| 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 | | 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 |
| 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 | | 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 | | 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 |
| 110 | 0.2.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 |
| 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 | | 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 |
| 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 | | 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 | | 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 |
@@ -292,6 +294,10 @@ The decision "With only the shipped set, a category loads on the first call reac
Needs item 104. `linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. `linkPasses` does the same: `linkColumnRead` reads the `arm.named` that `linkNames` wrote and writes `link.readsColumn` into the link `linkTemplate` built, and `checkNameReads` reads the arms both filled. `arm` is filled in three places, `compileFormat`, `compileArms` and `linkName`, and `splitArm` runs before the link and again after it, so five readers of an earlier panel traced three phases. Technical principle: one owner per value. Needs item 104. `linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. `linkPasses` does the same: `linkColumnRead` reads the `arm.named` that `linkNames` wrote and writes `link.readsColumn` into the link `linkTemplate` built, and `checkNameReads` reads the arms both filled. `arm` is filled in three places, `compileFormat`, `compileArms` and `linkName`, and `splitArm` runs before the link and again after it, so five readers of an earlier panel traced three phases. Technical principle: one owner per value.
### 111. Settle goal 14.1 against the three decisions that drop source rows: amend the goal to admit a row dropped by a stated rule that serves a higher goal, or name the item that ends each decision.
Goal 14.1 says a table holds every row its source holds. "`misc.territory` is the spine, and a `misc` table naming a territory links to it" drops 17 `misc.timezone` rows, "A postort's kommun comes from its name, its tätort or its codes, never from distance" drops postorter it cannot place, and "A highway designation is not a street, and a US postal code belongs to the place holding most of its land inside places" drops route names and ZCTAs; none names an item ending the conflict. The README's account of goal 14, "the script drops a row only by a rule it states", already reads like the amended goal. Which way is the maintainer's call.
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go. ### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged. GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged.
@@ -387,6 +393,10 @@ Require the path step to reach a sibling category.
`readTableOptions` and `loadDir` return on the first in Go's map order. `readTableOptions` and `loadDir` return on the first in Go's map order.
### 110. 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`.
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 and a sibling path run.
### 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; the draw state and the grammar sit in `internal/`, and items 99 to 103 move what else stands alone 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; the draw state and the grammar sit in `internal/`, and items 99 to 103 move what else stands alone there", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc".
+1 -1
View File
@@ -213,7 +213,7 @@ func (p *valueProof) proveTemplate(t *template) proven {
func (p *valueProof) proveCalc(o op) proven { func (p *valueProof) proveCalc(o op) proven {
body, args := o.Body, o.Args body, args := o.Body, o.Args
expr := parsedCalc(args[0]) expr := parsedCalc(args[0]).Expr
nodes := operandNodes(o) nodes := operandNodes(o)
v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) }) v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) })
if doubt == "" && !(magnitude(v) <= calcLimit) { if doubt == "" && !(magnitude(v) <= calcLimit) {