ZeroParse

Cancellation and Budget Limits

A parse of untrusted or very large input should be able to stop: when a caller gives up, when a deadline passes, or when the cost exceeds a budget. This guide shows the per-call options that do that and what a stopped parse returns. The nesting limit, which bounds rule calls, is described with the backends.

The examples use a parser p compiled as in Running Parsers. The repository's security policy, SECURITY.md at the root of the repository, lists what a parse bounds and what the caller must bound.

Cancelling a Parse

Parse accepts per-invocation WithContext and WithLimits options. A zero limit selects the default described below; zp.Unlimited disables the limit. The counter resets for the next call. Generated Go parsers offer them only when generated with -budget-controls; see the generated Go API. Generated TypeScript parsers offer the signal, budget and maxInput options only with -budget-controls too; see TypeScript.

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
node, err := p.Parse(input,
    zp.WithContext(ctx),
    zp.WithLimits(zp.Limits{Budget: 5_000_000}),
)
switch {
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
    // The invocation was cancelled.
case errors.Is(err, zp.ErrLimit):
    // A budget limit was reached; use errors.As for *zp.LimitError details.
case err != nil:
    // Syntax or another parse error.
case node != nil:
    // The parse completed.
}

Budget counts inspected input elements and backend execution steps in one counter, one unit each; it is neither a time nor a memory limit, and backtracking never refunds it. Scanning input, such as a repeated character class, charges it in proportion to the elements examined, so one limit bounds the cost of large input, rule calls and values that grow. The memory a parse allocates, long values included, is at most proportional to the budget it spends plus the input, so a smaller budget also bounds memory; there is no separate memory limit. An interruption is fatal to the parse and #recover cannot consume it, so a batch parse returns no partial result. Cancellation is cooperative: a parser cannot preempt a blocked reader or a callback of yours. The exact rules are in Cancellation and Budget Limits; the Streaming guide shows how to cancel caller-owned I/O.

The Default Budget

A parse with a zero Budget is not unbounded. It gets a budget of max(2^20, 1024 * n), where n is the length of the input in the position unit, so a parse of untrusted input stops without your choosing a limit. Input whose cost per element is higher than 1024 on average, such as a grammar that backtracks heavily, fails with a *zp.LimitError, whose Limit is the resolved budget. Pass a larger Budget when you expect that cost, or zp.Unlimited when the input is trusted:

node, err := p.Parse(input, zp.WithLimits(zp.Limits{Budget: zp.Unlimited}))

A parse with Unlimited and no context runs without any control, which is the fastest form. A stream does not know its length in advance: its default is max(2^20, 1024 * d), where d is the furthest position the parser has asked the stream for, so it does not depend on how your reader splits the input. A Document resolves the default from the text at each Parse. Generated Go and TypeScript parsers apply the default when they are generated with -budget-controls; a parser generated without it spends no budget and accepts only a zero limit and Unlimited.

Bounding the Input Length

Limits.MaxInput bounds the length of the input in the position unit. Its default is zp.Unlimited: the default budget already grows with the input, so a long input costs in proportion to its length. Set it when you want to refuse input above a size before spending any work on it:

node, err := p.Parse(input, zp.WithLimits(zp.Limits{MaxInput: 1 << 20}))
var le *zp.LimitError
if errors.As(err, &le) && le.Kind == zp.LimitInput {
    // le.Limit is MaxInput and le.Requested is the length of the input.
}

An input of exactly MaxInput units is accepted; a longer one fails at once with LimitInput, before the parser runs and even before a canceled context is noticed. A Document checks the text it holds at each Parse, so an edit that makes the text too long fails the next Parse and a later edit that shortens it makes the Parse succeed. A stream fails when the parser asks for a unit beyond the limit and the stream has one, however your reader chunks the input, and the error reports the number of units the parser asked for. A finite MaxInput needs no budget: with Budget: zp.Unlimited and no context the parse still runs without a control.

Limits on the Command Line

The zp commands that run a parse (parse, test, trace, profile, explain and edit-fuzz) take -budget and -max-input, which set Limits.Budget and Limits.MaxInput. Each takes a number, 0 for the default, or unlimited:

zp parse -g grammar.zp -budget unlimited -max-input 1048576 -i "$input"

A parse that reaches a limit prints the *zp.LimitError message and exits with status 1. zp test reports it as the runtime error of the case. A saved edit-fuzz sequence does not record the limits, so edit-fuzz -replay takes -budget and -max-input again. A Document reuses earlier work and spends less than a fresh parse, so edit-fuzz compares nothing at a check where either parse exhausts its budget.