into an AST (Parse) and evaluate that AST against a player roster
(Eval). Rule evaluators (internal/rule) compose these calls when they
need to compute a number or a set from the candidate match.
players.<attr> and players.<attr>[<key>] (the latter for
string_number_map attributes)
teams[<name>].players.<attr>, including teams[*]
one-argument function calls: flatten, avg, min, max, sum,
count, set_intersection
Eval(n Node, ctx *EvalContext) (Value, error) — evaluates a parsed
node. EvalContext supplies the unsorted player pool plus a
team-name → players map.
Value — tagged union returned by Eval. Notable kinds:
KindNumber, KindString
KindNumberList, KindStringList
KindStringMatrix (one string list per player, used by
set_intersection)
KindNone — produced when an aggregate (avg, min, max) is
applied to an empty list. Rule evaluators interpret this as "the rule
is not yet evaluable" and skip the comparison rather than failing it,
which is what lets the matchmaker grow a candidate match step by step.
Design notes
The grammar is intentionally narrow. The FlexMatch reference contains
many more compositions than we strictly need; add them here as concrete
rule examples motivate them, not speculatively.
The parser is hand-written and zero-dependency; benchmark before reaching
for a parser generator.
Values share backing slices with the input where practical. Callers must
not mutate slices read out of a Value.
Package expr implements a small parser and evaluator for the subset of
FlexMatch property expressions used inside rule measurements and reference
values, e.g. "avg(flatten(players.skill))" or "teams[red].players.skill".
const (
KindNone Kind = iota KindNumber
KindString
KindNumberList
KindStringList
KindNumberMatrix // []NumberList - one entry per player (used by set/avg ops) KindStringMatrix // []StringList - one entry per player)
type PlayerAccess struct {
Scope string// "", team name, or "*" Attr string MapKey string// optional, for string_number_map HasIndex bool}
PlayerAccess refers to a player attribute, optionally indexed for SDM types.
Scope is the team name; "" means "all players" (no team scope), "*" means
"every team".