525 lines
17 KiB
Go
525 lines
17 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larvit/fejkdata/internal/drawstate"
|
|
)
|
|
|
|
// 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
|
|
// (calc and the transforms name them). All must stay pure over (rng, emitted, args) so
|
|
// seeded output is reproducible; seq advances per-generator counter state, which is
|
|
// itself deterministic. arity is the exact arg count, or -1 for variadic (then
|
|
// checkArgs does all the validation).
|
|
type builtin struct {
|
|
arity int
|
|
// prep parses validated args once, at compile time, into the closure expand calls.
|
|
prep func(args []string) callFn
|
|
checkArgs func(fields map[string]node, args []string) error
|
|
// operands names the fields the call reads, which expand renders for it; nil
|
|
// for a builtin that reads none.
|
|
operands func(args []string) []string
|
|
// noRefOperands says no operand can be a reference, so a name read once there is not refused.
|
|
noRefOperands bool
|
|
// proveNumber bounds the number a call's text reads as, token its body, and says which
|
|
// datatypes that text is not; set it where every render reads as a finite number, which
|
|
// makes the call a calc operand, and leave it nil otherwise.
|
|
proveNumber func(token string, prints DataType, args []string) proven
|
|
// prints is the datatype a call's text is, handed to proveNumber: DataTypeString where it
|
|
// reads as a number no column should type, as digits' leading zeros; unset where
|
|
// proveNumber is nil.
|
|
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 ""
|
|
}
|
|
return "s"
|
|
}
|
|
|
|
// 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
|
|
b, known := builtins[name]
|
|
if !known {
|
|
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
|
}
|
|
if b.arity >= 0 && len(args) != b.arity {
|
|
return fmt.Errorf("token {%s}: %s takes %d argument%s, got %d", body, name, b.arity, plural(b.arity), len(args))
|
|
}
|
|
if b.checkArgs != nil {
|
|
if err := b.checkArgs(fields, args); err != nil {
|
|
return fmt.Errorf("token {%s}: %w", body, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseChecked(format string, fields map[string]node) ([]formatToken, []unboundRead, error) {
|
|
toks, err := parseFormat(format)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
unbound, err := checkTokens(toks, fields)
|
|
return toks, unbound, err
|
|
}
|
|
|
|
// unboundRead is a token reading a head no field holds, which only a name bound around the
|
|
// template can answer; err is the refusal where none does. tail is the path read into the name,
|
|
// "" for the name itself, operand marks a builtin's read of it, noRef marks such a read where no
|
|
// reference can stand, and body is the token's.
|
|
type unboundRead struct {
|
|
head, tail, body string
|
|
operand, noRef bool
|
|
err error
|
|
}
|
|
|
|
// 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) {
|
|
var unbound []unboundRead
|
|
for _, t := range toks {
|
|
u, err := checkToken(t, fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
unbound = append(unbound, u...)
|
|
}
|
|
return unbound, nil
|
|
}
|
|
|
|
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
|
|
switch t.kind {
|
|
case builtinCall:
|
|
if err := checkFunc(t, fields); err != nil {
|
|
return nil, err
|
|
}
|
|
return checkReads(t, fields, true)
|
|
case nameBind:
|
|
return nil, checkBind(t, fields)
|
|
case 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 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) {
|
|
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)
|
|
}
|
|
continue // its target is checked at New (see linkRefs)
|
|
}
|
|
missing, err := checkArm(name, fields, !operands && len(t.names) == 1)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
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})
|
|
}
|
|
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)
|
|
}
|
|
if err := checkBound(ref, fields); err != nil {
|
|
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)
|
|
}
|
|
if isOption(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)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
return err
|
|
}
|
|
missing, err := checkArm(ref, fields, false)
|
|
if missing {
|
|
return fmt.Errorf("%w; a binding names a field, or a reference, which starts with /, . or ..", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// checkArm validates one sibling name or path against a template's fields.
|
|
// wholeToken says the name is the token's entire body, so {/name} would render
|
|
// the same value and can be offered as the reference spelling. missing says no
|
|
// field holds the head, which a name may yet answer.
|
|
func checkArm(name string, fields map[string]node, wholeToken bool) (missing bool, err error) {
|
|
a := splitArm(name, nil)
|
|
if err := checkSegments(a); err != nil {
|
|
return false, err
|
|
}
|
|
field, ok := fields[a.head]
|
|
if !ok {
|
|
if a.head == "" {
|
|
return false, fmt.Errorf("a name is never empty, so this token can name no field")
|
|
}
|
|
if isOption(a.head) {
|
|
return false, fmt.Errorf("%q is an option and can never be a field", a.head)
|
|
}
|
|
if len(fields) == 0 {
|
|
hint := ""
|
|
if wholeToken && hintableRef(name) {
|
|
hint = fmt.Sprintf(" — write {/%s} to reference the data", name)
|
|
}
|
|
return true, fmt.Errorf("no field %q; a token names a sibling field, and this template has none%s", a.head, hint)
|
|
}
|
|
return true, fmt.Errorf("no field %q", a.head)
|
|
}
|
|
if err := checkPathResolves(field, a.tail, a.head); err != nil {
|
|
return false, fmt.Errorf("field %q: %w", a.head, err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// builtinOperands lists the fields a call reads as operands, empty for a builtin that
|
|
// reads none or is unknown.
|
|
func builtinOperands(name string, args []string) []string {
|
|
b, known := builtins[name]
|
|
if !known || b.operands == nil {
|
|
return nil
|
|
}
|
|
return b.operands(args)
|
|
}
|
|
|
|
// checkNoRepeatedArm rejects {a|a|b}: an alternation picks its arms evenly, so a
|
|
// repeated one is a second spelling of weight. The error names the spelling that
|
|
// does skew a pick.
|
|
func checkNoRepeatedArm(body string, names []string) error {
|
|
if len(names) < 2 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]bool, len(names))
|
|
for _, name := range names {
|
|
if seen[name] {
|
|
return fmt.Errorf("token {%s}: arm %q is repeated; an alternation picks its arms evenly, so skew the odds with a choice's weights instead", body, name)
|
|
}
|
|
seen[name] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// arm is one alternative of a {a|b} token or one operand, split into the head
|
|
// `template.head` resolves and the tail of a dotted path into it.
|
|
type arm struct {
|
|
spelling string // as written, for messages
|
|
head string
|
|
writtenHead string // head as written, sigil included
|
|
tail []string
|
|
levels []string // the path at each level the tail passes through, the head first
|
|
path string // head and tail, the one path every way of writing this read shares
|
|
steps []pathStep
|
|
leaves []node // every node the path may land on, one per variant it passes
|
|
kind armKind
|
|
named *nameBinding // namedRead: the binding of the name it reads through
|
|
}
|
|
|
|
// armKind is how expand reads an arm, fixed at compile.
|
|
type armKind uint8
|
|
|
|
const (
|
|
freshRead armKind = iota // drawn afresh at every read
|
|
namedRead // read through a name, kept in its pick
|
|
)
|
|
|
|
// 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) {
|
|
b, linked := refs[name]
|
|
if !linked || len(b.tail) == 0 {
|
|
head := name
|
|
if linked {
|
|
head = b.head
|
|
}
|
|
return arm{spelling: name, head: head, path: head}
|
|
}
|
|
sigil, rest, _ := refShape(name) // resolveLink proved it, and took b.tail as a suffix of its segments
|
|
written, _ := splitPath(rest)
|
|
return pathArm(name, b.head, sigil+joinSegments(written[:len(written)-len(b.tail)]), b.tail)
|
|
}
|
|
segs, err := splitPath(name)
|
|
if err != nil || len(segs) == 1 {
|
|
return arm{spelling: name, head: name, path: name}
|
|
}
|
|
return pathArm(name, segs[0], segs[0], segs[1:])
|
|
}
|
|
|
|
func pathArm(name, head, writtenHead string, segs []string) arm {
|
|
levels := []string{head}
|
|
for i := 0; i < len(segs)-1; i++ {
|
|
levels = append(levels, head+"."+strings.Join(segs[:i+1], "."))
|
|
}
|
|
return arm{spelling: name, head: head, writtenHead: writtenHead, tail: segs, levels: levels, path: head + "." + strings.Join(segs, ".")}
|
|
}
|
|
|
|
// checkSegments rejects an unfinished path: "{a.}" and "{a...b}" each have a
|
|
// segment naming nothing. A field really named "" would otherwise make them
|
|
// resolve, so a typo would read as a path that worked.
|
|
func checkSegments(a arm) error {
|
|
if len(a.tail) == 0 {
|
|
return nil
|
|
}
|
|
if a.head == "" {
|
|
return fmt.Errorf("path has an empty segment")
|
|
}
|
|
for _, seg := range a.tail {
|
|
if seg == "" {
|
|
return fmt.Errorf("path has an empty segment")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// callFn is a builtin prepared for one call site: its args already parsed. It reads the
|
|
// output emitted so far in the current expansion (a derivation's payload) and the
|
|
// values of the operands it named, which expand read for it.
|
|
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
|
|
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.
|
|
operands []arm
|
|
}
|
|
|
|
// formatOps is a compiled format: its ops, and the size of its literal text.
|
|
type formatOps struct {
|
|
ops []op
|
|
grow int
|
|
}
|
|
|
|
func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName func(string) bool) {
|
|
var operands []arm
|
|
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})
|
|
}
|
|
|
|
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 {
|
|
arms[i] = splitArm(name, refs)
|
|
if isName(arms[i].head) {
|
|
arms[i].kind = namedRead
|
|
}
|
|
}
|
|
c.ops = append(c.ops, op{formatToken: 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 {
|
|
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:
|
|
c.function(tok, refs, isName)
|
|
case nameRead:
|
|
c.field(tok, refs, isName)
|
|
}
|
|
}
|
|
return c
|
|
}
|