Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -6,45 +6,33 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
// evalCalc evaluates an expression over the operand values expand read, so the
|
||||||
// read, so the evaluator touches neither the rng nor the node tree.
|
// evaluator touches neither the rng nor the node tree.
|
||||||
type calcNode interface {
|
func evalCalc(n grammar.CalcNode, operands []string) float64 {
|
||||||
eval(operands []string) float64
|
switch n := n.(type) {
|
||||||
}
|
case grammar.CalcNum:
|
||||||
|
return float64(n)
|
||||||
type calcNum float64 // a number literal
|
case grammar.CalcVar:
|
||||||
type calcVar string // an operand, a sibling field or a name, before indexVars places it
|
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n.At]), 64)
|
||||||
type calcIdx int // an operand, by its position in the values expand read
|
if err != nil {
|
||||||
type calcNeg struct{ x calcNode }
|
return math.NaN() // a non-numeric operand stays visible, never an error
|
||||||
type calcBin struct { // a + - * / b
|
}
|
||||||
operator byte
|
return v
|
||||||
l, r calcNode
|
case grammar.CalcNeg:
|
||||||
}
|
return -evalCalc(n.X, operands)
|
||||||
|
case grammar.CalcBin:
|
||||||
func (n calcNum) eval([]string) float64 { return float64(n) }
|
return arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands))
|
||||||
|
|
||||||
func (n calcIdx) eval(operands []string) float64 {
|
|
||||||
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n]), 64)
|
|
||||||
if err != nil {
|
|
||||||
return math.NaN() // a non-numeric operand stays visible, never an error
|
|
||||||
}
|
}
|
||||||
return v
|
panic(internalError("calc node %#v has no evaluation", n))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n calcVar) eval([]string) float64 {
|
func arith(operator byte, l, r float64) float64 {
|
||||||
panic(internalError("calc operand %q was never placed", string(n)))
|
switch operator {
|
||||||
}
|
|
||||||
|
|
||||||
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
|
||||||
|
|
||||||
func (n calcBin) eval(operands []string) float64 {
|
|
||||||
l, r := n.l.eval(operands), n.r.eval(operands)
|
|
||||||
switch n.operator {
|
|
||||||
case '+':
|
case '+':
|
||||||
return l + r
|
return l + r
|
||||||
case '-':
|
case '-':
|
||||||
@@ -63,7 +51,7 @@ 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 := parseCalc(args[0])
|
expr, 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)
|
||||||
}
|
}
|
||||||
@@ -90,11 +78,11 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
// checkCalcNames holds each calc of t reading a name to the checks checkCalc makes of a field.
|
// checkCalcNames holds each calc of t reading a name to the checks checkCalc makes of a field.
|
||||||
func checkCalcNames(path string, t *template) error {
|
func checkCalcNames(path string, t *template) error {
|
||||||
for _, o := range t.compiled.ops {
|
for _, o := range t.compiled.ops {
|
||||||
if o.fn != "calc" || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
|
if o.Fn != "calc" || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := checkOperands(o.args[0], parsedCalc(o.args[0]), operandNodes(o)); err != nil {
|
if err := checkOperands(o.Args[0], parsedCalc(o.Args[0]), operandNodes(o)); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
|
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -113,8 +101,8 @@ 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 calcNode, operand func(name string) []node) error {
|
func checkOperands(text string, expr grammar.CalcNode, operand func(name string) []node) error {
|
||||||
for _, name := range calcVars(expr) {
|
for _, name := range grammar.CalcVars(expr) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -127,31 +115,31 @@ func checkOperands(text string, expr calcNode, operand func(name string) []node)
|
|||||||
|
|
||||||
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
||||||
// literals and fixed operands folded, anything that varies left unknown.
|
// literals and fixed operands folded, anything that varies left unknown.
|
||||||
func constantZeroDivisor(n calcNode, operand func(string) []node) (string, bool) {
|
func constantZeroDivisor(n grammar.CalcNode, operand func(string) []node) (string, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNeg:
|
case grammar.CalcNeg:
|
||||||
return constantZeroDivisor(n.x, operand)
|
return constantZeroDivisor(n.X, operand)
|
||||||
case calcBin:
|
case grammar.CalcBin:
|
||||||
if n.operator == '/' {
|
if n.Operator == '/' {
|
||||||
if v, known := constantValue(n.r, operand); known && v == 0 {
|
if v, known := constantValue(n.R, operand); known && v == 0 {
|
||||||
return calcText(n.r), true
|
return grammar.CalcText(n.R), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d, zero := constantZeroDivisor(n.l, operand); zero {
|
if d, zero := constantZeroDivisor(n.L, operand); zero {
|
||||||
return d, true
|
return d, true
|
||||||
}
|
}
|
||||||
return constantZeroDivisor(n.r, operand)
|
return constantZeroDivisor(n.R, operand)
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// constantValue evaluates an expression whose every operand is fixed.
|
// constantValue evaluates an expression whose every operand is fixed.
|
||||||
func constantValue(n calcNode, operand func(string) []node) (float64, bool) {
|
func constantValue(n grammar.CalcNode, operand func(string) []node) (float64, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNum:
|
case grammar.CalcNum:
|
||||||
return float64(n), true
|
return float64(n), true
|
||||||
case calcVar:
|
case grammar.CalcVar:
|
||||||
nodes := operand(string(n))
|
nodes := operand(n.Name)
|
||||||
if len(nodes) != 1 {
|
if len(nodes) != 1 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -165,35 +153,20 @@ func constantValue(n calcNode, operand func(string) []node) (float64, bool) {
|
|||||||
}
|
}
|
||||||
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
|
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
|
||||||
return v, err == nil
|
return v, err == nil
|
||||||
case calcNeg:
|
case grammar.CalcNeg:
|
||||||
v, ok := constantValue(n.x, operand)
|
v, ok := constantValue(n.X, operand)
|
||||||
return -v, ok
|
return -v, ok
|
||||||
case calcBin:
|
case grammar.CalcBin:
|
||||||
l, lok := constantValue(n.l, operand)
|
l, lok := constantValue(n.L, operand)
|
||||||
r, rok := constantValue(n.r, operand)
|
r, rok := constantValue(n.R, operand)
|
||||||
if !lok || !rok {
|
if !lok || !rok {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return calcBin{n.operator, calcNum(l), calcNum(r)}.eval(nil), true
|
return arith(n.Operator, l, r), true
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcText spells an expression node the way an author would read it.
|
|
||||||
func calcText(n calcNode) string {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case calcNum:
|
|
||||||
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
|
||||||
case calcVar:
|
|
||||||
return string(n)
|
|
||||||
case calcNeg:
|
|
||||||
return "-" + calcText(n.x)
|
|
||||||
case calcBin:
|
|
||||||
return "(" + calcText(n.l) + " " + string(n.operator) + " " + calcText(n.r) + ")"
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
// neverNumeric reports a node no render of which is a number: a null, fixed text that
|
// neverNumeric reports a node no render of which is a number: a null, fixed text that
|
||||||
// does not parse, or a choice of only such items. text is one such render.
|
// does not parse, or a choice of only such items. text is one such render.
|
||||||
func neverNumeric(n node) (text string, never bool) {
|
func neverNumeric(n node) (text string, never bool) {
|
||||||
@@ -226,26 +199,20 @@ func allNeverNumeric(nodes []node) (text string, never bool) {
|
|||||||
return text, len(nodes) > 0
|
return text, len(nodes) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcPrep parses the expression and decimals once, at compile time, and places each
|
// calcPrep parses the expression and decimals once, at compile time. checkCalc proved
|
||||||
// operand name at the position expand will read it into. checkCalc proved both args
|
// both args valid, so no step here can fail.
|
||||||
// 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])
|
||||||
at := make(map[string]int)
|
|
||||||
for i, name := range calcVars(expr) {
|
|
||||||
at[name] = i
|
|
||||||
}
|
|
||||||
placed := indexVars(expr, at)
|
|
||||||
dp := calcDecimals(args)
|
dp := calcDecimals(args)
|
||||||
return func(_ *drawstate.State, _ string, operands []string) string {
|
return func(_ *drawstate.State, _ string, operands []string) string {
|
||||||
return formatFloat(placed.eval(operands), dp)
|
return formatFloat(evalCalc(expr, operands), dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) calcNode {
|
func parsedCalc(expr string) grammar.CalcNode {
|
||||||
n, err := parseCalc(expr)
|
n, 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))
|
||||||
}
|
}
|
||||||
@@ -260,186 +227,15 @@ func calcDecimals(args []string) int {
|
|||||||
return shortestDecimals
|
return shortestDecimals
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexVars replaces each operand name with its position in the values expand reads.
|
|
||||||
// Both sides take that order from calcVars, so they cannot drift.
|
|
||||||
func indexVars(n calcNode, at map[string]int) calcNode {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case calcVar:
|
|
||||||
i, placed := at[string(n)]
|
|
||||||
if !placed { // calcVars named every operand, so a miss means the two disagree
|
|
||||||
panic(internalError("calc operand %q is not among the names read for it", string(n)))
|
|
||||||
}
|
|
||||||
return calcIdx(i)
|
|
||||||
case calcNeg:
|
|
||||||
return calcNeg{indexVars(n.x, at)}
|
|
||||||
case calcBin:
|
|
||||||
return calcBin{n.operator, indexVars(n.l, at), indexVars(n.r, at)}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcOperands lists the operands a calc's args read, fields or names. checkCalc reports
|
// calcOperands lists the operands a calc's args read, fields or names. checkCalc reports
|
||||||
// an expression that does not parse, so one that does not simply names nothing.
|
// an expression that does not parse, so one that does not simply names nothing.
|
||||||
func calcOperands(args []string) []string {
|
func calcOperands(args []string) []string {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
expr, err := parseCalc(args[0])
|
expr, err := grammar.ParseCalc(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return calcVars(expr)
|
return grammar.CalcVars(expr)
|
||||||
}
|
|
||||||
|
|
||||||
// calcVars lists the distinct operands an expression reads, in the order it first
|
|
||||||
// names each. That order is the contract between expand, which reads the operands
|
|
||||||
// into a slice, and indexVars, which places each name at its position in it.
|
|
||||||
func calcVars(n calcNode) []string {
|
|
||||||
var out []string
|
|
||||||
seen := map[string]bool{}
|
|
||||||
var walk func(calcNode)
|
|
||||||
walk = func(n calcNode) {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case calcVar:
|
|
||||||
if name := string(n); !seen[name] {
|
|
||||||
seen[name] = true
|
|
||||||
out = append(out, name)
|
|
||||||
}
|
|
||||||
case calcNeg:
|
|
||||||
walk(n.x)
|
|
||||||
case calcBin:
|
|
||||||
walk(n.l)
|
|
||||||
walk(n.r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk(n)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCalc parses a whole expression, requiring it to consume all input.
|
|
||||||
func parseCalc(expr string) (calcNode, error) {
|
|
||||||
p := &calcParser{rs: []rune(expr)}
|
|
||||||
if p.space(); p.pos >= len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("empty expression")
|
|
||||||
}
|
|
||||||
n, err := p.expr()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.space(); p.pos != len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) space() {
|
|
||||||
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
|
||||||
p.pos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) expr() (calcNode, error) { return p.binary(p.term, '+', '-') }
|
|
||||||
func (p *calcParser) term() (calcNode, error) { return p.binary(p.factor, '*', '/') }
|
|
||||||
|
|
||||||
// binary parses a left-associative run of next() operands joined by the given
|
|
||||||
// operators, the one shape expr and term share.
|
|
||||||
func (p *calcParser) binary(next func() (calcNode, error), ops ...byte) (calcNode, error) {
|
|
||||||
n, err := next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
p.space()
|
|
||||||
if p.pos >= len(p.rs) || !contains(ops, byte(p.rs[p.pos])) {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
op := byte(p.rs[p.pos])
|
|
||||||
p.pos++
|
|
||||||
r, err := next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
n = calcBin{op, n, r}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) factor() (calcNode, error) {
|
|
||||||
p.space()
|
|
||||||
if p.pos >= len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("unexpected end of expression")
|
|
||||||
}
|
|
||||||
switch c := p.rs[p.pos]; {
|
|
||||||
case c == '-':
|
|
||||||
p.pos++
|
|
||||||
x, err := p.factor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return calcNeg{x}, nil
|
|
||||||
case c == '(':
|
|
||||||
p.pos++
|
|
||||||
n, err := p.expr()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
|
||||||
return nil, fmt.Errorf("missing ')'")
|
|
||||||
}
|
|
||||||
p.pos++
|
|
||||||
return n, nil
|
|
||||||
case c == '.' || c >= '0' && c <= '9':
|
|
||||||
return p.number()
|
|
||||||
case c == '_' || unicode.IsLetter(c):
|
|
||||||
return p.ident()
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected %q", string(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) number() (calcNode, error) {
|
|
||||||
start, dot := p.pos, false
|
|
||||||
for p.pos < len(p.rs) {
|
|
||||||
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
|
||||||
p.pos++
|
|
||||||
} else if c == '.' && !dot {
|
|
||||||
dot, p.pos = true, p.pos+1
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
|
||||||
}
|
|
||||||
return calcNum(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ident reads a field or name: a letter or '_', then letters, digits or '_'. A '-'
|
|
||||||
// is always the minus operator, so a hyphenated one can't be an operand.
|
|
||||||
func (p *calcParser) ident() (calcNode, error) {
|
|
||||||
start := p.pos
|
|
||||||
for p.pos < len(p.rs) {
|
|
||||||
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
|
||||||
p.pos++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calcVar(string(p.rs[start:p.pos])), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(bs []byte, b byte) bool {
|
|
||||||
for _, x := range bs {
|
|
||||||
if x == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dataSource is one tree to load: an fs.FS and the directory in it to start from.
|
// dataSource is one tree to load: an fs.FS and the directory in it to start from.
|
||||||
@@ -132,7 +134,7 @@ func loadFolder(src dataSource, g *folder, full, name string) error {
|
|||||||
if len(child.children) == 0 {
|
if len(child.children) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := checkName(name); err != nil {
|
if err := grammar.CheckName(name); err != nil {
|
||||||
return fmt.Errorf("%s: folder %w", src.labelled(full), err)
|
return fmt.Errorf("%s: folder %w", src.labelled(full), err)
|
||||||
}
|
}
|
||||||
g.children[name] = child
|
g.children[name] = child
|
||||||
@@ -146,7 +148,7 @@ func loadFile(src dataSource, g *folder, full, file string, files *categoryFiles
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
name := strings.TrimSuffix(file, ".json")
|
name := strings.TrimSuffix(file, ".json")
|
||||||
if err := checkName(name); err != nil {
|
if err := grammar.CheckName(name); err != nil {
|
||||||
return fmt.Errorf("%s: category %w", src.labelled(full), err)
|
return fmt.Errorf("%s: category %w", src.labelled(full), err)
|
||||||
}
|
}
|
||||||
b, err := fs.ReadFile(src.fsys, full)
|
b, err := fs.ReadFile(src.fsys, full)
|
||||||
|
|||||||
@@ -5,121 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// scanUnit is one unit of a scanned format string: a literal rune or the body of a
|
|
||||||
// {…} token.
|
|
||||||
type scanUnit struct {
|
|
||||||
isToken bool // a {…} body, else a literal rune
|
|
||||||
char rune
|
|
||||||
body string
|
|
||||||
}
|
|
||||||
|
|
||||||
// eachScanUnit scans a format string once and calls fn for each unit, the single
|
|
||||||
// source of truth for how braces are read: "{{" and "}}" are literal braces, a "{"
|
|
||||||
// opens a token that must reach its "}", and a lone "}" is an error.
|
|
||||||
func eachScanUnit(format string, fn func(scanUnit) error) error {
|
|
||||||
rs := []rune(format)
|
|
||||||
for i := 0; i < len(rs); i++ {
|
|
||||||
var u scanUnit
|
|
||||||
switch c := rs[i]; c {
|
|
||||||
case '{':
|
|
||||||
if i+1 < len(rs) && rs[i+1] == '{' {
|
|
||||||
u.char = '{'
|
|
||||||
i++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
end := i + 1
|
|
||||||
for end < len(rs) && rs[end] != '}' {
|
|
||||||
if rs[end] == '{' {
|
|
||||||
return fmt.Errorf("'{' inside a token in %q; a literal brace is written {{", format)
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
if end >= len(rs) {
|
|
||||||
return fmt.Errorf("unterminated '{' in %q", format)
|
|
||||||
}
|
|
||||||
u.isToken, u.body = true, string(rs[i+1:end])
|
|
||||||
i = end
|
|
||||||
case '}':
|
|
||||||
if i+1 < len(rs) && rs[i+1] == '}' {
|
|
||||||
u.char = '}'
|
|
||||||
i++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return fmt.Errorf("lone '}' in %q; a literal brace is written }}", format)
|
|
||||||
default:
|
|
||||||
u.char = c
|
|
||||||
}
|
|
||||||
if err := fn(u); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatToken is one parsed unit of a format.
|
|
||||||
type formatToken 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
|
|
||||||
// nameBind: the reference it binds, and the name.
|
|
||||||
boundRef, bound string
|
|
||||||
}
|
|
||||||
|
|
||||||
type tokenKind uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
builtinCall tokenKind = iota + 1
|
|
||||||
nameRead
|
|
||||||
literalRun
|
|
||||||
nameBind
|
|
||||||
)
|
|
||||||
|
|
||||||
const asWord = " as "
|
|
||||||
|
|
||||||
// parseFormat is the one reading of a format's tokens: a '(' outside a selector makes
|
|
||||||
// a token a call.
|
|
||||||
func parseFormat(format string) ([]formatToken, error) {
|
|
||||||
var toks []formatToken
|
|
||||||
var lit strings.Builder
|
|
||||||
flush := func() {
|
|
||||||
if lit.Len() > 0 {
|
|
||||||
toks = append(toks, formatToken{kind: literalRun, lit: lit.String()})
|
|
||||||
lit.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := eachScanUnit(format, func(u scanUnit) error {
|
|
||||||
if !u.isToken {
|
|
||||||
lit.WriteRune(u.char)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
flush()
|
|
||||||
if ref, name, binds := cutOutside(u.body, asWord); binds && indexOutside(u.body, '(') < 0 {
|
|
||||||
toks = append(toks, formatToken{kind: nameBind, body: u.body, boundRef: ref, bound: name})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if indexOutside(u.body, '(') < 0 {
|
|
||||||
toks = append(toks, formatToken{kind: nameRead, body: u.body, names: splitOutside(u.body, '|')})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
name, args, ok := funcCall(u.body)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("malformed function token {%s}", u.body)
|
|
||||||
}
|
|
||||||
toks = append(toks, formatToken{kind: builtinCall, body: u.body, fn: name, args: args, names: builtinOperands(name, args)})
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
flush()
|
|
||||||
return toks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// builtin is a format-string function invoked as {name(args)}. It receives the
|
// builtin is a format-string function invoked as {name(args)}. It receives the
|
||||||
// draw state, the output emitted so far in the current expansion (for derivations
|
// draw state, the output emitted so far in the current expansion (for derivations
|
||||||
// such as a checksum over preceding digits), and the values of the operands it named
|
// such as a checksum over preceding digits), and the values of the operands it named
|
||||||
@@ -146,40 +34,6 @@ type builtin struct {
|
|||||||
prints DataType
|
prints DataType
|
||||||
}
|
}
|
||||||
|
|
||||||
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
|
||||||
// for a name-read body. A '(' without a trailing ')' yields ok=false.
|
|
||||||
func funcCall(body string) (name string, args []string, ok bool) {
|
|
||||||
lp := indexOutside(body, '(')
|
|
||||||
if lp < 0 || !strings.HasSuffix(body, ")") {
|
|
||||||
return "", nil, false
|
|
||||||
}
|
|
||||||
return body[:lp], splitArgs(body[lp+1 : len(body)-1]), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitArgs parses a function arg list: comma-separated outside a selector or a
|
|
||||||
// quoted layout, trimmed; empty -> none.
|
|
||||||
func splitArgs(s string) []string {
|
|
||||||
if strings.TrimSpace(s) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var args []string
|
|
||||||
depth, quoted, start := 0, false, 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
switch c := s[i]; {
|
|
||||||
case c == '\'' && depth == 0:
|
|
||||||
quoted = !quoted
|
|
||||||
case quoted:
|
|
||||||
case c == '[':
|
|
||||||
depth++
|
|
||||||
case c == ']' && depth > 0:
|
|
||||||
depth--
|
|
||||||
case c == ',' && depth == 0:
|
|
||||||
args, start = append(args, strings.TrimSpace(s[start:i])), i+1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return append(args, strings.TrimSpace(s[start:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func plural(n int) string {
|
func plural(n int) string {
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return ""
|
return ""
|
||||||
@@ -190,8 +44,8 @@ func plural(n int) string {
|
|||||||
// checkFunc validates a call at compile time: naming a known builtin, with the arg
|
// checkFunc validates a call at compile time: naming a known builtin, with the arg
|
||||||
// count that builtin takes and args its check accepts. fields is passed through for
|
// count that builtin takes and args its check accepts. fields is passed through for
|
||||||
// the builtins (calc, the transforms) that validate against them.
|
// the builtins (calc, the transforms) that validate against them.
|
||||||
func checkFunc(tok formatToken, fields map[string]node) error {
|
func checkFunc(tok grammar.Token, fields map[string]node) error {
|
||||||
body, name, args := tok.body, tok.fn, tok.args
|
body, name, args := tok.Body, tok.Fn, tok.Args
|
||||||
b, known := builtins[name]
|
b, known := builtins[name]
|
||||||
if !known {
|
if !known {
|
||||||
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
||||||
@@ -207,8 +61,8 @@ func checkFunc(tok formatToken, fields map[string]node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseChecked(format string, fields map[string]node) ([]formatToken, []unboundRead, error) {
|
func parseChecked(format string, fields map[string]node) ([]grammar.Token, []unboundRead, error) {
|
||||||
toks, err := parseFormat(format)
|
toks, err := grammar.ParseFormat(format, builtinOperands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -228,7 +82,7 @@ type unboundRead struct {
|
|||||||
|
|
||||||
// checkTokens proves every token's grammar, functions and field paths, and returns the reads
|
// checkTokens proves every token's grammar, functions and field paths, and returns the reads
|
||||||
// whose head no field holds, for bindNames to look up among the names.
|
// whose head no field holds, for bindNames to look up among the names.
|
||||||
func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, error) {
|
func checkTokens(toks []grammar.Token, fields map[string]node) ([]unboundRead, error) {
|
||||||
var unbound []unboundRead
|
var unbound []unboundRead
|
||||||
for _, t := range toks {
|
for _, t := range toks {
|
||||||
u, err := checkToken(t, fields)
|
u, err := checkToken(t, fields)
|
||||||
@@ -240,70 +94,70 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
|
|||||||
return unbound, nil
|
return unbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
|
func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error) {
|
||||||
switch t.kind {
|
switch t.Kind {
|
||||||
case builtinCall:
|
case grammar.BuiltinCall:
|
||||||
if err := checkFunc(t, fields); err != nil {
|
if err := checkFunc(t, fields); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return checkReads(t, fields, true)
|
return checkReads(t, fields, true)
|
||||||
case nameBind:
|
case grammar.NameBind:
|
||||||
return nil, checkBind(t, fields)
|
return nil, checkBind(t, fields)
|
||||||
case nameRead:
|
case grammar.NameRead:
|
||||||
unbound, err := checkReads(t, fields, false)
|
unbound, err := checkReads(t, fields, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 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.Names)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkReads proves each name t reads, an arm or an operand, is a reference or a path into a
|
// checkReads proves each name t reads, an arm or an operand, is a reference or a path into a
|
||||||
// field, returning those whose head no field holds.
|
// field, returning those whose head no field holds.
|
||||||
func checkReads(t formatToken, 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 {
|
for _, name := range t.Names {
|
||||||
if isRef(name) {
|
if grammar.IsRef(name) {
|
||||||
if _, _, err := 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(t.Names) == 1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = fmt.Errorf("token {%s}: %w", t.body, err)
|
err = fmt.Errorf("token {%s}: %w", t.Body, err)
|
||||||
if !missing {
|
if !missing {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
a := splitArm(name, nil)
|
a := splitArm(name, nil)
|
||||||
unbound = append(unbound, unboundRead{head: a.head, tail: joinSegments(a.tail), body: t.body, operand: operands, noRef: operands && builtins[t.fn].noRefOperands, err: err})
|
unbound = append(unbound, unboundRead{head: a.head, tail: grammar.JoinSegments(a.tail), body: t.Body, operand: operands, noRef: operands && builtins[t.Fn].noRefOperands, err: err})
|
||||||
}
|
}
|
||||||
return unbound, nil
|
return unbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkBind proves a {x as name} token is spelled once, binds a reference or a path into a
|
// checkBind proves a {x as name} token is spelled once, binds a reference or a path into a
|
||||||
// field, and names a valid name that is no option.
|
// field, and names a valid name that is no option.
|
||||||
func checkBind(t formatToken, fields map[string]node) error {
|
func checkBind(t grammar.Token, fields map[string]node) error {
|
||||||
ref, name := t.boundRef, t.bound
|
ref, name := t.BoundRef, t.Bound
|
||||||
if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body {
|
if trimmed := strings.TrimSpace(ref) + grammar.AsWord + strings.TrimSpace(name); trimmed != t.Body {
|
||||||
return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed)
|
return fmt.Errorf("token {%s}: write {%s}", t.Body, trimmed)
|
||||||
}
|
}
|
||||||
if err := checkBound(ref, fields); err != nil {
|
if err := checkBound(ref, fields); err != nil {
|
||||||
return fmt.Errorf("token {%s}: %w", t.body, err)
|
return fmt.Errorf("token {%s}: %w", t.Body, err)
|
||||||
}
|
}
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("token {%s}: a binding names nothing; write {%s as n}", t.body, ref)
|
return fmt.Errorf("token {%s}: a binding names nothing; write {%s as n}", t.Body, ref)
|
||||||
}
|
}
|
||||||
if isOption(name) {
|
if isOption(name) {
|
||||||
return fmt.Errorf("token {%s}: %q is an option and can never be a name; rename it", t.body, name)
|
return fmt.Errorf("token {%s}: %q is an option and can never be a name; rename it", t.Body, name)
|
||||||
}
|
}
|
||||||
if err := checkName(name); err != nil {
|
if err := grammar.CheckName(name); err != nil {
|
||||||
return fmt.Errorf("token {%s}: name %w", t.body, err)
|
return fmt.Errorf("token {%s}: name %w", t.Body, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -311,8 +165,8 @@ func checkBind(t formatToken, fields map[string]node) error {
|
|||||||
// checkBound proves what a binding picks is a reference or a path into a field of the binding
|
// checkBound proves what a binding picks is a reference or a path into a field of the binding
|
||||||
// template.
|
// template.
|
||||||
func checkBound(ref string, fields map[string]node) error {
|
func checkBound(ref string, fields map[string]node) error {
|
||||||
if isRef(ref) {
|
if grammar.IsRef(ref) {
|
||||||
_, _, err := refShape(ref)
|
_, _, err := grammar.RefShape(ref)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
missing, err := checkArm(ref, fields, false)
|
missing, err := checkArm(ref, fields, false)
|
||||||
@@ -357,7 +211,7 @@ func checkArm(name string, fields map[string]node, wholeToken bool) (missing boo
|
|||||||
// hintableRef reports whether {/name} is a reference the grammar accepts, so the
|
// hintableRef reports whether {/name} is a reference the grammar accepts, so the
|
||||||
// hint never names a spelling that fails too.
|
// hint never names a spelling that fails too.
|
||||||
func hintableRef(name string) bool {
|
func hintableRef(name string) bool {
|
||||||
return checkPathNames(name) == nil
|
return grammar.CheckPathNames(name) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -413,7 +267,7 @@ const (
|
|||||||
// splitArm splits one name into head and tail. refs maps a reference to what
|
// splitArm splits one name into head and tail. refs maps a reference to what
|
||||||
// linkRefs resolved it to; before linking, a reference is whole.
|
// linkRefs resolved it to; before linking, a reference is whole.
|
||||||
func splitArm(name string, refs map[string]refBinding) arm {
|
func splitArm(name string, refs map[string]refBinding) arm {
|
||||||
if isRef(name) {
|
if grammar.IsRef(name) {
|
||||||
b, linked := refs[name]
|
b, linked := refs[name]
|
||||||
if !linked || len(b.tail) == 0 {
|
if !linked || len(b.tail) == 0 {
|
||||||
head := name
|
head := name
|
||||||
@@ -422,11 +276,11 @@ func splitArm(name string, refs map[string]refBinding) arm {
|
|||||||
}
|
}
|
||||||
return arm{spelling: name, head: head, path: head}
|
return arm{spelling: name, head: head, path: head}
|
||||||
}
|
}
|
||||||
sigil, rest, _ := refShape(name) // resolveLink proved it, and took b.tail as a suffix of its segments
|
sigil, rest, _ := grammar.RefShape(name) // resolveLink proved it, and took b.tail as a suffix of its segments
|
||||||
written, _ := splitPath(rest)
|
written, _ := grammar.SplitPath(rest)
|
||||||
return pathArm(name, b.head, sigil+joinSegments(written[:len(written)-len(b.tail)]), b.tail)
|
return pathArm(name, b.head, sigil+grammar.JoinSegments(written[:len(written)-len(b.tail)]), b.tail)
|
||||||
}
|
}
|
||||||
segs, err := splitPath(name)
|
segs, err := grammar.SplitPath(name)
|
||||||
if err != nil || len(segs) == 1 {
|
if err != nil || len(segs) == 1 {
|
||||||
return arm{spelling: name, head: name, path: name}
|
return arm{spelling: name, head: name, path: name}
|
||||||
}
|
}
|
||||||
@@ -467,8 +321,8 @@ type callFn func(s *drawstate.State, emitted string, operands []string) string
|
|||||||
// op is one compiled unit of a format string: a literal run, a name read,
|
// op is one compiled unit of a format string: a literal run, a name read,
|
||||||
// or a builtin already prepared with its args.
|
// or a builtin already prepared with its args.
|
||||||
type op struct {
|
type op struct {
|
||||||
formatToken
|
grammar.Token
|
||||||
arms []arm // nameRead: the '|' alternatives, split into head and tail once
|
arms []arm // grammar.NameRead: the '|' alternatives, split into head and tail once
|
||||||
call callFn
|
call callFn
|
||||||
// operands are the fields the builtin reads, in the order its operands func
|
// operands are the fields the builtin reads, in the order its operands func
|
||||||
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
||||||
@@ -481,41 +335,41 @@ type formatOps struct {
|
|||||||
grow int
|
grow int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *formatOps) function(tok formatToken, 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 tok.Names {
|
||||||
a := splitArm(operand, refs)
|
a := splitArm(operand, refs)
|
||||||
if isName(a.head) {
|
if isName(a.head) {
|
||||||
a.kind = namedRead
|
a.kind = namedRead
|
||||||
}
|
}
|
||||||
operands = append(operands, a)
|
operands = append(operands, a)
|
||||||
}
|
}
|
||||||
c.ops = append(c.ops, op{formatToken: tok, call: builtins[tok.fn].prep(tok.args), operands: operands})
|
c.ops = append(c.ops, op{Token: tok, call: builtins[tok.Fn].prep(tok.Args), operands: operands})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *formatOps) field(tok formatToken, 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.Names))
|
||||||
for i, name := range tok.names {
|
for i, name := range tok.Names {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.ops = append(c.ops, op{formatToken: tok, arms: arms})
|
c.ops = append(c.ops, op{Token: tok, arms: arms})
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileFormat compiles a parsed format; a {ref as n} token compiles to no op. Call
|
// compileFormat compiles a parsed format; a {ref as n} token compiles to no op. Call
|
||||||
// checkTokens first: it is what proves every token valid.
|
// checkTokens first: it is what proves every token valid.
|
||||||
func compileFormat(toks []formatToken, refs map[string]refBinding, isName func(head string) bool) formatOps {
|
func compileFormat(toks []grammar.Token, refs map[string]refBinding, isName func(head string) bool) formatOps {
|
||||||
var c formatOps
|
var c formatOps
|
||||||
for _, tok := range toks {
|
for _, tok := range toks {
|
||||||
switch tok.kind {
|
switch tok.Kind {
|
||||||
case literalRun:
|
case grammar.LiteralRun:
|
||||||
c.grow += len(tok.lit)
|
c.grow += len(tok.Lit)
|
||||||
c.ops = append(c.ops, op{formatToken: tok})
|
c.ops = append(c.ops, op{Token: tok})
|
||||||
case builtinCall:
|
case grammar.BuiltinCall:
|
||||||
c.function(tok, refs, isName)
|
c.function(tok, refs, isName)
|
||||||
case nameRead:
|
case grammar.NameRead:
|
||||||
c.field(tok, refs, isName)
|
c.field(tok, refs, isName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template is an inline template compiled, referenced and validated against a
|
// Template is an inline template compiled, referenced and validated against a
|
||||||
@@ -57,76 +59,13 @@ func (f *Generator) FakeTemplate(input string) (string, error) {
|
|||||||
// do a template of one reference alone, which is a path written as a template, and a path
|
// do a template of one reference alone, which is a path written as a template, and a path
|
||||||
// written with a leading /.
|
// written with a leading /.
|
||||||
func IsTemplate(arg string) (bool, error) {
|
func IsTemplate(arg string) (bool, error) {
|
||||||
inline, err := isTemplate(arg)
|
inline, err := grammar.IsTemplate(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("fejkdata: %w", err)
|
return false, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
return inline, nil
|
return inline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTemplate(arg string) (bool, error) {
|
|
||||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
|
||||||
if path, lone := loneReference(arg); lone {
|
|
||||||
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if i := strings.IndexAny(arg, `}"`); i >= 0 {
|
|
||||||
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
|
|
||||||
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
|
|
||||||
}
|
|
||||||
if _, err := splitPath(arg); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
|
||||||
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSONStart(arg string) bool {
|
|
||||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
|
||||||
func pathAdvice(path, refusal string) error {
|
|
||||||
if err := checkPathNames(path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s; write %s", refusal, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loneReference is the path a template spells when the value it holds — a format string, a
|
|
||||||
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
|
||||||
func loneReference(arg string) (string, bool) {
|
|
||||||
var raw any
|
|
||||||
if json.Unmarshal([]byte(arg), &raw) != nil {
|
|
||||||
raw = arg
|
|
||||||
}
|
|
||||||
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
|
|
||||||
raw = m["format"]
|
|
||||||
}
|
|
||||||
format, isString := raw.(string)
|
|
||||||
if !isString {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
toks, err := parseFormat(format)
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
body, lone := loneRef(toks)
|
|
||||||
if !lone {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(body, "/") {
|
|
||||||
body = "/" + strings.TrimLeft(body, "/")
|
|
||||||
}
|
|
||||||
_, path, err := refShape(body)
|
|
||||||
return path, err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileInput(input string) (node, error) {
|
func compileInput(input string) (node, error) {
|
||||||
v, err := inputValue(input)
|
v, err := inputValue(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,7 +110,7 @@ func refuseFolderRefs(scope nodeScope) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, name := range refTokens(t.tokens) {
|
for _, name := range refTokens(t.tokens) {
|
||||||
sigil, rest, err := refShape(name)
|
sigil, rest, err := grammar.RefShape(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
}
|
}
|
||||||
@@ -206,11 +145,3 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loneRef is the reference a format of one reference token and nothing else reads.
|
|
||||||
func loneRef(toks []formatToken) (string, bool) {
|
|
||||||
if len(toks) != 1 || toks[0].kind != nameRead || len(toks[0].names) != 1 || !isRef(toks[0].names[0]) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return toks[0].names[0], true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CalcNode is a parsed calc expression node.
|
||||||
|
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.
|
||||||
|
type CalcVar struct {
|
||||||
|
Name string
|
||||||
|
At int
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalcNeg struct{ X CalcNode }
|
||||||
|
|
||||||
|
type CalcBin struct { // L + - * / R
|
||||||
|
Operator byte
|
||||||
|
L, R CalcNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CalcNum) calcNode() {}
|
||||||
|
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) {
|
||||||
|
case CalcNum:
|
||||||
|
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
||||||
|
case CalcVar:
|
||||||
|
return n.Name
|
||||||
|
case CalcNeg:
|
||||||
|
return "-" + CalcText(n.X)
|
||||||
|
case CalcBin:
|
||||||
|
return "(" + CalcText(n.L) + " " + string(n.Operator) + " " + CalcText(n.R) + ")"
|
||||||
|
}
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}}
|
||||||
|
if p.space(); p.pos >= len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("empty expression")
|
||||||
|
}
|
||||||
|
n, err := p.expr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p.space(); p.pos != len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) space() {
|
||||||
|
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
||||||
|
p.pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) expr() (CalcNode, error) { return p.binary(p.term, '+', '-') }
|
||||||
|
func (p *calcParser) term() (CalcNode, error) { return p.binary(p.factor, '*', '/') }
|
||||||
|
|
||||||
|
// binary parses a left-associative run of next() operands joined by the given
|
||||||
|
// operators, the one shape expr and term share.
|
||||||
|
func (p *calcParser) binary(next func() (CalcNode, error), ops ...byte) (CalcNode, error) {
|
||||||
|
n, err := next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
p.space()
|
||||||
|
if p.pos >= len(p.rs) || !slices.Contains(ops, byte(p.rs[p.pos])) {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
op := byte(p.rs[p.pos])
|
||||||
|
p.pos++
|
||||||
|
r, err := next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n = CalcBin{op, n, r}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) factor() (CalcNode, error) {
|
||||||
|
p.space()
|
||||||
|
if p.pos >= len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("unexpected end of expression")
|
||||||
|
}
|
||||||
|
switch c := p.rs[p.pos]; {
|
||||||
|
case c == '-':
|
||||||
|
p.pos++
|
||||||
|
x, err := p.factor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return CalcNeg{x}, nil
|
||||||
|
case c == '(':
|
||||||
|
p.pos++
|
||||||
|
n, err := p.expr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
||||||
|
return nil, fmt.Errorf("missing ')'")
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
return n, nil
|
||||||
|
case c == '.' || c >= '0' && c <= '9':
|
||||||
|
return p.number()
|
||||||
|
case c == '_' || unicode.IsLetter(c):
|
||||||
|
return p.ident()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected %q", string(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) number() (CalcNode, error) {
|
||||||
|
start, dot := p.pos, false
|
||||||
|
for p.pos < len(p.rs) {
|
||||||
|
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
||||||
|
p.pos++
|
||||||
|
} else if c == '.' && !dot {
|
||||||
|
dot, p.pos = true, p.pos+1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
||||||
|
}
|
||||||
|
return CalcNum(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ident reads a field or name: a letter or '_', then letters, digits or '_'. A '-'
|
||||||
|
// is always the minus operator, so a hyphenated one can't be an operand.
|
||||||
|
func (p *calcParser) ident() (CalcNode, error) {
|
||||||
|
start := p.pos
|
||||||
|
for p.pos < len(p.rs) {
|
||||||
|
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
||||||
|
p.pos++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := string(p.rs[start:p.pos])
|
||||||
|
at, seen := p.vars[name]
|
||||||
|
if !seen {
|
||||||
|
at = len(p.vars)
|
||||||
|
p.vars[name] = at
|
||||||
|
}
|
||||||
|
return CalcVar{name, at}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// 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.
|
||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scanUnit is one unit of a scanned format string: a literal rune or the body of a
|
||||||
|
// {…} token.
|
||||||
|
type scanUnit struct {
|
||||||
|
isToken bool // a {…} body, else a literal rune
|
||||||
|
char rune
|
||||||
|
body string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eachScanUnit scans a format string once and calls fn for each unit, the single
|
||||||
|
// source of truth for how braces are read: "{{" and "}}" are literal braces, a "{"
|
||||||
|
// opens a token that must reach its "}", and a lone "}" is an error.
|
||||||
|
func eachScanUnit(format string, fn func(scanUnit) error) error {
|
||||||
|
rs := []rune(format)
|
||||||
|
for i := 0; i < len(rs); i++ {
|
||||||
|
var u scanUnit
|
||||||
|
switch c := rs[i]; c {
|
||||||
|
case '{':
|
||||||
|
if i+1 < len(rs) && rs[i+1] == '{' {
|
||||||
|
u.char = '{'
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end := i + 1
|
||||||
|
for end < len(rs) && rs[end] != '}' {
|
||||||
|
if rs[end] == '{' {
|
||||||
|
return fmt.Errorf("'{' inside a token in %q; a literal brace is written {{", format)
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end >= len(rs) {
|
||||||
|
return fmt.Errorf("unterminated '{' in %q", format)
|
||||||
|
}
|
||||||
|
u.isToken, u.body = true, string(rs[i+1:end])
|
||||||
|
i = end
|
||||||
|
case '}':
|
||||||
|
if i+1 < len(rs) && rs[i+1] == '}' {
|
||||||
|
u.char = '}'
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return fmt.Errorf("lone '}' in %q; a literal brace is written }}", format)
|
||||||
|
default:
|
||||||
|
u.char = c
|
||||||
|
}
|
||||||
|
if err := fn(u); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// NameBind: the reference it binds, and the name.
|
||||||
|
BoundRef, Bound string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
BuiltinCall TokenKind = iota + 1
|
||||||
|
NameRead
|
||||||
|
LiteralRun
|
||||||
|
NameBind
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
var toks []Token
|
||||||
|
var lit strings.Builder
|
||||||
|
flush := func() {
|
||||||
|
if lit.Len() > 0 {
|
||||||
|
toks = append(toks, Token{Kind: LiteralRun, Lit: lit.String()})
|
||||||
|
lit.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := eachScanUnit(format, func(u scanUnit) error {
|
||||||
|
if !u.isToken {
|
||||||
|
lit.WriteRune(u.char)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
if ref, name, binds := CutOutside(u.body, AsWord); binds && IndexOutside(u.body, '(') < 0 {
|
||||||
|
toks = append(toks, Token{Kind: NameBind, Body: u.body, BoundRef: ref, Bound: name})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if IndexOutside(u.body, '(') < 0 {
|
||||||
|
toks = append(toks, Token{Kind: NameRead, Body: u.body, Names: 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)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
return toks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||||
|
// for a name-read body. A '(' without a trailing ')' yields ok=false.
|
||||||
|
func FuncCall(body string) (name string, args []string, ok bool) {
|
||||||
|
lp := IndexOutside(body, '(')
|
||||||
|
if lp < 0 || !strings.HasSuffix(body, ")") {
|
||||||
|
return "", nil, false
|
||||||
|
}
|
||||||
|
return body[:lp], SplitArgs(body[lp+1 : len(body)-1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitArgs parses a function arg list: comma-separated outside a selector or a
|
||||||
|
// quoted layout, trimmed; empty -> none.
|
||||||
|
func SplitArgs(s string) []string {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
depth, quoted, start := 0, false, 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch c := s[i]; {
|
||||||
|
case c == '\'' && depth == 0:
|
||||||
|
quoted = !quoted
|
||||||
|
case quoted:
|
||||||
|
case c == '[':
|
||||||
|
depth++
|
||||||
|
case c == ']' && depth > 0:
|
||||||
|
depth--
|
||||||
|
case c == ',' && depth == 0:
|
||||||
|
args, start = append(args, strings.TrimSpace(s[start:i])), i+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(args, strings.TrimSpace(s[start:]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reservedInName is what a category, folder or field name may not contain: a dot
|
||||||
|
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||||
|
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
||||||
|
// open a JSON value. A name carrying one is rejected where it is authored rather
|
||||||
|
// than where it would be unreachable.
|
||||||
|
const reservedInName = ".|({}/[]\""
|
||||||
|
|
||||||
|
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||||
|
|
||||||
|
// CheckName rejects a name the dot path, {token} and JSON grammars cannot spell, or a struct
|
||||||
|
// tag cannot read.
|
||||||
|
// Both a category or folder and a field go through it, so there is one answer to
|
||||||
|
// what a name may contain.
|
||||||
|
func CheckName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||||
|
}
|
||||||
|
if name == "-" {
|
||||||
|
return fmt.Errorf(`%q is reserved: the struct tag fake:"-" leaves a field unfilled, so no tag could read it; rename it`, name)
|
||||||
|
}
|
||||||
|
if strings.Contains(name, AsWord) {
|
||||||
|
return fmt.Errorf("%q contains %q, which a token reads as binding a name; rename it", name, AsWord)
|
||||||
|
}
|
||||||
|
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||||
|
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
||||||
|
name, name[i:i+1], reservedList)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPathNames rejects a dotted path with a segment no name may be.
|
||||||
|
func CheckPathNames(path string) error {
|
||||||
|
segs, err := SplitPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, seg := range NameSegments(segs) {
|
||||||
|
if err := CheckName(seg); err != nil {
|
||||||
|
return fmt.Errorf("path %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SplitPath splits a dotted path into segments, a selector — [key or name] after a
|
||||||
|
// table's name — becoming a segment of its own, brackets kept, and so does each "..". A
|
||||||
|
// dot inside a selector is part of the key or name.
|
||||||
|
func SplitPath(path string) ([]string, error) {
|
||||||
|
segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1)
|
||||||
|
start, open := 0, -1
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
switch path[i] {
|
||||||
|
case '[':
|
||||||
|
if err := checkOpen(path, i, start, open); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
segs = append(segs, path[start:i])
|
||||||
|
start, open = i, i
|
||||||
|
case ']':
|
||||||
|
if err := checkClose(path, i, open); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
segs = append(segs, path[start:i+1])
|
||||||
|
open = -1
|
||||||
|
segs, i = stepUpAt(segs, path, i+1)
|
||||||
|
start = i + 1
|
||||||
|
case '.':
|
||||||
|
if open < 0 {
|
||||||
|
segs = append(segs, path[start:i])
|
||||||
|
segs, i = stepUpAt(segs, path, i)
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if open >= 0 {
|
||||||
|
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path)
|
||||||
|
}
|
||||||
|
if start < len(path) || start == len(path) && (len(segs) == 0 || segs[len(segs)-1] != "..") {
|
||||||
|
segs = append(segs, path[start:])
|
||||||
|
}
|
||||||
|
return segs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stepUpAt appends a ".." segment where the dot at i is the first of two, returning the
|
||||||
|
// index of the last dot it consumed.
|
||||||
|
func stepUpAt(segs []string, path string, i int) ([]string, int) {
|
||||||
|
if i+1 < len(path) && path[i+1] == '.' {
|
||||||
|
return append(segs, ".."), i + 1
|
||||||
|
}
|
||||||
|
return segs, i
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one
|
||||||
|
// following no name.
|
||||||
|
func checkOpen(path string, i, start, open int) error {
|
||||||
|
switch {
|
||||||
|
case open >= 0:
|
||||||
|
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
|
||||||
|
case i == 0:
|
||||||
|
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
|
||||||
|
case i == start:
|
||||||
|
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
|
||||||
|
// followed by anything but a dot or the end.
|
||||||
|
func checkClose(path string, i, open int) error {
|
||||||
|
switch {
|
||||||
|
case open < 0:
|
||||||
|
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
|
||||||
|
case i == open+1:
|
||||||
|
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
|
||||||
|
case i+1 < len(path) && path[i+1] != '.':
|
||||||
|
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOutside is the first c in s outside a [selector], or -1.
|
||||||
|
func IndexOutside(s string, c byte) int {
|
||||||
|
depth := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch {
|
||||||
|
case s[i] == '[':
|
||||||
|
depth++
|
||||||
|
case s[i] == ']' && depth > 0:
|
||||||
|
depth--
|
||||||
|
case s[i] == c && depth == 0:
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CutOutside cuts s around the first sep outside a [selector].
|
||||||
|
func CutOutside(s, sep string) (before, after string, found bool) {
|
||||||
|
depth := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch {
|
||||||
|
case s[i] == '[':
|
||||||
|
depth++
|
||||||
|
case s[i] == ']' && depth > 0:
|
||||||
|
depth--
|
||||||
|
case depth == 0 && strings.HasPrefix(s[i:], sep):
|
||||||
|
return s[:i], s[i+len(sep):], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitOutside(s string, c byte) []string {
|
||||||
|
var parts []string
|
||||||
|
for {
|
||||||
|
i := IndexOutside(s, c)
|
||||||
|
if i < 0 {
|
||||||
|
return append(parts, s)
|
||||||
|
}
|
||||||
|
parts, s = append(parts, s[:i]), s[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
|
||||||
|
|
||||||
|
func HasSelector(segs []string) bool {
|
||||||
|
for _, s := range segs {
|
||||||
|
if IsSelector(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectorOf(seg string) string { return seg[1 : len(seg)-1] }
|
||||||
|
|
||||||
|
// NameSegments is the segments of a path that are names, its selectors and ".." left out.
|
||||||
|
func NameSegments(segs []string) []string {
|
||||||
|
out := segs[:0:0]
|
||||||
|
for _, s := range segs {
|
||||||
|
if !IsSelector(s) && s != ".." {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinSegments spells segments as a path: a selector attaches to the name before it, and
|
||||||
|
// so do a ".." and the name after it.
|
||||||
|
func JoinSegments(segs []string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for i, s := range segs {
|
||||||
|
if b.Len() > 0 && !IsSelector(s) && s != ".." && segs[i-1] != ".." {
|
||||||
|
b.WriteByte('.')
|
||||||
|
}
|
||||||
|
b.WriteString(s)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A reference names a node by path rather than as a sibling field: {/a.b} from the
|
||||||
|
// data root, {.a} from the folder this file sits in, {..a} from the folder above.
|
||||||
|
func IsRef(name string) bool { return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "/") }
|
||||||
|
|
||||||
|
// RefShape splits a reference into its sigil and the dotted path after it.
|
||||||
|
func RefShape(name string) (sigil, rest string, err error) {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(name, "/"):
|
||||||
|
sigil, rest = "/", name[1:]
|
||||||
|
case strings.HasPrefix(name, ".."):
|
||||||
|
sigil, rest = "..", name[2:]
|
||||||
|
default:
|
||||||
|
sigil, rest = ".", name[1:]
|
||||||
|
}
|
||||||
|
if rest == "" {
|
||||||
|
return "", "", fmt.Errorf("reference has no path")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rest, "/") {
|
||||||
|
return "", "", fmt.Errorf("the path after %s starts at a name, not a /; write {%s%s}", sigil, sigil, rest[1:])
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rest, ".") {
|
||||||
|
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
||||||
|
}
|
||||||
|
segs, err := SplitPath(rest)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
for _, seg := range segs {
|
||||||
|
if seg == "" {
|
||||||
|
return "", "", fmt.Errorf("path has an empty segment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sigil, rest, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"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 {
|
||||||
|
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if i := strings.IndexAny(arg, `}"`); i >= 0 {
|
||||||
|
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
|
||||||
|
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
|
||||||
|
}
|
||||||
|
if _, err := SplitPath(arg); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
||||||
|
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONStart(arg string) bool {
|
||||||
|
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||||
|
func pathAdvice(path, refusal string) error {
|
||||||
|
if err := CheckPathNames(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s; write %s", refusal, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loneReference is the path a template spells when the value it holds — a format string, a
|
||||||
|
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
||||||
|
func loneReference(arg string) (string, bool) {
|
||||||
|
var raw any
|
||||||
|
if json.Unmarshal([]byte(arg), &raw) != nil {
|
||||||
|
raw = arg
|
||||||
|
}
|
||||||
|
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
|
||||||
|
raw = m["format"]
|
||||||
|
}
|
||||||
|
format, isString := raw.(string)
|
||||||
|
if !isString {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
toks, err := ParseFormat(format, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
body, lone := loneRef(toks)
|
||||||
|
if !lone {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(body, "/") {
|
||||||
|
body = "/" + strings.TrimLeft(body, "/")
|
||||||
|
}
|
||||||
|
_, path, err := RefShape(body)
|
||||||
|
return path, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return toks[0].Names[0], true
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// nameScope is where a name is bound: a category, or one iteration of a repeat inside it. A
|
// nameScope is where a name is bound: a category, or one iteration of a repeat inside it. A
|
||||||
@@ -43,7 +45,7 @@ type nameUse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// bindsField reports whether b binds a path into a field, so a read of it stays in the category.
|
// bindsField reports whether b binds a path into a field, so a read of it stays in the category.
|
||||||
func (b *nameBinding) bindsField() bool { return !isRef(b.ref) }
|
func (b *nameBinding) bindsField() bool { return !grammar.IsRef(b.ref) }
|
||||||
|
|
||||||
func (sc *nameScope) lookup(name string) *nameBinding {
|
func (sc *nameScope) lookup(name string) *nameBinding {
|
||||||
for ; sc != nil; sc = sc.up {
|
for ; sc != nil; sc = sc.up {
|
||||||
@@ -104,16 +106,16 @@ func bindNames(root node) error {
|
|||||||
// bindAll binds every name t's tokens bind.
|
// bindAll binds every name t's tokens bind.
|
||||||
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
|
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
|
||||||
for _, tok := range t.tokens {
|
for _, tok := range t.tokens {
|
||||||
if tok.kind != nameBind {
|
if tok.Kind != grammar.NameBind {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if inChoice {
|
if inChoice {
|
||||||
return fmt.Errorf("%stoken {%s}: a choice's item binds no name, since every other item would leave it unbound; bind it outside the choice, or move the item into a category of its own and reference that", where, tok.body)
|
return fmt.Errorf("%stoken {%s}: a choice's item binds no name, since every other item would leave it unbound; bind it outside the choice, or move the item into a category of its own and reference that", where, tok.Body)
|
||||||
}
|
}
|
||||||
if b, twice := sc.bindings[tok.bound]; twice {
|
if b, twice := sc.bindings[tok.Bound]; twice {
|
||||||
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body)
|
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.Body, tok.Bound, sc.spelled(), b.body)
|
||||||
}
|
}
|
||||||
b := &nameBinding{name: tok.bound, ref: tok.boundRef, body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t}
|
b := &nameBinding{name: tok.Bound, ref: tok.BoundRef, body: tok.Body, where: where, scope: sc, index: len(sc.order), binder: t}
|
||||||
if sc.bindings == nil {
|
if sc.bindings == nil {
|
||||||
sc.bindings = map[string]*nameBinding{}
|
sc.bindings = map[string]*nameBinding{}
|
||||||
}
|
}
|
||||||
@@ -219,16 +221,16 @@ func unresolved(where string, u unboundRead, seen *nameScope, scopes []*nameScop
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *template) isName(head string) bool {
|
func (t *template) isName(head string) bool {
|
||||||
return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
|
return !grammar.IsRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkBindings resolves what each of t's bindings binds to a head node and a tail path.
|
// linkBindings resolves what each of t's bindings binds to a head node and a tail path.
|
||||||
func linkBindings(t *template) {
|
func linkBindings(t *template) {
|
||||||
for _, tok := range t.tokens {
|
for _, tok := range t.tokens {
|
||||||
if tok.kind != nameBind {
|
if tok.Kind != grammar.NameBind {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b := t.nameScope.bindings[tok.bound]
|
b := t.nameScope.bindings[tok.Bound]
|
||||||
a := splitArm(b.ref, t.link.refs)
|
a := splitArm(b.ref, t.link.refs)
|
||||||
b.head, b.tail = t.head(a.head), a.tail
|
b.head, b.tail = t.head(a.head), a.tail
|
||||||
}
|
}
|
||||||
@@ -260,7 +262,7 @@ var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRea
|
|||||||
func linkNames(path string, t *template) error {
|
func linkNames(path string, t *template) error {
|
||||||
return namedReads(t, func(o *op, a *arm) error {
|
return namedReads(t, func(o *op, a *arm) error {
|
||||||
if err := linkName(t, a); err != nil {
|
if err := linkName(t, a); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
|
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -268,8 +270,8 @@ func linkNames(path string, t *template) error {
|
|||||||
|
|
||||||
func linkName(t *template, a *arm) error {
|
func linkName(t *template, a *arm) error {
|
||||||
b := t.nameScope.lookup(a.head)
|
b := t.nameScope.lookup(a.head)
|
||||||
if hasSelector(a.tail) {
|
if grammar.HasSelector(a.tail) {
|
||||||
return fmt.Errorf("a path through name %q may not select a row; read it directly, {%s.%s}, or bind the row to a name of its own", a.head, b.ref, joinSegments(a.tail))
|
return fmt.Errorf("a path through name %q may not select a row; read it directly, {%s.%s}, or bind the row to a name of its own", a.head, b.ref, grammar.JoinSegments(a.tail))
|
||||||
}
|
}
|
||||||
full := append(b.tail[:len(b.tail):len(b.tail)], a.tail...)
|
full := append(b.tail[:len(b.tail):len(b.tail)], a.tail...)
|
||||||
if err := checkPathResolves(b.head, full, a.head); err != nil {
|
if err := checkPathResolves(b.head, full, a.head); err != nil {
|
||||||
@@ -279,9 +281,9 @@ func linkName(t *template, a *arm) error {
|
|||||||
a.named, a.steps, a.leaves = b, w.steps, w.leaves
|
a.named, a.steps, a.leaves = b, w.steps, w.leaves
|
||||||
a.levels = make([]string, len(full)+1)
|
a.levels = make([]string, len(full)+1)
|
||||||
for i := range a.levels {
|
for i := range a.levels {
|
||||||
a.levels[i] = joinSegments(full[:i])
|
a.levels[i] = grammar.JoinSegments(full[:i])
|
||||||
}
|
}
|
||||||
a.path = joinSegments(full)
|
a.path = grammar.JoinSegments(full)
|
||||||
if b.addressed == nil {
|
if b.addressed == nil {
|
||||||
b.addressed = map[string]string{}
|
b.addressed = map[string]string{}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-10
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
|
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
|
||||||
@@ -11,20 +13,20 @@ import (
|
|||||||
// draw of w.
|
// draw of w.
|
||||||
func checkNameReads(path string, t *template) error {
|
func checkNameReads(path string, t *template) error {
|
||||||
for _, tok := range t.tokens {
|
for _, tok := range t.tokens {
|
||||||
if tok.kind != nameBind {
|
if tok.Kind != grammar.NameBind {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := t.nameScope.bindings[tok.bound].checkUses(); err != nil {
|
if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), tok.body, err)
|
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), tok.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return namedReads(t, func(o *op, a *arm) error {
|
return namedReads(t, func(o *op, a *arm) error {
|
||||||
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
|
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
|
||||||
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
|
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", t.site.label(path), o.Body, a.named.name, a.named.ref)
|
||||||
}
|
}
|
||||||
for _, leaf := range a.leaves {
|
for _, leaf := range a.leaves {
|
||||||
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
|
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
|
||||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
|
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -73,7 +75,7 @@ func (b *nameBinding) checkUses() error {
|
|||||||
if !reaches {
|
if !reaches {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
spelling = joinSegments(append(down, b.ref))
|
spelling = grammar.JoinSegments(append(down, b.ref))
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(r.tail, ".."):
|
case strings.HasPrefix(r.tail, ".."):
|
||||||
@@ -107,9 +109,9 @@ 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 := parseCalc(spelling)
|
n, err := grammar.ParseCalc(spelling)
|
||||||
v, isVar := n.(calcVar)
|
v, isVar := n.(grammar.CalcVar)
|
||||||
return err == nil && isVar && string(v) == spelling
|
return err == nil && isVar && v.Name == spelling
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses.
|
// checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses.
|
||||||
@@ -132,7 +134,7 @@ func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) er
|
|||||||
var into []arm
|
var into []arm
|
||||||
for _, o := range t.compiled.ops {
|
for _, o := range t.compiled.ops {
|
||||||
for _, a := range slices.Concat(o.arms, o.operands) {
|
for _, a := range slices.Concat(o.arms, o.operands) {
|
||||||
if isRef(a.head) || a.kind == namedRead {
|
if grammar.IsRef(a.head) || a.kind == namedRead {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, kept := b.addressed[join(key, a.path)]; kept {
|
if _, kept := b.addressed[join(key, a.path)]; kept {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// node is a compiled element of the namespace tree: a folder, choice, null,
|
// node is a compiled element of the namespace tree: a folder, choice, null,
|
||||||
@@ -48,7 +50,7 @@ func (*nullItem) isNode() {}
|
|||||||
type template struct {
|
type template struct {
|
||||||
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
|
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
|
||||||
format string
|
format string
|
||||||
tokens []formatToken
|
tokens []grammar.Token
|
||||||
fields map[string]node
|
fields map[string]node
|
||||||
repeat int
|
repeat int
|
||||||
separator string
|
separator string
|
||||||
@@ -99,7 +101,7 @@ func (*template) isNode() {}
|
|||||||
|
|
||||||
// head is the node an arm's head names: a sibling field, or a reference's category.
|
// head is the node an arm's head names: a sibling field, or a reference's category.
|
||||||
func (t *template) head(name string) node {
|
func (t *template) head(name string) node {
|
||||||
if isRef(name) {
|
if grammar.IsRef(name) {
|
||||||
return t.link.refHeads[name]
|
return t.link.refHeads[name]
|
||||||
}
|
}
|
||||||
return t.fields[name]
|
return t.fields[name]
|
||||||
@@ -247,8 +249,8 @@ func (t *template) fixedText() (string, bool) {
|
|||||||
switch {
|
switch {
|
||||||
case len(t.tokens) == 0:
|
case len(t.tokens) == 0:
|
||||||
return "", true
|
return "", true
|
||||||
case len(t.tokens) == 1 && t.tokens[0].kind == literalRun:
|
case len(t.tokens) == 1 && t.tokens[0].Kind == grammar.LiteralRun:
|
||||||
return t.tokens[0].lit, true
|
return t.tokens[0].Lit, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -397,7 +399,7 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
|||||||
if isOption(k) {
|
if isOption(k) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := checkName(k); err != nil {
|
if err := grammar.CheckName(k); err != nil {
|
||||||
return nil, fmt.Errorf("field %w", err)
|
return nil, fmt.Errorf("field %w", err)
|
||||||
}
|
}
|
||||||
n, err := compileAt(m[k], pos)
|
n, err := compileAt(m[k], pos)
|
||||||
@@ -459,50 +461,6 @@ func weightOf(raw any) (float64, error) {
|
|||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
|
||||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
|
||||||
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
|
||||||
// open a JSON value. A name carrying one is rejected where it is authored rather
|
|
||||||
// than where it would be unreachable.
|
|
||||||
const reservedInName = ".|({}/[]\""
|
|
||||||
|
|
||||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
|
||||||
|
|
||||||
// checkName rejects a name the dot path, {token} and JSON grammars cannot spell, or a struct
|
|
||||||
// tag cannot read.
|
|
||||||
// Both a category or folder and a field go through it, so there is one answer to
|
|
||||||
// what a name may contain.
|
|
||||||
func checkName(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
|
||||||
}
|
|
||||||
if name == "-" {
|
|
||||||
return fmt.Errorf(`%q is reserved: the struct tag fake:"-" leaves a field unfilled, so no tag could read it; rename it`, name)
|
|
||||||
}
|
|
||||||
if strings.Contains(name, asWord) {
|
|
||||||
return fmt.Errorf("%q contains %q, which a token reads as binding a name; rename it", name, asWord)
|
|
||||||
}
|
|
||||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
|
||||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
|
||||||
name, name[i:i+1], reservedList)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPathNames rejects a dotted path with a segment no name may be.
|
|
||||||
func checkPathNames(path string) error {
|
|
||||||
segs, err := splitPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, seg := range nameSegments(segs) {
|
|
||||||
if err := checkName(seg); err != nil {
|
|
||||||
return fmt.Errorf("path %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOption(name string) bool {
|
func isOption(name string) bool {
|
||||||
switch name {
|
switch name {
|
||||||
case "datatype", "format", "repeat", "separator", "weight":
|
case "datatype", "format", "repeat", "separator", "weight":
|
||||||
|
|||||||
@@ -5,156 +5,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// splitPath splits a dotted path into segments, a selector — [key or name] after a
|
|
||||||
// table's name — becoming a segment of its own, brackets kept, and so does each "..". A
|
|
||||||
// dot inside a selector is part of the key or name.
|
|
||||||
func splitPath(path string) ([]string, error) {
|
|
||||||
segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1)
|
|
||||||
start, open := 0, -1
|
|
||||||
for i := 0; i < len(path); i++ {
|
|
||||||
switch path[i] {
|
|
||||||
case '[':
|
|
||||||
if err := checkOpen(path, i, start, open); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
segs = append(segs, path[start:i])
|
|
||||||
start, open = i, i
|
|
||||||
case ']':
|
|
||||||
if err := checkClose(path, i, open); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
segs = append(segs, path[start:i+1])
|
|
||||||
open = -1
|
|
||||||
segs, i = stepUpAt(segs, path, i+1)
|
|
||||||
start = i + 1
|
|
||||||
case '.':
|
|
||||||
if open < 0 {
|
|
||||||
segs = append(segs, path[start:i])
|
|
||||||
segs, i = stepUpAt(segs, path, i)
|
|
||||||
start = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if open >= 0 {
|
|
||||||
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path)
|
|
||||||
}
|
|
||||||
if start < len(path) || start == len(path) && (len(segs) == 0 || segs[len(segs)-1] != "..") {
|
|
||||||
segs = append(segs, path[start:])
|
|
||||||
}
|
|
||||||
return segs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// stepUpAt appends a ".." segment where the dot at i is the first of two, returning the
|
|
||||||
// index of the last dot it consumed.
|
|
||||||
func stepUpAt(segs []string, path string, i int) ([]string, int) {
|
|
||||||
if i+1 < len(path) && path[i+1] == '.' {
|
|
||||||
return append(segs, ".."), i + 1
|
|
||||||
}
|
|
||||||
return segs, i
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one
|
|
||||||
// following no name.
|
|
||||||
func checkOpen(path string, i, start, open int) error {
|
|
||||||
switch {
|
|
||||||
case open >= 0:
|
|
||||||
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
|
|
||||||
case i == 0:
|
|
||||||
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
|
|
||||||
case i == start:
|
|
||||||
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
|
|
||||||
// followed by anything but a dot or the end.
|
|
||||||
func checkClose(path string, i, open int) error {
|
|
||||||
switch {
|
|
||||||
case open < 0:
|
|
||||||
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
|
|
||||||
case i == open+1:
|
|
||||||
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
|
|
||||||
case i+1 < len(path) && path[i+1] != '.':
|
|
||||||
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexOutside is the first c in s outside a [selector], or -1.
|
|
||||||
func indexOutside(s string, c byte) int {
|
|
||||||
depth := 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
switch {
|
|
||||||
case s[i] == '[':
|
|
||||||
depth++
|
|
||||||
case s[i] == ']' && depth > 0:
|
|
||||||
depth--
|
|
||||||
case s[i] == c && depth == 0:
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// cutOutside cuts s around the first sep outside a [selector].
|
|
||||||
func cutOutside(s, sep string) (before, after string, found bool) {
|
|
||||||
depth := 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
switch {
|
|
||||||
case s[i] == '[':
|
|
||||||
depth++
|
|
||||||
case s[i] == ']' && depth > 0:
|
|
||||||
depth--
|
|
||||||
case depth == 0 && strings.HasPrefix(s[i:], sep):
|
|
||||||
return s[:i], s[i+len(sep):], true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s, "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitOutside(s string, c byte) []string {
|
|
||||||
var parts []string
|
|
||||||
for {
|
|
||||||
i := indexOutside(s, c)
|
|
||||||
if i < 0 {
|
|
||||||
return append(parts, s)
|
|
||||||
}
|
|
||||||
parts, s = append(parts, s[:i]), s[i+1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
|
|
||||||
|
|
||||||
func hasSelector(segs []string) bool {
|
|
||||||
for _, s := range segs {
|
|
||||||
if isSelector(s) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
|
|
||||||
|
|
||||||
// nameSegments is the segments of a path that are names, its selectors and ".." left out.
|
|
||||||
func nameSegments(segs []string) []string {
|
|
||||||
out := segs[:0:0]
|
|
||||||
for _, s := range segs {
|
|
||||||
if !isSelector(s) && s != ".." {
|
|
||||||
out = append(out, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// stepInto is what seg names under n: a folder's entry or a template's field.
|
// stepInto is what seg names under n: a folder's entry or a template's field.
|
||||||
func stepInto(n node, seg string) (node, error) {
|
func stepInto(n node, seg string) (node, error) {
|
||||||
switch {
|
switch {
|
||||||
case isSelector(seg):
|
case grammar.IsSelector(seg):
|
||||||
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg))
|
return nil, fmt.Errorf("%s is not a table, so it has no row to select", grammar.SelectorOf(seg))
|
||||||
case seg == "..":
|
case seg == "..":
|
||||||
if c, ok := n.(*tableColumn); ok {
|
if c, ok := n.(*tableColumn); ok {
|
||||||
return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i])
|
return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i])
|
||||||
@@ -215,7 +73,7 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
|
|||||||
}
|
}
|
||||||
// A selector further down pins this table by ancestry, so the walk draws only
|
// A selector further down pins this table by ancestry, so the walk draws only
|
||||||
// where none follows; drawing first could pick a row the selector is not inside.
|
// where none follows; drawing first could pick a row the selector is not inside.
|
||||||
r := tableRoute{sel: sel, draw: sel == "" && (descended || len(tail) > 0) && !hasSelector(tail)}
|
r := tableRoute{sel: sel, draw: sel == "" && (descended || len(tail) > 0) && !grammar.HasSelector(tail)}
|
||||||
switch {
|
switch {
|
||||||
case len(tail) == 0 && sel == "" && !descended:
|
case len(tail) == 0 && sel == "" && !descended:
|
||||||
r.next = t
|
r.next = t
|
||||||
@@ -239,8 +97,8 @@ func (t *table) stepUp(rest []string) error {
|
|||||||
return fmt.Errorf("path has an empty segment")
|
return fmt.Errorf("path has an empty segment")
|
||||||
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 hasSelector(rest):
|
case grammar.HasSelector(rest):
|
||||||
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, joinSegments(append([]string{t.parentT.path}, rest[1:]...)))
|
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, grammar.JoinSegments(append([]string{t.parentT.path}, rest[1:]...)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -314,7 +172,7 @@ func (w *pathCheck) walk(n node, tail []string) (node, error) {
|
|||||||
n, tail, descended = r.next, r.rest, r.descends
|
n, tail, descended = r.next, r.rest, r.descends
|
||||||
continue
|
continue
|
||||||
case *template:
|
case *template:
|
||||||
if isSelector(tail[0]) {
|
if grammar.IsSelector(tail[0]) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := w.enter(x, tail); err != nil {
|
if err := w.enter(x, tail); err != nil {
|
||||||
@@ -490,11 +348,11 @@ func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *table) selector(tail []string) (sel string, rest []string, err error) {
|
func (t *table) selector(tail []string) (sel string, rest []string, err error) {
|
||||||
if len(tail) == 0 || !isSelector(tail[0]) {
|
if len(tail) == 0 || !grammar.IsSelector(tail[0]) {
|
||||||
return "", tail, nil
|
return "", tail, nil
|
||||||
}
|
}
|
||||||
sel, rest = selectorOf(tail[0]), tail[1:]
|
sel, rest = grammar.SelectorOf(tail[0]), tail[1:]
|
||||||
if len(rest) > 0 && isSelector(rest[0]) {
|
if len(rest) > 0 && grammar.IsSelector(rest[0]) {
|
||||||
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.segment, sel)
|
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.segment, sel)
|
||||||
}
|
}
|
||||||
return sel, rest, nil
|
return sel, rest, nil
|
||||||
@@ -542,20 +400,7 @@ func checkPathResolves(n node, tail []string, level string) error {
|
|||||||
func compilePath(n node, tail []string) pathCheck {
|
func compilePath(n node, tail []string) pathCheck {
|
||||||
w := pathCheck{tail: tail}
|
w := pathCheck{tail: tail}
|
||||||
if _, err := w.run(n); err != nil {
|
if _, err := w.run(n); err != nil {
|
||||||
panic(internalError("%s: %v; the path should have been proved before it was compiled", joinSegments(tail), err))
|
panic(internalError("%s: %v; the path should have been proved before it was compiled", grammar.JoinSegments(tail), err))
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// joinSegments spells segments as a path: a selector attaches to the name before it, and
|
|
||||||
// so do a ".." and the name after it.
|
|
||||||
func joinSegments(segs []string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for i, s := range segs {
|
|
||||||
if b.Len() > 0 && !isSelector(s) && s != ".." && segs[i-1] != ".." {
|
|
||||||
b.WriteByte('.')
|
|
||||||
}
|
|
||||||
b.WriteString(s)
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Column is one rendered column of a record. Value is the rendered text, which a
|
// Column is one rendered column of a record. Value is the rendered text, which a
|
||||||
@@ -123,7 +124,7 @@ func literal(c Column, quote func(string) string, nullText string) string {
|
|||||||
func (f *Generator) FakeRecord(path string) (*Record, error) {
|
func (f *Generator) FakeRecord(path string) (*Record, error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
segments, err := splitPath(path)
|
segments, err := grammar.SplitPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
|||||||
}
|
}
|
||||||
shape := f.recordShapeOf(n)
|
shape := f.recordShapeOf(n)
|
||||||
if errors.Is(shape.err, ErrNoColumns) {
|
if errors.Is(shape.err, ErrNoColumns) {
|
||||||
ns := nameSegments(segments)
|
ns := grammar.NameSegments(segments)
|
||||||
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, shape.err, ns[len(ns)-1], path)
|
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, shape.err, ns[len(ns)-1], path)
|
||||||
}
|
}
|
||||||
if shape.err != nil {
|
if shape.err != nil {
|
||||||
|
|||||||
+12
-45
@@ -3,11 +3,9 @@ package fejkdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
|
||||||
|
|
||||||
// A reference names a node by path rather than as a sibling field: {/a.b} from the
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
// data root, {.a} from the folder this file sits in, {..a} from the folder above.
|
)
|
||||||
func isRef(name string) bool { return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "/") }
|
|
||||||
|
|
||||||
// refBinding is what a reference resolves to: the head its category is held
|
// refBinding is what a reference resolves to: the head its category is held
|
||||||
// under, and the tail read into it.
|
// under, and the tail read into it.
|
||||||
@@ -16,40 +14,9 @@ type refBinding struct {
|
|||||||
tail []string
|
tail []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// refShape splits a reference into its sigil and the dotted path after it.
|
|
||||||
func refShape(name string) (sigil, rest string, err error) {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(name, "/"):
|
|
||||||
sigil, rest = "/", name[1:]
|
|
||||||
case strings.HasPrefix(name, ".."):
|
|
||||||
sigil, rest = "..", name[2:]
|
|
||||||
default:
|
|
||||||
sigil, rest = ".", name[1:]
|
|
||||||
}
|
|
||||||
if rest == "" {
|
|
||||||
return "", "", fmt.Errorf("reference has no path")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(rest, "/") {
|
|
||||||
return "", "", fmt.Errorf("the path after %s starts at a name, not a /; write {%s%s}", sigil, sigil, rest[1:])
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(rest, ".") {
|
|
||||||
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
|
||||||
}
|
|
||||||
segs, err := splitPath(rest)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
for _, seg := range segs {
|
|
||||||
if seg == "" {
|
|
||||||
return "", "", fmt.Errorf("path has an empty segment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sigil, rest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// refSegments resolves a reference written in folder to a path from the root.
|
// refSegments resolves a reference written in folder to a path from the root.
|
||||||
func refSegments(name string, folder []string) ([]string, error) {
|
func refSegments(name string, folder []string) ([]string, error) {
|
||||||
sigil, rest, err := refShape(name)
|
sigil, rest, err := grammar.RefShape(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -63,7 +30,7 @@ func refSegments(name string, folder []string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
base = folder[:len(folder)-1]
|
base = folder[:len(folder)-1]
|
||||||
}
|
}
|
||||||
segs, _ := splitPath(rest) // refShape proved it splits
|
segs, _ := grammar.SplitPath(rest) // grammar.RefShape proved it splits
|
||||||
return append(append([]string{}, base...), segs...), nil
|
return append(append([]string{}, base...), segs...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +135,7 @@ type columnRead struct {
|
|||||||
// reference or name, once the names are linked.
|
// reference or name, once the names are linked.
|
||||||
func linkColumnRead(_ string, t *template) error {
|
func linkColumnRead(_ string, t *template) error {
|
||||||
ops := t.compiled.ops
|
ops := t.compiled.ops
|
||||||
if t.repeat != 1 || len(ops) != 1 || ops[0].kind != nameRead || len(ops[0].arms) != 1 {
|
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a := ops[0].arms[0]
|
a := ops[0].arms[0]
|
||||||
@@ -181,7 +148,7 @@ func linkColumnRead(_ string, t *template) error {
|
|||||||
ref := b.binder.link.refs[b.ref]
|
ref := b.binder.link.refs[b.ref]
|
||||||
head, category = b.head, ref.head[1:]
|
head, category = b.head, ref.head[1:]
|
||||||
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
|
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
|
||||||
case !isRef(a.head):
|
case !grammar.IsRef(a.head):
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
target, isTemplate := head.(*template)
|
target, isTemplate := head.(*template)
|
||||||
@@ -214,7 +181,7 @@ func resolveCategory(root map[string]node, segments []string) (categorySegs []st
|
|||||||
switch {
|
switch {
|
||||||
case i == len(segments):
|
case i == len(segments):
|
||||||
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
||||||
case isSelector(segments[i]):
|
case grammar.IsSelector(segments[i]):
|
||||||
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
|
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
|
||||||
}
|
}
|
||||||
n, ok := g.children[segments[i]]
|
n, ok := g.children[segments[i]]
|
||||||
@@ -239,16 +206,16 @@ func folderAt(root *folder, segs []string) (*folder, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// refTokens returns the reference names a format reads, as tokens or as operands, or binds.
|
// refTokens returns the reference names a format reads, as tokens or as operands, or binds.
|
||||||
func refTokens(toks []formatToken) []string {
|
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 := tok.Names
|
||||||
if tok.kind == nameBind {
|
if tok.Kind == grammar.NameBind {
|
||||||
names = []string{tok.boundRef}
|
names = []string{tok.BoundRef}
|
||||||
}
|
}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if isRef(name) && !seen[name] {
|
if grammar.IsRef(name) && !seen[name] {
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
refs = append(refs, name)
|
refs = append(refs, name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rng is the randomness a builtin sample draws from, which *drawstate.State satisfies. The
|
// rng is the randomness a builtin sample draws from, which *drawstate.State satisfies. The
|
||||||
@@ -21,7 +22,7 @@ type rng interface {
|
|||||||
func (f *Generator) Fake(path string) (string, error) {
|
func (f *Generator) Fake(path string) (string, error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
segments, err := splitPath(path)
|
segments, err := grammar.SplitPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("fejkdata: %w", err)
|
return "", fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
@@ -127,12 +128,12 @@ func expand(s *drawstate.State, t *template, sc renderScope) string {
|
|||||||
b.Grow(t.compiled.grow)
|
b.Grow(t.compiled.grow)
|
||||||
for i := range t.compiled.ops {
|
for i := range t.compiled.ops {
|
||||||
o := &t.compiled.ops[i]
|
o := &t.compiled.ops[i]
|
||||||
switch o.kind {
|
switch o.Kind {
|
||||||
case literalRun:
|
case grammar.LiteralRun:
|
||||||
b.WriteString(o.lit)
|
b.WriteString(o.Lit)
|
||||||
case nameRead:
|
case grammar.NameRead:
|
||||||
b.WriteString(readField(s, t, sc, o.arms[s.IntN(len(o.arms))]).text)
|
b.WriteString(readField(s, t, sc, o.arms[s.IntN(len(o.arms))]).text)
|
||||||
case builtinCall:
|
case grammar.BuiltinCall:
|
||||||
var operands []string
|
var operands []string
|
||||||
if len(o.operands) > 0 {
|
if len(o.operands) > 0 {
|
||||||
operands = make([]string, len(o.operands))
|
operands = make([]string, len(o.operands))
|
||||||
@@ -153,11 +154,11 @@ func readField(s *drawstate.State, t *template, sc renderScope, a arm) readValue
|
|||||||
switch {
|
switch {
|
||||||
case a.kind == namedRead:
|
case a.kind == namedRead:
|
||||||
return readName(s, sc, a)
|
return readName(s, sc, a)
|
||||||
case sc.pick != nil && !isRef(a.head) && sc.keeps(a):
|
case sc.pick != nil && !grammar.IsRef(a.head) && sc.keeps(a):
|
||||||
return readUnder(s, t, sc, a)
|
return readUnder(s, t, sc, a)
|
||||||
}
|
}
|
||||||
sc.pick = nil
|
sc.pick = nil
|
||||||
if !isRef(a.head) {
|
if !grammar.IsRef(a.head) {
|
||||||
leaf, _ := drawSteps(s, t.head(a.head), a.steps, nil, nil, a.levels)
|
leaf, _ := drawSteps(s, t.head(a.head), a.steps, nil, nil, a.levels)
|
||||||
return renderLeaf(s, leaf, sc)
|
return renderLeaf(s, leaf, sc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
|
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
|
||||||
@@ -222,14 +223,14 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
|||||||
if err := checkTaggedType(sf); err != nil {
|
if err := checkTaggedType(sf); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
inline, err := isTemplate(tag)
|
inline, err := grammar.IsTemplate(tag)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return nil, err
|
return nil, err
|
||||||
case inline:
|
case inline:
|
||||||
return inputValue(tag)
|
return inputValue(tag)
|
||||||
}
|
}
|
||||||
if err := checkPathNames(tag); err != nil {
|
if err := grammar.CheckPathNames(tag); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return "{/" + tag + "}", nil
|
return "{/" + tag + "}", nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// table is a category whose rows come from a TSV beside it: the header names the
|
// table is a category whose rows come from a TSV beside it: the header names the
|
||||||
@@ -158,7 +159,7 @@ func (t *table) parseRows(data string) error {
|
|||||||
t.header = strings.Split(header, "\t")
|
t.header = strings.Split(header, "\t")
|
||||||
t.col = make(map[string]int, len(t.header))
|
t.col = make(map[string]int, len(t.header))
|
||||||
for i, name := range t.header {
|
for i, name := range t.header {
|
||||||
if err := checkName(name); err != nil {
|
if err := grammar.CheckName(name); err != nil {
|
||||||
return fmt.Errorf("column %w", err)
|
return fmt.Errorf("column %w", err)
|
||||||
}
|
}
|
||||||
if _, dup := t.col[name]; dup {
|
if _, dup := t.col[name]; dup {
|
||||||
@@ -321,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 := parseFormat(format)
|
toks, err := grammar.ParseFormat(format, builtinOperands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -329,12 +330,12 @@ func (t *table) compileRowFormat(format string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, tok := range toks {
|
for _, tok := range toks {
|
||||||
if tok.kind != nameRead {
|
if tok.Kind != grammar.NameRead {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, name := range tok.names {
|
for _, name := range tok.Names {
|
||||||
a := splitArm(name, nil)
|
a := splitArm(name, nil)
|
||||||
if _, ok := t.col[a.head]; !ok && !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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,10 +357,10 @@ func (t *table) compileRowFormat(format string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// refuseTableBinding refuses a table's format or cell binding a name.
|
// refuseTableBinding refuses a table's format or cell binding a name.
|
||||||
func refuseTableBinding(toks []formatToken) error {
|
func refuseTableBinding(toks []grammar.Token) error {
|
||||||
for _, tok := range toks {
|
for _, tok := range toks {
|
||||||
if tok.kind == nameBind {
|
if tok.Kind == grammar.NameBind {
|
||||||
return fmt.Errorf("token {%s}: a table binds no name; bind it in a template reading the table", tok.body)
|
return fmt.Errorf("token {%s}: a table binds no name; bind it in a template reading the table", tok.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/drawstate"
|
"github.com/larvit/fejkdata/internal/drawstate"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// transforms are the builtins that rewrite one operand's value; they nest, so
|
// transforms are the builtins that rewrite one operand's value; they nest, so
|
||||||
@@ -30,7 +31,7 @@ func withTransforms(calls map[string]builtin) map[string]builtin {
|
|||||||
// field it finally names and the transforms to apply, innermost last.
|
// field it finally names and the transforms to apply, innermost last.
|
||||||
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
|
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
|
||||||
for {
|
for {
|
||||||
name, args, isCall := funcCall(arg)
|
name, args, isCall := grammar.FuncCall(arg)
|
||||||
if !isCall {
|
if !isCall {
|
||||||
return arg, chain, nil
|
return arg, chain, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-18
@@ -8,6 +8,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// proven is what a proof knows of every render of a node: bounds on the number each
|
// proven is what a proof knows of every render of a node: bounds on the number each
|
||||||
@@ -191,9 +193,9 @@ func (p *valueProof) proveTemplate(t *template) proven {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
o := t.compiled.ops[0]
|
o := t.compiled.ops[0]
|
||||||
body, name, args := o.body, o.fn, o.args
|
body, name, args := o.Body, o.Fn, o.Args
|
||||||
switch _, isTransform := transforms[name]; {
|
switch _, isTransform := transforms[name]; {
|
||||||
case o.kind == nameRead:
|
case o.Kind == grammar.NameRead:
|
||||||
var leaves []node
|
var leaves []node
|
||||||
for _, a := range o.arms {
|
for _, a := range o.arms {
|
||||||
leaves = append(leaves, a.leaves...)
|
leaves = append(leaves, a.leaves...)
|
||||||
@@ -210,12 +212,12 @@ 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])
|
||||||
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) {
|
||||||
doubt = calcText(expr) + " is not proven within 1e300"
|
doubt = grammar.CalcText(expr) + " is not proven within 1e300"
|
||||||
}
|
}
|
||||||
if doubt != "" {
|
if doubt != "" {
|
||||||
return unproven(fmt.Sprintf("{%s}: %s", body, doubt))
|
return unproven(fmt.Sprintf("{%s}: %s", body, doubt))
|
||||||
@@ -228,27 +230,27 @@ func (p *valueProof) proveCalc(o op) proven {
|
|||||||
const calcLimit = 1e300
|
const calcLimit = 1e300
|
||||||
|
|
||||||
// proveExpr bounds a calc expression from its operands, or says why it cannot.
|
// proveExpr bounds a calc expression from its operands, or says why it cannot.
|
||||||
func (p *valueProof) proveExpr(n calcNode, operand func(name string) proven) (proven, string) {
|
func (p *valueProof) proveExpr(n grammar.CalcNode, operand func(name string) proven) (proven, string) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNum:
|
case grammar.CalcNum:
|
||||||
v := float64(n)
|
v := float64(n)
|
||||||
return bounded(v, v, v == math.Trunc(v)), ""
|
return bounded(v, v, v == math.Trunc(v)), ""
|
||||||
case calcVar:
|
case grammar.CalcVar:
|
||||||
v := operand(string(n))
|
v := operand(n.Name)
|
||||||
if v.notOperand != "" {
|
if v.notOperand != "" {
|
||||||
return proven{}, fmt.Sprintf("operand %q: %s", string(n), v.notOperand)
|
return proven{}, fmt.Sprintf("operand %q: %s", n.Name, v.notOperand)
|
||||||
}
|
}
|
||||||
return proven{lo: v.lo, hi: v.hi, nonZero: v.nonZero, integral: v.integral}, ""
|
return proven{lo: v.lo, hi: v.hi, nonZero: v.nonZero, integral: v.integral}, ""
|
||||||
case calcNeg:
|
case grammar.CalcNeg:
|
||||||
v, doubt := p.proveExpr(n.x, operand)
|
v, doubt := p.proveExpr(n.X, operand)
|
||||||
v.lo, v.hi = -v.hi, -v.lo
|
v.lo, v.hi = -v.hi, -v.lo
|
||||||
return v, doubt
|
return v, doubt
|
||||||
case calcBin:
|
case grammar.CalcBin:
|
||||||
l, doubt := p.proveExpr(n.l, operand)
|
l, doubt := p.proveExpr(n.L, operand)
|
||||||
if doubt != "" {
|
if doubt != "" {
|
||||||
return l, doubt
|
return l, doubt
|
||||||
}
|
}
|
||||||
r, doubt := p.proveExpr(n.r, operand)
|
r, doubt := p.proveExpr(n.R, operand)
|
||||||
if doubt != "" {
|
if doubt != "" {
|
||||||
return r, doubt
|
return r, doubt
|
||||||
}
|
}
|
||||||
@@ -258,10 +260,10 @@ func (p *valueProof) proveExpr(n calcNode, operand func(name string) proven) (pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// combine bounds one operation from the bounds of its sides.
|
// combine bounds one operation from the bounds of its sides.
|
||||||
func combine(n calcBin, l, r proven) (proven, string) {
|
func combine(n grammar.CalcBin, l, r proven) (proven, string) {
|
||||||
var v proven
|
var v proven
|
||||||
integral := l.integral && r.integral
|
integral := l.integral && r.integral
|
||||||
switch n.operator {
|
switch n.Operator {
|
||||||
case '+':
|
case '+':
|
||||||
v = bounded(l.lo+r.lo, l.hi+r.hi, integral)
|
v = bounded(l.lo+r.lo, l.hi+r.hi, integral)
|
||||||
case '-':
|
case '-':
|
||||||
@@ -271,13 +273,13 @@ func combine(n calcBin, l, r proven) (proven, string) {
|
|||||||
v.nonZero = max(v.nonZero, l.nonZero*r.nonZero)
|
v.nonZero = max(v.nonZero, l.nonZero*r.nonZero)
|
||||||
default:
|
default:
|
||||||
if r.nonZero == 0 {
|
if r.nonZero == 0 {
|
||||||
return v, fmt.Sprintf("divides by %s, which is not proven nonzero", calcText(n.r))
|
return v, fmt.Sprintf("divides by %s, which is not proven nonzero", grammar.CalcText(n.R))
|
||||||
}
|
}
|
||||||
m := magnitude(l) / r.nonZero
|
m := magnitude(l) / r.nonZero
|
||||||
v = proven{lo: -m, hi: m, nonZero: l.nonZero / magnitude(r)}
|
v = proven{lo: -m, hi: m, nonZero: l.nonZero / magnitude(r)}
|
||||||
}
|
}
|
||||||
if !(magnitude(v) <= calcLimit) {
|
if !(magnitude(v) <= calcLimit) {
|
||||||
return v, calcText(n) + " is not proven within 1e300"
|
return v, grammar.CalcText(n) + " is not proven within 1e300"
|
||||||
}
|
}
|
||||||
return v, ""
|
return v, ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user