diff --git a/calc.go b/calc.go index 5205611..b0d5330 100644 --- a/calc.go +++ b/calc.go @@ -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) - if err != nil { - return math.NaN() // a non-numeric operand stays visible, never an error +// 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)) } - return v + 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) } diff --git a/data.go b/data.go index e583617..035cc87 100644 --- a/data.go +++ b/data.go @@ -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) diff --git a/format.go b/format.go index 604abe0..af233db 100644 --- a/format.go +++ b/format.go @@ -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) } } diff --git a/inline.go b/inline.go index e6038ce..2d38ed7 100644 --- a/inline.go +++ b/inline.go @@ -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 -} diff --git a/internal/grammar/calc.go b/internal/grammar/calc.go new file mode 100644 index 0000000..6b69b09 --- /dev/null +++ b/internal/grammar/calc.go @@ -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 +} diff --git a/internal/grammar/format.go b/internal/grammar/format.go new file mode 100644 index 0000000..4f7b7a4 --- /dev/null +++ b/internal/grammar/format.go @@ -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:])) +} diff --git a/internal/grammar/name.go b/internal/grammar/name.go new file mode 100644 index 0000000..7ac185d --- /dev/null +++ b/internal/grammar/name.go @@ -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 +} diff --git a/internal/grammar/path.go b/internal/grammar/path.go new file mode 100644 index 0000000..3438a8f --- /dev/null +++ b/internal/grammar/path.go @@ -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() +} diff --git a/internal/grammar/reference.go b/internal/grammar/reference.go new file mode 100644 index 0000000..69b7ea9 --- /dev/null +++ b/internal/grammar/reference.go @@ -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 +} diff --git a/internal/grammar/template.go b/internal/grammar/template.go new file mode 100644 index 0000000..fc49929 --- /dev/null +++ b/internal/grammar/template.go @@ -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 +} diff --git a/name.go b/name.go index cf027c8..a605276 100644 --- a/name.go +++ b/name.go @@ -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{} } diff --git a/namefence.go b/namefence.go index f1aa824..be39c95 100644 --- a/namefence.go +++ b/namefence.go @@ -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 { diff --git a/node.go b/node.go index 91635c7..daf8e4c 100644 --- a/node.go +++ b/node.go @@ -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": diff --git a/path.go b/path.go index ff16816..5d1b6d5 100644 --- a/path.go +++ b/path.go @@ -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() -} diff --git a/record.go b/record.go index 604a3a1..70e41fa 100644 --- a/record.go +++ b/record.go @@ -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 { diff --git a/reference.go b/reference.go index 3c68d69..ab6ece8 100644 --- a/reference.go +++ b/reference.go @@ -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) } diff --git a/render.go b/render.go index 3b03d7c..2b724b0 100644 --- a/render.go +++ b/render.go @@ -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) } diff --git a/struct.go b/struct.go index f41e684..2c7e1ef 100644 --- a/struct.go +++ b/struct.go @@ -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 diff --git a/table.go b/table.go index 553a580..58e9eeb 100644 --- a/table.go +++ b/table.go @@ -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 diff --git a/transform.go b/transform.go index 005323e..2dd0feb 100644 --- a/transform.go +++ b/transform.go @@ -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 } diff --git a/valueproof.go b/valueproof.go index d353ea0..9f93c5a 100644 --- a/valueproof.go +++ b/valueproof.go @@ -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, "" }