Package pego/sample
import "github.com/ornew/pego/sample"
Generated from the source of this version. Also on pkg.go.dev.
Overview
Package sample generates inputs that a grammar accepts, for testing code that consumes parse results and for seeding fuzz tests.
A Generator walks the grammar AST of a parser from its start rule and writes candidate inputs, making random decisions at ordered choices, repetitions, optional expressions and Operators Rule expressions. The decisions are reproducible: the same parser, options and seed give the same inputs with the same version of this package, whichever Go release built it (the generator's algorithm may change between versions; the package draws its random numbers with its own generator, as math/rand/v2 does not promise to keep its algorithms across Go releases). Recursion and length are bounded softly (WithMaxDepth, WithMaxLen): past the bounds, the generator prefers the shortest way to finish the input. The search itself has hard limits (WithBudget, and the limits on nesting and iterations described with WithMaxDepth and WithMaxRepeat).
Every input that the generator returns has been parsed with the parser and accepted without errors (also without errors recovered with #recover). Generating a candidate by walking the grammar is not enough under PEG semantics: an ordered choice may never reach a later alternative for some inputs, a greedy repetition consumes as much as it can (no input generated for a* a parses), and lookaheads, predicates and variables depend on the input around them. The generator steers away from such candidates where it can tell, evaluating predicates on the text it generated and checking what the parser would match as the text grows, and retries otherwise; what it cannot evaluate, such as predicates over the results of actions, is handled only by generating and checking. The design is described in docs/design/records/015-input-generation.md.
With WithCoverage, the generator prefers rules and alternatives that no accepted input has exercised yet, and Coverage reports those that remain. Invalid generates near-miss invalid inputs: single mutations of valid inputs that the parser rejects. Seed adds generated inputs to the seed corpus of a fuzz test.
Index
ConstantsVariablesfunc Generatefunc Seedtype Alternativetype Coveragetype Ftype Generatorfunc Newfunc (*Generator) Coveragefunc (*Generator) Cutsfunc (*Generator) Generatefunc (*Generator) GenerateInvalidfunc (*Generator) Invalidfunc (*Generator) Nextfunc (*Generator) Statstype Invalidtype Optionfunc WithAttemptsfunc WithBudgetfunc WithCoveragefunc WithMaxDepthfunc WithMaxLenfunc WithMaxRepeatfunc WithSeedtype RuleCutstype Stats
Constants
const (
DefaultMaxDepth = 5
DefaultMaxRepeat = 3
DefaultMaxLen = 512
DefaultAttempts = 100
DefaultBudget = 20000
)
Defaults of the options.
Variables
var ErrNoInput = errors.New("sample: no input found that the parser accepts")
ErrNoInput is returned when the generator finds no input within its attempts. The error returned wraps it with what became of the attempts (see Stats); test for it with errors.Is.
var ErrNoInvalid = errors.New("sample: no mutation found that the parser rejects")
ErrNoInvalid is returned when the generator finds valid inputs but no mutation of them that the parser rejects.
Functions
func Generate source
func Generate(p *pego.Parser, n int, opts ...Option) ([]string, error)
Generate returns up to n distinct inputs that p accepts (see Generator.Generate).
func Seed source
func Seed(f F, p *pego.Parser, n int, opts ...Option) []string
Seed adds up to n distinct inputs that p accepts to the seed corpus of the fuzz test f, each as a single string argument, and returns them; the fuzz target must therefore take one string. It verifies again that p accepts every input, and fails the test if it cannot generate any.
func FuzzParse(f *testing.F) {
p := ... // the parser under test
sample.Seed(f, p, 50)
f.Fuzz(func(t *testing.T, in string) {
p.Parse(in) // must not panic
})
}
Types
type Alternative source
type Alternative struct {
Rule string `json:"rule"`
// Kind is "choice", "operand" or "operator".
Kind string `json:"kind"`
// Index is the position of the alternative in its choice, or of the operand or operator in the
// Operators Rule expression (operators are counted across all levels).
Index int `json:"index"`
// Expr is the alternative in PEGO syntax.
Expr string `json:"expr"`
}
Alternative identifies an alternative of an ordered choice, or an operand or operator of a OperatorsRule expression.
func (Alternative) String source
func (a Alternative) String() string
type Coverage source
type Coverage struct {
// Rules counts the rules that the start rule can reach and that can match; RulesCovered those that
// an accepted input exercised.
Rules int `json:"rules"`
RulesCovered int `json:"rulesCovered"`
// Alternatives counts the alternatives of ordered choices and the operands and operators of OperatorsRule
// expressions in those rules, leaving out those that can never match or that occur only inside an
// expression that can never match, or in OperatorsRule tails that no reachable entry or prefix RHS permits.
Alternatives int `json:"alternatives"`
AlternativesCovered int `json:"alternativesCovered"`
// MissedRules and MissedAlternatives list what was not exercised.
MissedRules []string `json:"missedRules,omitempty"`
MissedAlternatives []Alternative `json:"missedAlternatives,omitempty"`
// Unreachable lists the rules that generation cannot exercise from the start rule: rules that are
// not called, or called only inside negative lookaheads (!keyword), the skip of #recover, or
// expressions that can never match or blocked Operators Rule operator parts. Impossible lists the rules
// that the start rule calls (outside negative lookaheads and #recover) but that can never match,
// such as rules that end in _|_ to report an error. Neither is counted in Rules.
Unreachable []string `json:"unreachable,omitempty"`
Impossible []string `json:"impossible,omitempty"`
}
Coverage reports which parts of the grammar the accepted inputs exercised.
Coverage is measured on the way the generator derived each accepted input. The parser takes the same way unless the generator could not evaluate a lookahead, a predicate or an earlier alternative of a choice (see the package documentation); counts are therefore close to, but not guaranteed to be, the parser's own coverage.
func (Coverage) Report source
func (c Coverage) Report() string
Report describes the coverage in detail, one item per line.
func (Coverage) String source
func (c Coverage) String() string
String summarizes the coverage on one line.
type F source
type F interface {
Helper()
Add(args ...any)
Fatalf(format string, args ...any)
}
F is the part of *testing.F that Seed uses. (Taking an interface keeps the testing package out of programs that import sample without seeding fuzz tests.)
type Generator source
type Generator struct {
// contains filtered or unexported fields
}
Generator generates inputs for a parser. It is not safe for concurrent use.
func New source
func New(p *pego.Parser, opts ...Option) (*Generator, error)
New returns a generator of inputs for p, starting at p's start rule. The parser must be compiled from the grammar source: a parser loaded from a compiled grammar has no grammar AST and cannot be sampled.
func (*Generator) Coverage source
func (g *Generator) Coverage() Coverage
Coverage returns the coverage of the inputs accepted so far.
func (*Generator) Cuts source
func (g *Generator) Cuts() []RuleCuts
Cuts returns the cuts of each rule so far, for the rules with any, the rules with the most cuts first (ties in rule name order).
func (*Generator) Generate source
func (g *Generator) Generate(n int) ([]string, error)
Generate returns up to n distinct inputs that the parser accepts. It returns fewer if it cannot find more (a grammar may accept only a few inputs; it gives up after as many inputs in a row that it already returned as the attempts per input), and ErrNoInput if it finds none.
func (*Generator) GenerateInvalid source
func (g *Generator) GenerateInvalid(n int) ([]Invalid, error)
GenerateInvalid returns up to n near-miss invalid inputs with distinct Input. It returns fewer if it cannot find more, and the error of Invalid (ErrNoInput or ErrNoInvalid) if it finds none.
func (*Generator) Invalid source
func (g *Generator) Invalid() (Invalid, error)
Invalid returns a near-miss invalid input. It returns ErrNoInput if it finds no valid input to mutate, and ErrNoInvalid if no mutation of the valid inputs it found is rejected (a grammar such as .* accepts every input).
func (*Generator) Next source
func (g *Generator) Next() (string, error)
Next returns an input that the parser accepts. Inputs may repeat. It returns ErrNoInput if no candidate was accepted within the attempts.
func (*Generator) Stats source
func (g *Generator) Stats() Stats
Stats returns the counts of the work done so far.
type Invalid source
type Invalid struct {
Input string // the mutated input
Base string // the valid input it was made from
Mutation string // the mutation, such as `delete "]" at 12` (positions are byte offsets in Base)
Err error // the error the parser returned for Input
}
Invalid is a near-miss invalid input: an input that the parser accepts, changed by one mutation so that the parser rejects it. It exercises the error paths of the parser and of the code around it.
func WithAttempts source
func WithAttempts(n int) Option
WithAttempts sets how many candidates the generator tries for one input before giving up (default DefaultAttempts).
func WithBudget source
func WithBudget(steps int) Option
WithBudget sets the number of steps one attempt may take before it gives up (default DefaultBudget). A step is a parsing expression generated or a OperatorsRule scope inspected; checks have a budget of their own in proportion. Past WithMaxDepth and WithMaxLen, the generator only prefers the shortest way to finish. The budget and the nesting/iteration limits (see WithMaxDepth and WithMaxRepeat) bound an attempt. Inputs that need many steps, such as an expression repeated 25,000 times, need a larger budget; the budget also bounds the search stack, which grows with the steps of an attempt.
func WithCoverage source
func WithCoverage() Option
WithCoverage biases the generator toward rules, alternatives of ordered choices, and Operators Rule operands and operators that no accepted input has exercised yet.
func WithMaxDepth source
func WithMaxDepth(d int) Option
WithMaxDepth bounds recursion: once rule calls are nested d deep inside calls of the same rules, the generator prefers the shortest way to finish (default DefaultMaxDepth). Calls of rules that are not already active do not count. The bound is soft: when the input requires deeper nesting (a predicate that counts it, for example), the search goes deeper, but never beyond d + 8: raise d for inputs that must nest deeper.
func WithMaxLen source
func WithMaxLen(n int) Option
WithMaxLen sets a soft limit on the length of an input in bytes (default DefaultMaxLen): once the input is that long, the generator prefers the alternatives with the shortest text to finish it.
func WithMaxRepeat source
func WithMaxRepeat(n int) Option
WithMaxRepeat sets how many iterations an unbounded repetition aims for beyond its minimum, at most (default DefaultMaxRepeat). It also bounds the operators of an Operators Rule expression chain. The search may take more iterations when the input requires them (a predicate after the repetition wants a longer match), but never more than 8 × n + 4 beyond the minimum (28 by default): raise n for inputs with longer repetitions.
func WithSeed source
func WithSeed(seed uint64) Option
WithSeed sets the seed of the random decisions (default 0). The same parser, options and seed give the same inputs with the same version of this package (see the package documentation).
type RuleCuts source
type RuleCuts struct {
Rule string
Checks int
Predicates int
}
RuleCuts counts the cuts of the checks and predicates in one rule's body (see Stats.CheckCuts and Generator.Cuts). A check belongs to the rule whose body holds the lookahead, choice, optional, repetition or $$ that installed it.
type Stats source
type Stats struct {
Candidates int // candidates passed to the parser
Rejected int // candidates that the parser rejected
Failed int // searches that ended without a candidate
// Exhausted counts the failed searches that used up their budget (WithBudget). The others gave up
// on every way to finish the input that they tried: the grammar may accept nothing within the
// bounds, or the checks below may rule out the ways the search takes.
Exhausted int
// Duplicates counts the inputs that Generate and GenerateInvalid found again and skipped.
Duplicates int
// CheckCuts counts the ways that the search abandoned because a lookahead, the order of a choice
// or $$ showed that the parser would not match them; PredicateCuts those abandoned because a
// predicate would certainly fail. Many cuts in a failing search point at the checks that block it.
CheckCuts int
PredicateCuts int
}
Stats counts the work of a Generator.