package main
import (
"fmt"
"math/bits"
"math/rand"
"strings"
"time"
"github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer"
)
// GRAMMAR (EBNF):
//
// path ::= list
// list ::= expr (',' expr)*
// expr ::= parallelGroup | chainGroup | REMOTE | LOCAL
// parallelGroup ::= '{' list '}'
// chainGroup ::= '(' list ')'
// REMOTE ::= [A-Z]+
// LOCAL ::= 'cpu' | 'mem' | 'disk' | 'net' | 'noop' | 'io'
type opKind int
const (
opLocal opKind = iota // local op: cpu, mem, disk, net, noop, io
opRemote // single RPC call to a named service
opChain // sequential composition of ops (mixed kinds ok)
opParallel // fan-out to N chains, wait on all
)
type op struct {
kind opKind
name string
subOps []*op
}
func (o *op) String() string {
switch o.kind {
case opLocal, opRemote:
return o.name
case opChain:
ops := make([]string, 0, len(o.subOps))
for _, so := range o.subOps {
ops = append(ops, so.String())
}
return fmt.Sprintf("(%s)", strings.Join(ops, ","))
case opParallel:
ops := make([]string, 0, len(o.subOps))
for _, so := range o.subOps {
ops = append(ops, so.String())
}
return fmt.Sprintf("{%s}", strings.Join(ops, ","))
}
return ""
}
func geom(rng *rand.Rand, depth int) int {
r := bits.LeadingZeros32(rng.Uint32()) & 3
return max(1, r>>depth)
}
var pathLexer = lexer.MustSimple([]lexer.SimpleRule{
{Name: "Remote", Pattern: `[A-Z]+`},
{Name: "Local", Pattern: `cpu|mem|disk|net|noop|io`},
{Name: "Punct", Pattern: `[\[\](){}^,]`},
{Name: "WS", Pattern: `\s+`},
})
type grammarParallelGroup struct {
List *grammarList `parser:"'{' @@ '}'"`
}
type grammarChainGroup struct {
List *grammarList `parser:"'(' @@ ')'"`
}
type grammarExpr struct {
Parallel *grammarParallelGroup `parser:" @@"`
Chain *grammarChainGroup `parser:"| @@"`
Svc string `parser:"| @Remote"`
Local string `parser:"| @Local"`
}
type grammarList struct {
Exprs []*grammarExpr `parser:"@@ (',' @@)*"`
}
func parsePath(s string) (*op, error) {
var parser = participle.MustBuild[grammarList](
participle.Lexer(pathLexer),
participle.Elide("WS"),
)
ast, err := parser.ParseString("", s)
if err != nil {
return nil, err
}
return parseList(ast)
}
func parseList(g *grammarList) (*op, error) {
ops := make([]*op, 0, len(g.Exprs))
for _, expr := range g.Exprs {
o, err := parseExpr(expr)
if err != nil {
return nil, err
}
ops = append(ops, o)
}
return &op{kind: opChain, subOps: ops}, nil
}
func parseExpr(g *grammarExpr) (*op, error) {
switch {
case g.Parallel != nil:
inner, err := parseList(g.Parallel.List)
if err != nil {
return nil, err
}
return &op{kind: opParallel, subOps: inner.subOps}, nil
case g.Chain != nil:
return parseList(g.Chain.List)
case g.Svc != "":
return &op{kind: opRemote, name: g.Svc}, nil
case g.Local != "":
return &op{kind: opLocal, name: g.Local}, nil
}
return nil, fmt.Errorf("empty expr")
}
func genOp(rng *rand.Rand, locals, svcs []string, depth int, b *strings.Builder) {
maxKind := 4 >> max(0, depth-1)
switch rng.Intn(maxKind) {
case 0:
b.WriteString(locals[rng.Intn(len(locals))])
case 1:
b.WriteString(svcs[rng.Intn(len(svcs))])
case 2:
n := 2 + rng.Intn(2)
b.WriteByte('(')
for i := 0; i < n; i++ {
if i > 0 {
b.WriteByte(',')
}
genNewOpExpr(rng, locals, svcs, depth+1, b)
}
b.WriteByte(')')
default:
n := 2 + rng.Intn(2)
b.WriteByte('{')
for i := 0; i < n; i++ {
if i > 0 {
b.WriteByte(',')
}
genNewOpExpr(rng, locals, svcs, depth+1, b)
}
b.WriteByte('}')
}
}
func genNewOpExpr(rng *rand.Rand, locals, svcs []string, d int, b *strings.Builder) {
for i := 0; i < geom(rng, d); i++ {
if i > 0 {
b.WriteByte(',')
}
genOp(rng, locals, svcs, d, b)
}
}
func main() {
for i := 0; i < 5; i++ {
var b strings.Builder
genNewOpExpr(
rand.New(rand.NewSource(time.Now().UnixNano())),
[]string{"cpu", "mem"}, // local
[]string{"AA", "AB", "AC"}, // remote
0, // starting depth, used to manage maximum depth...
&b,
)
fmt.Println("> ", b.String())
}
}