Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
21 changed files with 903 additions and 853 deletions
Showing only changes of commit c2f2e86735 - Show all commits
+49 -253
View File
@@ -6,45 +6,33 @@ import (
"slices"
"strconv"
"strings"
"unicode"
"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
// read, so the evaluator touches neither the rng nor the node tree.
type calcNode interface {
eval(operands []string) float64
}
type calcNum float64 // a number literal
type calcVar string // an operand, a sibling field or a name, before indexVars places it
type calcIdx int // an operand, by its position in the values expand read
type calcNeg struct{ x calcNode }
type calcBin struct { // a + - * / b
operator byte
l, r calcNode
}
func (n calcNum) eval([]string) float64 { return float64(n) }
func (n calcIdx) eval(operands []string) float64 {
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n]), 64)
// evalCalc evaluates an expression over the operand values expand read, so the
// evaluator touches neither the rng nor the node tree.
func evalCalc(n grammar.CalcNode, operands []string) float64 {
switch n := n.(type) {
case grammar.CalcNum:
return float64(n)
case grammar.CalcVar:
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n.At]), 64)
if err != nil {
return math.NaN() // a non-numeric operand stays visible, never an error
}
return v
case grammar.CalcNeg:
return -evalCalc(n.X, operands)
case grammar.CalcBin:
return arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands))
}
panic(internalError("calc node %#v has no evaluation", n))
}
func (n calcVar) eval([]string) float64 {
panic(internalError("calc operand %q was never placed", string(n)))
}
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 {
func arith(operator byte, l, r float64) float64 {
switch operator {
case '+':
return l + r
case '-':
@@ -63,7 +51,7 @@ func checkCalc(fields map[string]node, args []string) error {
if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
}
expr, err := parseCalc(args[0])
expr, err := grammar.ParseCalc(args[0])
if err != nil {
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.
func checkCalcNames(path string, t *template) error {
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
}
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)
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 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.
// 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 {
for _, name := range calcVars(expr) {
func checkOperands(text string, expr grammar.CalcNode, operand func(name string) []node) error {
for _, name := range grammar.CalcVars(expr) {
if rendered, never := allNeverNumeric(operand(name)); never {
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
// 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) {
case calcNeg:
return constantZeroDivisor(n.x, operand)
case calcBin:
if n.operator == '/' {
if v, known := constantValue(n.r, operand); known && v == 0 {
return calcText(n.r), true
case grammar.CalcNeg:
return constantZeroDivisor(n.X, operand)
case grammar.CalcBin:
if n.Operator == '/' {
if v, known := constantValue(n.R, operand); known && v == 0 {
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 constantZeroDivisor(n.r, operand)
return constantZeroDivisor(n.R, operand)
}
return "", false
}
// 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) {
case calcNum:
case grammar.CalcNum:
return float64(n), true
case calcVar:
nodes := operand(string(n))
case grammar.CalcVar:
nodes := operand(n.Name)
if len(nodes) != 1 {
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)
return v, err == nil
case calcNeg:
v, ok := constantValue(n.x, operand)
case grammar.CalcNeg:
v, ok := constantValue(n.X, operand)
return -v, ok
case calcBin:
l, lok := constantValue(n.l, operand)
r, rok := constantValue(n.r, operand)
case grammar.CalcBin:
l, lok := constantValue(n.L, operand)
r, rok := constantValue(n.R, operand)
if !lok || !rok {
return 0, false
}
return calcBin{n.operator, calcNum(l), calcNum(r)}.eval(nil), true
return arith(n.Operator, l, r), true
}
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
// does not parse, or a choice of only such items. text is one such render.
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
}
// calcPrep parses the expression and decimals once, at compile time, and places each
// operand name at the position expand will read it into. checkCalc proved both args
// valid, so no step here can fail.
// calcPrep parses the expression and decimals once, at compile time. checkCalc proved
// both args valid, so no step here can fail.
func calcPrep(args []string) callFn {
expr := parsedCalc(args[0])
at := make(map[string]int)
for i, name := range calcVars(expr) {
at[name] = i
}
placed := indexVars(expr, at)
dp := calcDecimals(args)
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
// no message.
func parsedCalc(expr string) calcNode {
n, err := parseCalc(expr)
func parsedCalc(expr string) grammar.CalcNode {
n, err := grammar.ParseCalc(expr)
if err != nil {
panic(internalError("calc(%q) passed its check unparsed: %v", expr, err))
}
@@ -260,186 +227,15 @@ func calcDecimals(args []string) int {
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
// an expression that does not parse, so one that does not simply names nothing.
func calcOperands(args []string) []string {
if len(args) == 0 {
return nil
}
expr, err := parseCalc(args[0])
expr, err := grammar.ParseCalc(args[0])
if err != nil {
return nil
}
return 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
return grammar.CalcVars(expr)
}
+4 -2
View File
@@ -7,6 +7,8 @@ import (
"os"
"path"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
)
// 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 {
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)
}
g.children[name] = child
@@ -146,7 +148,7 @@ func loadFile(src dataSource, g *folder, full, file string, files *categoryFiles
return nil
}
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)
}
b, err := fs.ReadFile(src.fsys, full)
+53 -199
View File
@@ -5,121 +5,9 @@ import (
"strings"
"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
// 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
@@ -146,40 +34,6 @@ type builtin struct {
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 {
if n == 1 {
return ""
@@ -190,8 +44,8 @@ func plural(n int) string {
// 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
// the builtins (calc, the transforms) that validate against them.
func checkFunc(tok formatToken, fields map[string]node) error {
body, name, args := tok.body, tok.fn, tok.args
func checkFunc(tok grammar.Token, fields map[string]node) error {
body, name, args := tok.Body, tok.Fn, tok.Args
b, known := builtins[name]
if !known {
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
}
func parseChecked(format string, fields map[string]node) ([]formatToken, []unboundRead, error) {
toks, err := parseFormat(format)
func parseChecked(format string, fields map[string]node) ([]grammar.Token, []unboundRead, error) {
toks, err := grammar.ParseFormat(format, builtinOperands)
if err != nil {
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
// 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
for _, t := range toks {
u, err := checkToken(t, fields)
@@ -240,70 +94,70 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
return unbound, nil
}
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
switch t.kind {
case builtinCall:
func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error) {
switch t.Kind {
case grammar.BuiltinCall:
if err := checkFunc(t, fields); err != nil {
return nil, err
}
return checkReads(t, fields, true)
case nameBind:
case grammar.NameBind:
return nil, checkBind(t, fields)
case nameRead:
case grammar.NameRead:
unbound, err := checkReads(t, fields, false)
if err != nil {
return nil, err
}
// Last, so an arm broken on its own terms is reported as that: a repeat is
// the consequence of such a mistake, not the mistake itself.
return unbound, checkNoRepeatedArm(t.body, t.names)
return unbound, checkNoRepeatedArm(t.Body, t.Names)
}
return nil, nil
}
// 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.
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
for _, name := range t.names {
if isRef(name) {
if _, _, err := refShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.body, err)
for _, name := range t.Names {
if grammar.IsRef(name) {
if _, _, err := grammar.RefShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.Body, err)
}
continue // its target is checked at New (see linkRefs)
}
missing, err := checkArm(name, fields, !operands && len(t.names) == 1)
missing, err := checkArm(name, fields, !operands && len(t.Names) == 1)
if err == nil {
continue
}
err = fmt.Errorf("token {%s}: %w", t.body, err)
err = fmt.Errorf("token {%s}: %w", t.Body, err)
if !missing {
return nil, err
}
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
}
// 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.
func checkBind(t formatToken, fields map[string]node) error {
ref, name := t.boundRef, t.bound
if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body {
return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed)
func checkBind(t grammar.Token, fields map[string]node) error {
ref, name := t.BoundRef, t.Bound
if trimmed := strings.TrimSpace(ref) + grammar.AsWord + strings.TrimSpace(name); trimmed != t.Body {
return fmt.Errorf("token {%s}: write {%s}", t.Body, trimmed)
}
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 == "" {
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) {
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 {
return fmt.Errorf("token {%s}: name %w", t.body, err)
if err := grammar.CheckName(name); err != nil {
return fmt.Errorf("token {%s}: name %w", t.Body, err)
}
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
// template.
func checkBound(ref string, fields map[string]node) error {
if isRef(ref) {
_, _, err := refShape(ref)
if grammar.IsRef(ref) {
_, _, err := grammar.RefShape(ref)
return err
}
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
// hint never names a spelling that fails too.
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
@@ -413,7 +267,7 @@ const (
// splitArm splits one name into head and tail. refs maps a reference to what
// linkRefs resolved it to; before linking, a reference is whole.
func splitArm(name string, refs map[string]refBinding) arm {
if isRef(name) {
if grammar.IsRef(name) {
b, linked := refs[name]
if !linked || len(b.tail) == 0 {
head := name
@@ -422,11 +276,11 @@ func splitArm(name string, refs map[string]refBinding) arm {
}
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
written, _ := splitPath(rest)
return pathArm(name, b.head, sigil+joinSegments(written[:len(written)-len(b.tail)]), b.tail)
sigil, rest, _ := grammar.RefShape(name) // resolveLink proved it, and took b.tail as a suffix of its segments
written, _ := grammar.SplitPath(rest)
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 {
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,
// or a builtin already prepared with its args.
type op struct {
formatToken
arms []arm // nameRead: the '|' alternatives, split into head and tail once
grammar.Token
arms []arm // grammar.NameRead: the '|' alternatives, split into head and tail once
call callFn
// 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.
@@ -481,41 +335,41 @@ type formatOps struct {
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
for _, operand := range tok.names {
for _, operand := range tok.Names {
a := splitArm(operand, refs)
if isName(a.head) {
a.kind = namedRead
}
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) {
arms := make([]arm, len(tok.names))
for i, name := range tok.names {
func (c *formatOps) field(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) {
arms := make([]arm, len(tok.Names))
for i, name := range tok.Names {
arms[i] = splitArm(name, refs)
if isName(arms[i].head) {
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
// 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
for _, tok := range toks {
switch tok.kind {
case literalRun:
c.grow += len(tok.lit)
c.ops = append(c.ops, op{formatToken: tok})
case builtinCall:
switch tok.Kind {
case grammar.LiteralRun:
c.grow += len(tok.Lit)
c.ops = append(c.ops, op{Token: tok})
case grammar.BuiltinCall:
c.function(tok, refs, isName)
case nameRead:
case grammar.NameRead:
c.field(tok, refs, isName)
}
}
+4 -73
View File
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
)
// 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
// written with a leading /.
func IsTemplate(arg string) (bool, error) {
inline, err := isTemplate(arg)
inline, err := grammar.IsTemplate(arg)
if err != nil {
return false, fmt.Errorf("fejkdata: %w", err)
}
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) {
v, err := inputValue(input)
if err != nil {
@@ -171,7 +110,7 @@ func refuseFolderRefs(scope nodeScope) error {
return nil
}
for _, name := range refTokens(t.tokens) {
sigil, rest, err := refShape(name)
sigil, rest, err := grammar.RefShape(name)
if err != nil {
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
}
// 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
}
+194
View File
@@ -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
}
+159
View File
@@ -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:]))
}
+50
View File
@@ -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
}
+162
View File
@@ -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()
}
+41
View File
@@ -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
}
+80
View File
@@ -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
}
+16 -14
View File
@@ -5,6 +5,8 @@ import (
"sort"
"strconv"
"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
@@ -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.
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 {
for ; sc != nil; sc = sc.up {
@@ -104,16 +106,16 @@ func bindNames(root node) error {
// bindAll binds every name t's tokens bind.
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
for _, tok := range t.tokens {
if tok.kind != nameBind {
if tok.Kind != grammar.NameBind {
continue
}
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 {
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)
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)
}
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 {
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 {
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.
func linkBindings(t *template) {
for _, tok := range t.tokens {
if tok.kind != nameBind {
if tok.Kind != grammar.NameBind {
continue
}
b := t.nameScope.bindings[tok.bound]
b := t.nameScope.bindings[tok.Bound]
a := splitArm(b.ref, t.link.refs)
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 {
return namedReads(t, func(o *op, a *arm) error {
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
})
@@ -268,8 +270,8 @@ func linkNames(path string, t *template) error {
func linkName(t *template, a *arm) error {
b := t.nameScope.lookup(a.head)
if 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))
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, grammar.JoinSegments(a.tail))
}
full := append(b.tail[:len(b.tail):len(b.tail)], a.tail...)
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.levels = make([]string, len(full)+1)
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 {
b.addressed = map[string]string{}
}
+12 -10
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"slices"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
)
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
@@ -11,20 +13,20 @@ import (
// draw of w.
func checkNameReads(path string, t *template) error {
for _, tok := range t.tokens {
if tok.kind != nameBind {
if tok.Kind != grammar.NameBind {
continue
}
if err := t.nameScope.bindings[tok.bound].checkUses(); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), tok.body, err)
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 namedReads(t, func(o *op, a *arm) error {
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 {
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
@@ -73,7 +75,7 @@ func (b *nameBinding) checkUses() error {
if !reaches {
return nil
}
spelling = joinSegments(append(down, b.ref))
spelling = grammar.JoinSegments(append(down, b.ref))
}
switch {
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.
func calcReads(spelling string) bool {
n, err := parseCalc(spelling)
v, isVar := n.(calcVar)
return err == nil && isVar && string(v) == spelling
n, err := grammar.ParseCalc(spelling)
v, isVar := n.(grammar.CalcVar)
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.
@@ -132,7 +134,7 @@ func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) er
var into []arm
for _, o := range t.compiled.ops {
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
}
if _, kept := b.addressed[join(key, a.path)]; kept {
+7 -49
View File
@@ -7,6 +7,8 @@ import (
"slices"
"sort"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
)
// node is a compiled element of the namespace tree: a folder, choice, null,
@@ -48,7 +50,7 @@ func (*nullItem) isNode() {}
type template struct {
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
format string
tokens []formatToken
tokens []grammar.Token
fields map[string]node
repeat int
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.
func (t *template) head(name string) node {
if isRef(name) {
if grammar.IsRef(name) {
return t.link.refHeads[name]
}
return t.fields[name]
@@ -247,8 +249,8 @@ func (t *template) fixedText() (string, bool) {
switch {
case len(t.tokens) == 0:
return "", true
case len(t.tokens) == 1 && t.tokens[0].kind == literalRun:
return t.tokens[0].lit, true
case len(t.tokens) == 1 && t.tokens[0].Kind == grammar.LiteralRun:
return t.tokens[0].Lit, true
}
return "", false
}
@@ -397,7 +399,7 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) {
if isOption(k) {
continue
}
if err := checkName(k); err != nil {
if err := grammar.CheckName(k); err != nil {
return nil, fmt.Errorf("field %w", err)
}
n, err := compileAt(m[k], pos)
@@ -459,50 +461,6 @@ func weightOf(raw any) (float64, error) {
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 {
switch name {
case "datatype", "format", "repeat", "separator", "weight":
+11 -166
View File
@@ -5,156 +5,14 @@ import (
"strings"
"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.
func stepInto(n node, seg string) (node, error) {
switch {
case isSelector(seg):
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg))
case grammar.IsSelector(seg):
return nil, fmt.Errorf("%s is not a table, so it has no row to select", grammar.SelectorOf(seg))
case seg == "..":
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])
@@ -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
// 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 {
case len(tail) == 0 && sel == "" && !descended:
r.next = t
@@ -239,8 +97,8 @@ func (t *table) stepUp(rest []string) error {
return fmt.Errorf("path has an empty 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))
case 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:]...)))
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`, grammar.JoinSegments(append([]string{t.parentT.path}, rest[1:]...)))
}
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
continue
case *template:
if isSelector(tail[0]) {
if grammar.IsSelector(tail[0]) {
break
}
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) {
if len(tail) == 0 || !isSelector(tail[0]) {
if len(tail) == 0 || !grammar.IsSelector(tail[0]) {
return "", tail, nil
}
sel, rest = selectorOf(tail[0]), tail[1:]
if len(rest) > 0 && isSelector(rest[0]) {
sel, rest = grammar.SelectorOf(tail[0]), tail[1:]
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 sel, rest, nil
@@ -542,20 +400,7 @@ func checkPathResolves(n node, tail []string, level string) error {
func compilePath(n node, tail []string) pathCheck {
w := pathCheck{tail: tail}
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
}
// 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()
}
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"unicode/utf8"
"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
@@ -123,7 +124,7 @@ func literal(c Column, quote func(string) string, nullText string) string {
func (f *Generator) FakeRecord(path string) (*Record, error) {
f.mu.Lock()
defer f.mu.Unlock()
segments, err := splitPath(path)
segments, err := grammar.SplitPath(path)
if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
@@ -143,7 +144,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
}
shape := f.recordShapeOf(n)
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)
}
if shape.err != nil {
+12 -45
View File
@@ -3,11 +3,9 @@ package fejkdata
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, "/") }
"github.com/larvit/fejkdata/internal/grammar"
)
// refBinding is what a reference resolves to: the head its category is held
// under, and the tail read into it.
@@ -16,40 +14,9 @@ type refBinding struct {
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.
func refSegments(name string, folder []string) ([]string, error) {
sigil, rest, err := refShape(name)
sigil, rest, err := grammar.RefShape(name)
if err != nil {
return nil, err
}
@@ -63,7 +30,7 @@ func refSegments(name string, folder []string) ([]string, error) {
}
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
}
@@ -168,7 +135,7 @@ type columnRead struct {
// reference or name, once the names are linked.
func linkColumnRead(_ string, t *template) error {
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
}
a := ops[0].arms[0]
@@ -181,7 +148,7 @@ func linkColumnRead(_ string, t *template) error {
ref := b.binder.link.refs[b.ref]
head, category = b.head, ref.head[1:]
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
case !isRef(a.head):
case !grammar.IsRef(a.head):
return nil
}
target, isTemplate := head.(*template)
@@ -214,7 +181,7 @@ func resolveCategory(root map[string]node, segments []string) (categorySegs []st
switch {
case i == len(segments):
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], "."))
}
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.
func refTokens(toks []formatToken) []string {
func refTokens(toks []grammar.Token) []string {
var refs []string
seen := map[string]bool{}
for _, tok := range toks {
names := tok.names
if tok.kind == nameBind {
names = []string{tok.boundRef}
names := tok.Names
if tok.Kind == grammar.NameBind {
names = []string{tok.BoundRef}
}
for _, name := range names {
if isRef(name) && !seen[name] {
if grammar.IsRef(name) && !seen[name] {
seen[name] = true
refs = append(refs, name)
}
+9 -8
View File
@@ -5,6 +5,7 @@ import (
"strings"
"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
@@ -21,7 +22,7 @@ type rng interface {
func (f *Generator) Fake(path string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
segments, err := splitPath(path)
segments, err := grammar.SplitPath(path)
if err != nil {
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)
for i := range t.compiled.ops {
o := &t.compiled.ops[i]
switch o.kind {
case literalRun:
b.WriteString(o.lit)
case nameRead:
switch o.Kind {
case grammar.LiteralRun:
b.WriteString(o.Lit)
case grammar.NameRead:
b.WriteString(readField(s, t, sc, o.arms[s.IntN(len(o.arms))]).text)
case builtinCall:
case grammar.BuiltinCall:
var operands []string
if len(o.operands) > 0 {
operands = make([]string, len(o.operands))
@@ -153,11 +154,11 @@ func readField(s *drawstate.State, t *template, sc renderScope, a arm) readValue
switch {
case a.kind == namedRead:
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)
}
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)
return renderLeaf(s, leaf, sc)
}
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"strings"
"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
@@ -222,14 +223,14 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
if err := checkTaggedType(sf); err != nil {
return nil, err
}
inline, err := isTemplate(tag)
inline, err := grammar.IsTemplate(tag)
switch {
case err != nil:
return nil, err
case inline:
return inputValue(tag)
}
if err := checkPathNames(tag); err != nil {
if err := grammar.CheckPathNames(tag); err != nil {
return nil, err
}
return "{/" + tag + "}", nil
+9 -8
View File
@@ -9,6 +9,7 @@ import (
"sync"
"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
@@ -158,7 +159,7 @@ func (t *table) parseRows(data string) error {
t.header = strings.Split(header, "\t")
t.col = make(map[string]int, len(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)
}
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
// fields.
func (t *table) compileRowFormat(format string) error {
toks, err := parseFormat(format)
toks, err := grammar.ParseFormat(format, builtinOperands)
if err != nil {
return err
}
@@ -329,12 +330,12 @@ func (t *table) compileRowFormat(format string) error {
return err
}
for _, tok := range toks {
if tok.kind != nameRead {
if tok.Kind != grammar.NameRead {
continue
}
for _, name := range tok.names {
for _, name := range tok.Names {
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)
}
}
@@ -356,10 +357,10 @@ func (t *table) compileRowFormat(format string) error {
}
// 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 {
if tok.kind == nameBind {
return fmt.Errorf("token {%s}: a table binds no name; bind it in a template reading the table", tok.body)
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 nil
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"unicode"
"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
@@ -30,7 +31,7 @@ func withTransforms(calls map[string]builtin) map[string]builtin {
// field it finally names and the transforms to apply, innermost last.
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
for {
name, args, isCall := funcCall(arg)
name, args, isCall := grammar.FuncCall(arg)
if !isCall {
return arg, chain, nil
}
+20 -18
View File
@@ -8,6 +8,8 @@ import (
"slices"
"strconv"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
)
// 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
}
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]; {
case o.kind == nameRead:
case o.Kind == grammar.NameRead:
var leaves []node
for _, a := range o.arms {
leaves = append(leaves, a.leaves...)
@@ -210,12 +212,12 @@ func (p *valueProof) proveTemplate(t *template) proven {
}
func (p *valueProof) proveCalc(o op) proven {
body, args := o.body, o.args
body, args := o.Body, o.Args
expr := parsedCalc(args[0])
nodes := operandNodes(o)
v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) })
if doubt == "" && !(magnitude(v) <= calcLimit) {
doubt = calcText(expr) + " is not proven within 1e300"
doubt = grammar.CalcText(expr) + " is not proven within 1e300"
}
if doubt != "" {
return unproven(fmt.Sprintf("{%s}: %s", body, doubt))
@@ -228,27 +230,27 @@ func (p *valueProof) proveCalc(o op) proven {
const calcLimit = 1e300
// 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) {
case calcNum:
case grammar.CalcNum:
v := float64(n)
return bounded(v, v, v == math.Trunc(v)), ""
case calcVar:
v := operand(string(n))
case grammar.CalcVar:
v := operand(n.Name)
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}, ""
case calcNeg:
v, doubt := p.proveExpr(n.x, operand)
case grammar.CalcNeg:
v, doubt := p.proveExpr(n.X, operand)
v.lo, v.hi = -v.hi, -v.lo
return v, doubt
case calcBin:
l, doubt := p.proveExpr(n.l, operand)
case grammar.CalcBin:
l, doubt := p.proveExpr(n.L, operand)
if doubt != "" {
return l, doubt
}
r, doubt := p.proveExpr(n.r, operand)
r, doubt := p.proveExpr(n.R, operand)
if 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.
func combine(n calcBin, l, r proven) (proven, string) {
func combine(n grammar.CalcBin, l, r proven) (proven, string) {
var v proven
integral := l.integral && r.integral
switch n.operator {
switch n.Operator {
case '+':
v = bounded(l.lo+r.lo, l.hi+r.hi, integral)
case '-':
@@ -271,13 +273,13 @@ func combine(n calcBin, l, r proven) (proven, string) {
v.nonZero = max(v.nonZero, l.nonZero*r.nonZero)
default:
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
v = proven{lo: -m, hi: m, nonZero: l.nonZero / magnitude(r)}
}
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, ""
}