Parsing & Evaluation
A parser built with createParser exposes five entry points. Which one you
reach for depends on where the expression comes from:
| Entry point | Input | Checked | On failure |
|---|---|---|---|
parse(literal, schema) |
string literal | compile time | compile error |
safeParse(input, schema) |
dynamic string | runtime | returns { success: false, error } |
evaluate(literal, schema, values) |
string literal | compile time | compile error |
evaluateAst(ast, values) |
a parsed AST | runtime | throws EvaluationError |
compile(input, schema, options?) |
literal or dynamic | both | throws StringentParseError |
Literal inputs: parse and evaluate
Section titled “Literal inputs: parse and evaluate”parse() only accepts literals that fully parse against the grammar, and the
AST type is exactly inferred. evaluate() parses and evaluates in one step;
the result type is inferred from the expression — including through
polymorphic nodes:
parser.evaluate("1+2*3", {}, {}); // 7, typed numberparser.evaluate("'a'+'b'", {}, {}); // "ab", typed stringparser.evaluate("1==2 ? 'yes' : 'no'", {}, {}); // "no", typed stringparser.evaluate("x*2", { x: "number" }, { x: 21 }); // 42Dynamic strings are rejected by parse() at compile time — use
safeParse() for anything that isn’t a literal.
Before evaluating, evaluate() validates the values object against the full
schema — refinements included, so { x: "number > 0" } with { x: -5 }
fails with values do not match the schema: x must be positive (was -5).
Dynamic inputs: safeParse + evaluateAst
Section titled “Dynamic inputs: safeParse + evaluateAst”safeParse() requires full consumption of the input and returns structured
errors instead of throwing:
const parsed = parser.safeParse(userInput, schema);if (parsed.success) { const value = parser.evaluateAst(parsed.ast, values);} else { console.error(parsed.error.message, parsed.error.position);}See Error handling for the error shape and ranking rules.
Rules as arktype Types: compile
Section titled “Rules as arktype Types: compile”parser.compile(input, schema, { path?, message? }) compiles a rule into a
real arktype Type — and arktype Types are Standard Schemas, so a stringent
rule drops directly into react-hook-form, tRPC, hono, or oRPC:
const rule = parser.compile( "values.password == values.confirmPassword", { values: { password: "string", confirmPassword: "string" } }, { path: ["values", "confirmPassword"], message: "passwords to match" });rule({ values: { password: "a", confirmPassword: "a" } }); // → the values objectrule({ values: { password: "a", confirmPassword: "b" } }); // → ArkErrors// "values.confirmPassword must be passwords to match"- A rule whose output type is boolean becomes a predicate Type: it
validates the values object against the (refined) schema, evaluates the
rule, and rejects with an
ArkErrorsentry atoptions.pathwhen the rule is false. Values in, values out — exactly what a form resolver wants. - Any other rule becomes a morph Type: values in, evaluated result out.
rule.inis the values contract;rule.in.toJsonSchema()exports it (for predicate rules pass{ fallback: { predicate: (ctx) => ctx.base } }— the predicate node itself is not JSON-Schema-representable).
Unlike parse/evaluate, compile accepts dynamic strings (rules live in
config); invalid input throws StringentParseError. Literal inputs
additionally get precise compile-time typing.
Evaluation model
Section titled “Evaluation model”evaluate/evaluateAst walk the AST post-order: literals yield their values,
identifiers and paths look up the values object, and the node’s
eval(bindings) runs with each binding delivered as a memoized thunk.
The parsed output type types the result, so evaluation is typed end-to-end
for literal inputs.
Evaluation is uniformly lazy: untaken ternary branches (and the right side
of a short-circuiting &&) are never evaluated, and memoization means each
child evaluates at most once.
All identifier and path lookups — in the evaluator and in parse-time schema
resolution — use own-property checks (Object.hasOwn). constructor,
__proto__, x.constructor and the like resolve to “not defined”, never to
prototype internals.
Limits & rules
Section titled “Limits & rules”- Precedence must be a non-negative safe integer; the highest level present is the leaf level.
- Whitespace is skipped between tokens, but not allowed inside paths
around
.. - String escapes:
\xHH/\uHHHHdecode at runtime only — literal-mode parsing (parse/evaluate) rejects them; usesafeParse. - Evaluation is synchronous (arktype morphs cannot be async); async operators must be promise-valued outputs handled by the caller.
- Type-level input length: left-associative chains use a tail-recursive
fold and comfortably handle 30+ terms. Right-associative chains and
expr()nesting (parens, ternary branches) pay instantiation depth per precedence level — on a 6-level grammar that’s roughly 8 right-associative terms and 3 nesting levels before TypeScript’s recursion limit (TS2589); fewer precedence levels stretch these limits. Runtime parsing (safeParse) has no such limit.