From 6fd7da19e5982a6630c003cfc1becf67420a617b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 12:25:22 +0200 Subject: [PATCH] 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 --- README.md | 2 +- calc.go | 22 +++++++------- format.go | 38 +++++++++++------------ internal/grammar/calc.go | 57 +++++++++++++---------------------- internal/grammar/format.go | 29 ++++++++---------- internal/grammar/path.go | 10 ++++++ internal/grammar/reference.go | 6 ++-- internal/grammar/template.go | 8 ++--- namefence.go | 4 +-- path.go | 10 +++--- reference.go | 2 +- table.go | 4 +-- todo.md | 12 +++++++- valueproof.go | 2 +- 14 files changed, 103 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index d9d735f..d4d6c32 100644 --- a/README.md +++ b/README.md @@ -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 cmd/fejkdata/ the fejkdata CLI 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-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 diff --git a/calc.go b/calc.go index b0d5330..ced49ca 100644 --- a/calc.go +++ b/calc.go @@ -51,11 +51,11 @@ func checkCalc(fields map[string]node, args []string) error { if len(args) < 1 || len(args) > 2 { 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 { 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 { 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. // 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 { - for _, name := range grammar.CalcVars(expr) { +func checkOperands(text string, c grammar.Calc, operand func(name string) []node) error { + for _, name := range c.Operands { if rendered, never := allNeverNumeric(operand(name)); never { 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 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 // both args valid, so no step here can fail. func calcPrep(args []string) callFn { - expr := parsedCalc(args[0]) + expr := parsedCalc(args[0]).Expr dp := calcDecimals(args) return func(_ *drawstate.State, _ string, operands []string) string { 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 // no message. -func parsedCalc(expr string) grammar.CalcNode { - n, err := grammar.ParseCalc(expr) +func parsedCalc(expr string) grammar.Calc { + c, err := grammar.ParseCalc(expr) if err != nil { 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. @@ -233,9 +233,9 @@ func calcOperands(args []string) []string { if len(args) == 0 { return nil } - expr, err := grammar.ParseCalc(args[0]) + c, err := grammar.ParseCalc(args[0]) if err != nil { return nil } - return grammar.CalcVars(expr) + return c.Operands } diff --git a/format.go b/format.go index af233db..2ff008a 100644 --- a/format.go +++ b/format.go @@ -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) { - toks, err := grammar.ParseFormat(format, builtinOperands) + toks, err := grammar.ParseFormat(format) if err != nil { 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 // 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 } @@ -119,14 +119,15 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error) // field, returning those whose head no field holds. func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unboundRead, error) { var unbound []unboundRead - for _, name := range t.Names { + names := tokenReads(t) + for _, name := range names { if grammar.IsRef(name) { if _, _, err := grammar.RefShape(name); err != nil { return nil, fmt.Errorf("token {%s}: %w", t.Body, err) } 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 { continue } @@ -214,6 +215,14 @@ func hintableRef(name string) bool { 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 // reads none or is unknown. 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, ".")} } -// checkSegments rejects an unfinished path: "{a.}" and "{a...b}" each have a -// segment naming nothing. A field really named "" would otherwise make them -// resolve, so a typo would read as a path that worked. +// checkSegments refuses an empty segment in a path, which a field really named "" would +// otherwise resolve, so a typo would read as a path that worked. func checkSegments(a arm) error { if len(a.tail) == 0 { return nil } - if a.head == "" { - 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 + return grammar.CheckSegments(append([]string{a.head}, a.tail...)) } // 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) { var operands []arm - for _, operand := range tok.Names { + for _, operand := range builtinOperands(tok.Fn, tok.Args) { a := splitArm(operand, refs) if isName(a.head) { 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) { - arms := make([]arm, len(tok.Names)) - for i, name := range tok.Names { + arms := make([]arm, len(tok.Arms)) + for i, name := range tok.Arms { arms[i] = splitArm(name, refs) if isName(arms[i].head) { arms[i].kind = namedRead diff --git a/internal/grammar/calc.go b/internal/grammar/calc.go index 6b69b09..8ecca45 100644 --- a/internal/grammar/calc.go +++ b/internal/grammar/calc.go @@ -12,8 +12,14 @@ type CalcNode interface{ calcNode() } type CalcNum float64 // a number literal -// CalcVar is an operand, a sibling field or a name. At is its position among the -// distinct operands in the order the expression first names each, which CalcVars lists. +// Calc is a parsed calc expression and the distinct operands it reads, in the order it +// 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 { Name string At int @@ -31,27 +37,6 @@ func (CalcVar) calcNode() {} func (CalcNeg) 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. func CalcText(n CalcNode) string { switch n := n.(type) { @@ -70,25 +55,25 @@ func CalcText(n CalcNode) string { // calcParser is a recursive-descent parser over the expression runes, threading // expr -> term -> factor for the standard * / before + - precedence. type calcParser struct { - rs []rune - pos int - vars map[string]int + rs []rune + pos int + operands []string } // ParseCalc parses a whole expression, requiring it to consume all input. -func ParseCalc(expr string) (CalcNode, error) { - p := &calcParser{rs: []rune(expr), vars: map[string]int{}} +func ParseCalc(expr string) (Calc, error) { + p := &calcParser{rs: []rune(expr)} if p.space(); p.pos >= len(p.rs) { - return nil, fmt.Errorf("empty expression") + return Calc{}, fmt.Errorf("empty expression") } n, err := p.expr() if err != nil { - return nil, err + return Calc{}, err } 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() { @@ -185,10 +170,10 @@ func (p *calcParser) ident() (CalcNode, error) { } } name := string(p.rs[start:p.pos]) - at, seen := p.vars[name] - if !seen { - at = len(p.vars) - p.vars[name] = at + at := slices.Index(p.operands, name) + if at < 0 { + at = len(p.operands) + p.operands = append(p.operands, name) } return CalcVar{name, at}, nil } diff --git a/internal/grammar/format.go b/internal/grammar/format.go index 4f7b7a4..de84682 100644 --- a/internal/grammar/format.go +++ b/internal/grammar/format.go @@ -1,5 +1,6 @@ // 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 import ( @@ -60,12 +61,12 @@ func eachScanUnit(format string, fn func(scanUnit) error) error { // Token is one parsed unit of a format. type Token struct { - Kind TokenKind - Lit string // LiteralRun - Body string // the braces' content, as written - Fn string // BuiltinCall - Args []string // BuiltinCall - Names []string // NameRead: the '|' arms; BuiltinCall: the operands its builtin reads + Kind TokenKind + Lit string // LiteralRun + Body string // the braces' content, as written + Fn string // BuiltinCall + Args []string // BuiltinCall + Arms []string // NameRead: the '|' alternatives // NameBind: the reference it binds, and the name. BoundRef, Bound string } @@ -74,16 +75,16 @@ type TokenKind uint8 const ( BuiltinCall TokenKind = iota + 1 - NameRead LiteralRun NameBind + NameRead ) const AsWord = " as " // 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. -func ParseFormat(format string, operands func(fn string, args []string) []string) ([]Token, error) { +// a token a call. +func ParseFormat(format string) ([]Token, error) { var toks []Token var lit strings.Builder flush := func() { @@ -103,18 +104,14 @@ func ParseFormat(format string, operands func(fn string, args []string) []string return nil } 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 } name, args, ok := FuncCall(u.body) if !ok { return fmt.Errorf("malformed function token {%s}", u.body) } - tok := Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args} - if operands != nil { - tok.Names = operands(name, args) - } - toks = append(toks, tok) + toks = append(toks, Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args}) return nil }) if err != nil { diff --git a/internal/grammar/path.go b/internal/grammar/path.go index 3438a8f..8838d2f 100644 --- a/internal/grammar/path.go +++ b/internal/grammar/path.go @@ -2,6 +2,7 @@ package grammar import ( "fmt" + "slices" "strings" ) @@ -160,3 +161,12 @@ func JoinSegments(segs []string) 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 +} diff --git a/internal/grammar/reference.go b/internal/grammar/reference.go index 69b7ea9..26bc93a 100644 --- a/internal/grammar/reference.go +++ b/internal/grammar/reference.go @@ -32,10 +32,8 @@ func RefShape(name string) (sigil, rest string, err error) { if err != nil { return "", "", err } - for _, seg := range segs { - if seg == "" { - return "", "", fmt.Errorf("path has an empty segment") - } + if err := CheckSegments(segs); err != nil { + return "", "", err } return sigil, rest, nil } diff --git a/internal/grammar/template.go b/internal/grammar/template.go index fc49929..d8af71c 100644 --- a/internal/grammar/template.go +++ b/internal/grammar/template.go @@ -6,8 +6,6 @@ import ( "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) { if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) { if path, lone := loneReference(arg); lone { @@ -56,7 +54,7 @@ func loneReference(arg string) (string, bool) { if !isString { return "", false } - toks, err := ParseFormat(format, nil) + toks, err := ParseFormat(format) if err != nil { 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. 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 toks[0].Names[0], true + return toks[0].Arms[0], true } diff --git a/namefence.go b/namefence.go index be39c95..1ead9ec 100644 --- a/namefence.go +++ b/namefence.go @@ -109,8 +109,8 @@ func fieldPathTo(t, target *template) ([]string, bool) { // calcReads reports whether a calc reads spelling as one operand. func calcReads(spelling string) bool { - n, err := grammar.ParseCalc(spelling) - v, isVar := n.(grammar.CalcVar) + c, err := grammar.ParseCalc(spelling) + v, isVar := c.Expr.(grammar.CalcVar) return err == nil && isVar && v.Name == spelling } diff --git a/path.go b/path.go index 5d1b6d5..83a1075 100644 --- a/path.go +++ b/path.go @@ -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 // selector follows, since a row selected below the parent row could lie outside it. func (t *table) stepUp(rest []string) error { - switch { - case t.parentT == nil: + if t.parentT == nil { 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: 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): diff --git a/reference.go b/reference.go index ab6ece8..f9b882f 100644 --- a/reference.go +++ b/reference.go @@ -210,7 +210,7 @@ func refTokens(toks []grammar.Token) []string { var refs []string seen := map[string]bool{} for _, tok := range toks { - names := tok.Names + names := tokenReads(tok) if tok.Kind == grammar.NameBind { names = []string{tok.BoundRef} } diff --git a/table.go b/table.go index 58e9eeb..36b678c 100644 --- a/table.go +++ b/table.go @@ -322,7 +322,7 @@ func (t *table) checkCells() error { // compileRowFormat compiles the format a row renders through, over the columns as its // fields. func (t *table) compileRowFormat(format string) error { - toks, err := grammar.ParseFormat(format, builtinOperands) + toks, err := grammar.ParseFormat(format) if err != nil { return err } @@ -333,7 +333,7 @@ func (t *table) compileRowFormat(format string) error { if tok.Kind != grammar.NameRead { continue } - for _, name := range tok.Names { + for _, name := range tok.Arms { a := splitArm(name, nil) 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) diff --git a/todo.md b/todo.md index 0b1db7e..503c456 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 110` +`Next ID = 112` | 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 | | 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 | +| 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 | | 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 | @@ -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 | | 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 | +| 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 | | 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 | @@ -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. +### 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. 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. +### 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. 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". diff --git a/valueproof.go b/valueproof.go index 9f93c5a..444c636 100644 --- a/valueproof.go +++ b/valueproof.go @@ -213,7 +213,7 @@ func (p *valueProof) proveTemplate(t *template) proven { func (p *valueProof) proveCalc(o op) proven { body, args := o.Body, o.Args - expr := parsedCalc(args[0]) + expr := parsedCalc(args[0]).Expr nodes := operandNodes(o) v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) }) if doubt == "" && !(magnitude(v) <= calcLimit) {