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