Answer architecture round 1: calls carry no operands in the grammar, ParseCalc returns its operands, one empty-segment check, sorted token kinds, and file items 110 and 111
This commit is contained in:
+21
-36
@@ -12,8 +12,14 @@ type CalcNode interface{ calcNode() }
|
||||
|
||||
type CalcNum float64 // a number literal
|
||||
|
||||
// CalcVar is an operand, a sibling field or a name. At is its position among the
|
||||
// distinct operands in the order the expression first names each, which CalcVars lists.
|
||||
// Calc is a parsed calc expression and the distinct operands it reads, in the order it
|
||||
// first names each.
|
||||
type Calc struct {
|
||||
Expr CalcNode
|
||||
Operands []string
|
||||
}
|
||||
|
||||
// CalcVar is an operand, a sibling field or a name, at its position in Calc.Operands.
|
||||
type CalcVar struct {
|
||||
Name string
|
||||
At int
|
||||
@@ -31,27 +37,6 @@ func (CalcVar) calcNode() {}
|
||||
func (CalcNeg) calcNode() {}
|
||||
func (CalcBin) calcNode() {}
|
||||
|
||||
// CalcVars lists the distinct operands an expression reads, each at its At.
|
||||
func CalcVars(n CalcNode) []string {
|
||||
var out []string
|
||||
var walk func(CalcNode)
|
||||
walk = func(n CalcNode) {
|
||||
switch n := n.(type) {
|
||||
case CalcVar:
|
||||
if n.At == len(out) {
|
||||
out = append(out, n.Name)
|
||||
}
|
||||
case CalcNeg:
|
||||
walk(n.X)
|
||||
case CalcBin:
|
||||
walk(n.L)
|
||||
walk(n.R)
|
||||
}
|
||||
}
|
||||
walk(n)
|
||||
return out
|
||||
}
|
||||
|
||||
// CalcText spells an expression node the way an author would read it.
|
||||
func CalcText(n CalcNode) string {
|
||||
switch n := n.(type) {
|
||||
@@ -70,25 +55,25 @@ func CalcText(n CalcNode) string {
|
||||
// calcParser is a recursive-descent parser over the expression runes, threading
|
||||
// expr -> term -> factor for the standard * / before + - precedence.
|
||||
type calcParser struct {
|
||||
rs []rune
|
||||
pos int
|
||||
vars map[string]int
|
||||
rs []rune
|
||||
pos int
|
||||
operands []string
|
||||
}
|
||||
|
||||
// ParseCalc parses a whole expression, requiring it to consume all input.
|
||||
func ParseCalc(expr string) (CalcNode, error) {
|
||||
p := &calcParser{rs: []rune(expr), vars: map[string]int{}}
|
||||
func ParseCalc(expr string) (Calc, error) {
|
||||
p := &calcParser{rs: []rune(expr)}
|
||||
if p.space(); p.pos >= len(p.rs) {
|
||||
return nil, fmt.Errorf("empty expression")
|
||||
return Calc{}, fmt.Errorf("empty expression")
|
||||
}
|
||||
n, err := p.expr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Calc{}, err
|
||||
}
|
||||
if p.space(); p.pos != len(p.rs) {
|
||||
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
||||
return Calc{}, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
||||
}
|
||||
return n, nil
|
||||
return Calc{n, p.operands}, nil
|
||||
}
|
||||
|
||||
func (p *calcParser) space() {
|
||||
@@ -185,10 +170,10 @@ func (p *calcParser) ident() (CalcNode, error) {
|
||||
}
|
||||
}
|
||||
name := string(p.rs[start:p.pos])
|
||||
at, seen := p.vars[name]
|
||||
if !seen {
|
||||
at = len(p.vars)
|
||||
p.vars[name] = at
|
||||
at := slices.Index(p.operands, name)
|
||||
if at < 0 {
|
||||
at = len(p.operands)
|
||||
p.operands = append(p.operands, name)
|
||||
}
|
||||
return CalcVar{name, at}, nil
|
||||
}
|
||||
|
||||
+13
-16
@@ -1,5 +1,6 @@
|
||||
// Package grammar is how the template language is written: format tokens, paths and
|
||||
// their selectors, reference sigils, names, and calc expressions. It reads strings only.
|
||||
// their selectors, reference sigils, names, calc expressions, and whether an argument is a
|
||||
// template or a path. It reads strings only.
|
||||
package grammar
|
||||
|
||||
import (
|
||||
@@ -60,12 +61,12 @@ func eachScanUnit(format string, fn func(scanUnit) error) error {
|
||||
|
||||
// Token is one parsed unit of a format.
|
||||
type Token struct {
|
||||
Kind TokenKind
|
||||
Lit string // LiteralRun
|
||||
Body string // the braces' content, as written
|
||||
Fn string // BuiltinCall
|
||||
Args []string // BuiltinCall
|
||||
Names []string // NameRead: the '|' arms; BuiltinCall: the operands its builtin reads
|
||||
Kind TokenKind
|
||||
Lit string // LiteralRun
|
||||
Body string // the braces' content, as written
|
||||
Fn string // BuiltinCall
|
||||
Args []string // BuiltinCall
|
||||
Arms []string // NameRead: the '|' alternatives
|
||||
// NameBind: the reference it binds, and the name.
|
||||
BoundRef, Bound string
|
||||
}
|
||||
@@ -74,16 +75,16 @@ type TokenKind uint8
|
||||
|
||||
const (
|
||||
BuiltinCall TokenKind = iota + 1
|
||||
NameRead
|
||||
LiteralRun
|
||||
NameBind
|
||||
NameRead
|
||||
)
|
||||
|
||||
const AsWord = " as "
|
||||
|
||||
// ParseFormat is the one reading of a format's tokens: a '(' outside a selector makes
|
||||
// a token a call. operands lists the operands a call reads; nil reads none.
|
||||
func ParseFormat(format string, operands func(fn string, args []string) []string) ([]Token, error) {
|
||||
// a token a call.
|
||||
func ParseFormat(format string) ([]Token, error) {
|
||||
var toks []Token
|
||||
var lit strings.Builder
|
||||
flush := func() {
|
||||
@@ -103,18 +104,14 @@ func ParseFormat(format string, operands func(fn string, args []string) []string
|
||||
return nil
|
||||
}
|
||||
if IndexOutside(u.body, '(') < 0 {
|
||||
toks = append(toks, Token{Kind: NameRead, Body: u.body, Names: SplitOutside(u.body, '|')})
|
||||
toks = append(toks, Token{Kind: NameRead, Body: u.body, Arms: SplitOutside(u.body, '|')})
|
||||
return nil
|
||||
}
|
||||
name, args, ok := FuncCall(u.body)
|
||||
if !ok {
|
||||
return fmt.Errorf("malformed function token {%s}", u.body)
|
||||
}
|
||||
tok := Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args}
|
||||
if operands != nil {
|
||||
tok.Names = operands(name, args)
|
||||
}
|
||||
toks = append(toks, tok)
|
||||
toks = append(toks, Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -160,3 +161,12 @@ func JoinSegments(segs []string) string {
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// CheckSegments refuses an unfinished path: "a." and "a...b" each have a segment naming
|
||||
// nothing.
|
||||
func CheckSegments(segs []string) error {
|
||||
if slices.Contains(segs, "") {
|
||||
return fmt.Errorf("path has an empty segment")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,10 +32,8 @@ func RefShape(name string) (sigil, rest string, err error) {
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, seg := range segs {
|
||||
if seg == "" {
|
||||
return "", "", fmt.Errorf("path has an empty segment")
|
||||
}
|
||||
if err := CheckSegments(segs); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return sigil, rest, nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsTemplate reports whether arg is an inline template rather than a path, by its shape;
|
||||
// the root's IsTemplate documents the rule.
|
||||
func IsTemplate(arg string) (bool, error) {
|
||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||
if path, lone := loneReference(arg); lone {
|
||||
@@ -56,7 +54,7 @@ func loneReference(arg string) (string, bool) {
|
||||
if !isString {
|
||||
return "", false
|
||||
}
|
||||
toks, err := ParseFormat(format, nil)
|
||||
toks, err := ParseFormat(format)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -73,8 +71,8 @@ func loneReference(arg string) (string, bool) {
|
||||
|
||||
// loneRef is the reference a format of one reference token and nothing else reads.
|
||||
func loneRef(toks []Token) (string, bool) {
|
||||
if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Names) != 1 || !IsRef(toks[0].Names[0]) {
|
||||
if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) {
|
||||
return "", false
|
||||
}
|
||||
return toks[0].Names[0], true
|
||||
return toks[0].Arms[0], true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user