Getting Started
Stringent is a type-safe expression parser and evaluator for TypeScript. One grammar definition drives two engines:
- a type-level parser — expressions in string literals are validated and fully typed at compile time, and
- a runtime parser — dynamic strings get structured errors and evaluation.
Installation
Section titled “Installation”npm install stringentpnpm add stringentyarn add stringentStringent is ESM-only, ships its own type definitions, and depends on arktype — constraints, result types, and schemas are all arktype definitions.
Quickstart
Section titled “Quickstart”-
Define your grammar. Each
defineNodecall declares one grammar rule: a pattern of elements, a precedence, and (usually) a result type and an evaluation function.import {defineNode,function defineNode<const TName extends string, const TPattern extends readonly PatternSchema[], const TPrecedence extends Precedence, const TResultType extends ResultSpec | undefined = undefined>(config: {readonly name: TName;readonly precedence: TPrecedence;readonly pattern: (p: PatternBuilder) => BuiltPattern<TPattern, TResultType>;}): NodeSchema<TName, TPattern, TPrecedence, TResultType>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.overlapping,const overlapping: <const TBinding extends string>(binding: TBinding) => OverlapsRef<TBinding>Create a symmetric (overlap) constraint referencing an earlier binding
createParser } from "stringent";function createParser<const TNodes extends readonly NodeSchema[], const TScope extends ScopeAliases = {}>(nodes: TNodes, options?: {readonly scope?: TScope;}): Parser<ComputeGrammar<TNodes>, TNodes, InferScope<TScope>>Create a type-safe parser from node schemas.
// Leaf nodes live at the HIGHEST precedence level.// Single-element passthrough patterns take no resultType.constnumberLit =const numberLit: NodeSchema<"num", readonly [NumberSchema], 4, undefined>defineNode({defineNode<"num", readonly [NumberSchema], 4, undefined>(config: {readonly name: "num";readonly precedence: 4;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NumberSchema], undefined>;}): NodeSchema<"num", readonly [NumberSchema], 4, undefined>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "num",name: "num"precedence: 4,precedence: 4pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NumberSchema], undefined>p) =>p: PatternBuilder<readonly [], {}>p.p: PatternBuilder<readonly [], {}>number(),PatternBuilder<readonly [], {}>.number(): NameableBuilder<readonly [], {}, NumberSchema>Append a number literal element
});conststringLit =const stringLit: NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>defineNode({defineNode<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>(config: {readonly name: "str";readonly precedence: 4;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [StringSchema<readonly ["\"", "'"]>], undefined>;}): NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "str",name: "str"precedence: 4,precedence: 4pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [StringSchema<readonly ["\"", "'"]>], undefined>p) =>p: PatternBuilder<readonly [], {}>p.p: PatternBuilder<readonly [], {}>string(['"', "'"]),PatternBuilder<readonly [], {}>.string<readonly ["\"", "'"]>(quotes: readonly ["\"", "'"]): NameableBuilder<readonly [], {}, StringSchema<readonly ["\"", "'"]>>Append a string literal element, e.g.
p.string(['"', "'"])— escapes are processed});// Keyword literals must come before path() in the level, or `true`// would parse as an identifier.// ONE node for both booleans: const ALTERNATION binds the matched textconstbooleanLit =const booleanLit: NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">defineNode({defineNode<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">(config: {readonly name: "bool";readonly precedence: 4;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">;}): NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "bool",name: "bool"precedence: 4,precedence: 4pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.constVal("true", "false").PatternBuilder<readonly [], {}>.constVal<readonly ["true", "false"]>(values_0: "true", values_1: "false"): NameableBuilder<readonly [], {}, ConstSchema<readonly ["true", "false"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:as("word")NameableBuilder<readonly [], {}, ConstSchema<readonly ["true", "false"]>>.as<"word">(name: "word"): PatternBuilder<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], {}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.result("boolean")PatternBuilder<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], {}>.result<"boolean">(def: "boolean"): ResultedBuilder<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval(({ResultedBuilder<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">]>>) => boolean): EvaledBuilder<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
word }) =>word: () => "true" | "false"word() === "true"),word: () => "true" | "false"});constvariable =const variable: NodeSchema<"var", readonly [PathSchema], 4, undefined>defineNode({defineNode<"var", readonly [PathSchema], 4, undefined>(config: {readonly name: "var";readonly precedence: 4;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [PathSchema], undefined>;}): NodeSchema<"var", readonly [PathSchema], 4, undefined>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "var",name: "var"precedence: 4,precedence: 4pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [PathSchema], undefined>p) =>p: PatternBuilder<readonly [], {}>p.p: PatternBuilder<readonly [], {}>path(),PatternBuilder<readonly [], {}>.path(): NameableBuilder<readonly [], {}, PathSchema>Append a member-access path element: matches
ident(.ident)*(e.g.values.password), type resolved by walking the schema. Whitespace around the dots is not allowed.});// path() matches identifiers and dotted paths: x, values.password// Polymorphic parens: the result type is whatever is insideconstparens =const parens: NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">defineNode({defineNode<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">(config: {readonly name: "parens";readonly precedence: 4;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">;}): NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<...>, ConstSchema<...>], 4, "inner">Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "parens",name: "parens"precedence: 4,precedence: 4pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.constVal("(")PatternBuilder<readonly [], {}>.constVal<readonly ["("]>(values_0: "("): NameableBuilder<readonly [], {}, ConstSchema<readonly ["("]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.expr().PatternBuilder<readonly [ConstSchema<readonly ["("]>], {}>.expr(): NameableBuilder<readonly [ConstSchema<readonly ["("]>], {}, ExprSchema<undefined, "expr">> (+2 overloads)Append a FULL expression element.
Resets to the full grammar (precedence 0). Only for DELIMITED contexts — every expr() must be followed by at least one constVal in the same pattern (parentheses' ")", ternary's ":"), otherwise it would swallow looser operators and break precedence.
as("inner")NameableBuilder<readonly [ConstSchema<readonly ["("]>], {}, ExprSchema<undefined, "expr">>.as<"inner">(name: "inner"): PatternBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">], {inner: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal(")")PatternBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">], { inner: unknown; }>.constVal<readonly [")"]>(values_0: ")"): NameableBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">], {inner: unknown;}, ConstSchema<readonly [")"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.result("inner")PatternBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], { inner: unknown; }>.result<"inner">(def: "inner"): ResultedBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval(({ResultedBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>]>>) => unknown): EvaledBuilder<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
inner }) =>inner: () => unknowninner()),inner: () => unknown});// Overlap-typed equality: 1 == 'a' is a parse-time type error,// but operand order never matters (x == 1 and 1 == x both parse)consteq =const eq: NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">defineNode({defineNode<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">(config: {readonly name: "eq";readonly precedence: 1;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">;}): NodeSchema<...>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "eq",name: "eq"precedence: 1,precedence: 1pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.operand().PatternBuilder<readonly [], {}>.operand(): NameableBuilder<readonly [], {}, ExprSchema<undefined, "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("left")NameableBuilder<readonly [], {}, ExprSchema<undefined, "operand">>.as<"left">(name: "left"): PatternBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">], {left: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal("==")PatternBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">], { left: unknown; }>.constVal<readonly ["=="]>(values_0: "=="): NameableBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">], {left: unknown;}, ConstSchema<readonly ["=="]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.rest(PatternBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>], { left: unknown; }>.rest<"left">(constraint: OverlapsRef<"left">): NameableBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>], {left: unknown;}, ExprSchema<OverlapsRef<"left">, "rest">> (+2 overloads)Append a CURRENT-LEVEL expression element.
Parses at the same level. A pattern whose final operand is rest() makes its level RIGHT-associative (
a ^ b ^ c→a ^ (b ^ c)).Constraint forms: rest("number"), rest("left"), rest("left | null"), rest(overlapping("left")), rest().
overlapping("left")).overlapping<"left">(binding: "left"): OverlapsRef<"left">Create a symmetric (overlap) constraint referencing an earlier binding
as("right")NameableBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>], { left: unknown; }, ExprSchema<OverlapsRef<"left">, "rest">>.as<"right">(name: "right"): PatternBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], {left: unknown;} & {right: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.result("boolean")PatternBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], { left: unknown; } & { ...; }>.result<"boolean">(def: "boolean"): ResultedBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval(({ResultedBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">]>>) => boolean): EvaledBuilder<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
left,left: () => unknownright }) =>right: () => unknownleft() ===left: () => unknownright()),right: () => unknown});// ONE overloaded add: number+number → number, string+string → string.// "number | string" is an arktype def; "left" is a binding reference.// The operand() tail makes the level LEFT-associative: 1+2+3 = (1+2)+3.constadd =const add: NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">defineNode({defineNode<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">(config: {readonly name: "add";readonly precedence: 2;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">;}): NodeSchema<...>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "add",name: "add"precedence: 2,precedence: 2pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.operand("number | string").PatternBuilder<readonly [], {}>.operand<"number | string">(constraint: "number | string"): NameableBuilder<readonly [], {}, ExprSchema<"number | string", "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("left")NameableBuilder<readonly [], {}, ExprSchema<"number | string", "operand">>.as<"left">(name: "left"): PatternBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">], {left: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal("+")PatternBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">], { left: unknown; }>.constVal<readonly ["+"]>(values_0: "+"): NameableBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">], {left: unknown;}, ConstSchema<readonly ["+"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.operand("left").PatternBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>], { left: unknown; }>.operand<"left">(constraint: "left"): NameableBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>], {left: unknown;}, ExprSchema<"left", "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("right")NameableBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>], { left: unknown; }, ExprSchema<"left", "operand">>.as<"right">(name: "right"): PatternBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], {left: unknown;} & {right: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.result("left")PatternBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], { left: unknown; } & { ...; }>.result<"left">(def: "left"): ResultedBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval((ResultedBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>) => string | number): EvaledBuilder<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
b) => {b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>constl =const l: string | numberb.b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>left(),left: () => string | numberr =const r: string | numberb.b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>right();right: () => string | numberreturn typeofl === "string" ? `${const l: string | numberl}${const l: stringString(var String: StringConstructor(value?: any) => stringAllows manipulation and formatting of text strings and determination and location of substrings within strings.
r)}` :const r: string | numberNumber(var Number: NumberConstructor(value?: any) => numberAn object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
l) +const l: numberNumber(var Number: NumberConstructor(value?: any) => numberAn object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
r);const r: string | number}),});constmul =const mul: NodeSchema<"mul", readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], 3, "number">defineNode({defineNode<"mul", readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], 3, "number">(config: {readonly name: "mul";readonly precedence: 3;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">;}): NodeSchema<...>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "mul",name: "mul"precedence: 3,precedence: 3pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.operand("number").PatternBuilder<readonly [], {}>.operand<"number">(constraint: "number"): NameableBuilder<readonly [], {}, ExprSchema<"number", "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("left")NameableBuilder<readonly [], {}, ExprSchema<"number", "operand">>.as<"left">(name: "left"): PatternBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">], {left: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal("*")PatternBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">], { left: unknown; }>.constVal<readonly ["*"]>(values_0: "*"): NameableBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">], {left: unknown;}, ConstSchema<readonly ["*"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.operand("number").PatternBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>], { left: unknown; }>.operand<"number">(constraint: "number"): NameableBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>], {left: unknown;}, ExprSchema<"number", "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("right")NameableBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>], { left: unknown; }, ExprSchema<"number", "operand">>.as<"right">(name: "right"): PatternBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], {left: unknown;} & {right: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.result("number")PatternBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], { left: unknown; } & { ...; }>.result<"number">(def: "number"): ResultedBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval(({ResultedBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">]>>) => number): EvaledBuilder<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
left,left: () => numberright }) =>right: () => numberleft() *left: () => numberright()),right: () => number});// A short-circuiting polymorphic ternary. The rest() tail makes the// level RIGHT-associative.constternary =const ternary: NodeSchema<"ternary", readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], 0, "then">defineNode({defineNode<"ternary", readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], 0, "then">(config: {readonly name: "ternary";readonly precedence: 0;readonly pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<...>, NamedSchema<...>, ConstSchema<...>, NamedSchema<...>], "then">;}): NodeSchema<...>Define a node type for the grammar.
The pattern is authored through a fluent builder (see PatternBuilder): elements chain left to right,
.as(name)names the element just appended,.result(def)declares the node's result type (a binding name, or an arktype def — validated against the chain's bindings), and.eval(fn)attaches evaluation (bindings arrive as memoized thunks; the return type is checked against the declared result). Every def is validated by arktype WHERE IT IS WRITTEN — a typo like operand("nmbr") is a compile error at that call.name: "ternary",name: "ternary"precedence: 0,precedence: 0pattern: (pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], "then">p) =>p: PatternBuilder<readonly [], {}>pp: PatternBuilder<readonly [], {}>.operand("boolean").PatternBuilder<readonly [], {}>.operand<"boolean">(constraint: "boolean"): NameableBuilder<readonly [], {}, ExprSchema<"boolean", "operand">> (+2 overloads)Append a TIGHTER-LEVEL expression element.
Parses at the next-higher grammar level, avoiding left-recursion. A pattern whose final operand is operand() makes its level LEFT-associative (the engine folds repetitions:
a-b-c→(a-b)-c).Constraint forms: operand("number"), operand("string | number"), operand("left") (a binding reference — not valid at position 0, where no earlier operand exists), operand().
as("cond")NameableBuilder<readonly [], {}, ExprSchema<"boolean", "operand">>.as<"cond">(name: "cond"): PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">], {cond: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal("?")PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">], { cond: unknown; }>.constVal<readonly ["?"]>(values_0: "?"): NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">], {cond: unknown;}, ConstSchema<readonly ["?"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.expr().PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>], { cond: unknown; }>.expr(): NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>], {cond: unknown;}, ExprSchema<undefined, "expr">> (+2 overloads)Append a FULL expression element.
Resets to the full grammar (precedence 0). Only for DELIMITED contexts — every expr() must be followed by at least one constVal in the same pattern (parentheses' ")", ternary's ":"), otherwise it would swallow looser operators and break precedence.
as("then")NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>], { cond: unknown; }, ExprSchema<undefined, "expr">>.as<"then">(name: "then"): PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">], {cond: unknown;} & {then: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.constVal(":")PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">], { cond: unknown; } & { ...; }>.constVal<readonly [":"]>(values_0: ":"): NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">], {cond: unknown;} & {then: unknown;}, ConstSchema<readonly [":"]>>Append a constant element — one or more values as an ORDERED alternation (first match wins). IDENTIFIER-LIKE values (
constVal("null"),constVal("and")) match only as a WHOLE identifier —nullableorandynever match (word-boundary rule; pinned in design-claims); other values ("+","==") match as raw text, per member. Keyword literals are ordinary const-pattern nodes; name the element to receive the MATCHED text in eval:.rest("then").PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<...>], { cond: unknown; } & { ...; }>.rest<"then">(constraint: "then"): NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>], {cond: unknown;} & {then: unknown;}, ExprSchema<"then", "rest">> (+2 overloads)Append a CURRENT-LEVEL expression element.
Parses at the same level. A pattern whose final operand is rest() makes its level RIGHT-associative (
a ^ b ^ c→a ^ (b ^ c)).Constraint forms: rest("number"), rest("left"), rest("left | null"), rest(overlapping("left")), rest().
as("else")NameableBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<...>], { cond: unknown; } & { ...; }, ExprSchema<...>>.as<"else">(name: "else"): PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], {cond: unknown;} & {then: unknown;} & {else: unknown;}>Name the element just appended: it becomes a binding — a field on the AST node, a typed thunk in eval's parameter, and (for non-const elements) a scope alias for later constraints and the result def.
.result("then")PatternBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<...>, NamedSchema<...>], { cond: unknown; } & ... 1 more ... & { ...; }>.result<"then">(def: "then"): ResultedBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], "then">Declare the node's result type (trailing — after the last element):
- a binding name ("then") — derived per-parse from that operand
- an arktype def ("boolean", { min: "number", max: "number" }) — the node mints a type; defs may EMBED binding references ("then | null"), resolved per-parse
Validated by arktype with the chain's bindings in scope. Required for every node that CONSTRUCTS a result; only a single-element passthrough pattern omits it. (The "~resolved" key is reserved — enforced at construction.)
.eval(({ResultedBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<...>, NamedSchema<...>], "then">.eval(fn: (bindings: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">]>>) => unknown): EvaledBuilder<readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<...>, ConstSchema<...>, NamedSchema<...>], "then">Attach the node's evaluation function. Bindings arrive as MEMOIZED THUNKS (evaluation is uniformly lazy — call a binding to evaluate it; untaken branches never run). The return type is checked against the declared result.
cond,cond: () => booleanthen,then: () => unknownelse:else: () => unknownalt }) => (alt: () => unknowncond() ?cond: () => booleanthen() :then: () => unknownalt())),alt: () => unknown});constparser =const parser: Parser<[[NodeSchema<"ternary", readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], 0, "then">], [NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [...], [...], [...]], readonly [...], {}>createParser(createParser<readonly [NodeSchema<"num", readonly [NumberSchema], 4, undefined>, NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>, NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">, NodeSchema<"var", readonly [PathSchema], 4, undefined>, NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">, NodeSchema<...>, NodeSchema<...>, NodeSchema<...>, NodeSchema<...>], {}>(nodes: readonly [...], options?: {...;} | undefined): Parser<...>Create a type-safe parser from node schemas.
[numberLit,const numberLit: NodeSchema<"num", readonly [NumberSchema], 4, undefined>stringLit,const stringLit: NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>booleanLit,const booleanLit: NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">variable,const variable: NodeSchema<"var", readonly [PathSchema], 4, undefined>parens,const parens: NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">ternary,const ternary: NodeSchema<"ternary", readonly [NamedSchema<ExprSchema<"boolean", "operand">, "cond">, ConstSchema<readonly ["?"]>, NamedSchema<ExprSchema<undefined, "expr">, "then">, ConstSchema<readonly [":"]>, NamedSchema<ExprSchema<"then", "rest">, "else">], 0, "then">eq,const eq: NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">add,const add: NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">mul] asconst mul: NodeSchema<"mul", readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], 3, "number">consttype const = readonly [NodeSchema<"num", readonly [NumberSchema], 4, undefined>, NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>, NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">, NodeSchema<"var", readonly [PathSchema], 4, undefined>, NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">, NodeSchema<...>, NodeSchema<...>, NodeSchema<...>, NodeSchema<...>]); -
Parse string literals (compile-time checked).
parse()only accepts literals that fully parse against the grammar — an invalid expression is a compile-time error, and the AST type is fully inferred. Hoverastbelow to see it:const [ast] =parser.const parser: Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">], [...], [...]], readonly [...], {}>parse("1+2*3", {});Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [...], [...], [...]], readonly [...], {}>.parse<"1+2*3", {}>(input: "1+2*3", schema: {}): [{readonly node: "add";readonly outputSchema: "number";} & {left: NumberNode<"1">;right: {readonly node: "mul";readonly outputSchema: "number";} & {left: NumberNode<"2">;right: NumberNode<"3">;};}, ""]Parse a string literal, validated at compile time.
The input must be a literal that FULLY parses against the grammar — anything else is a compile-time error. For runtime-provided strings, use safeParse. Throws StringentParseError if the compile-time check was bypassed and the input is invalid.
const ast: {readonly node: "add";readonly outputSchema: "number";} & {left: NumberNode<"1">;right: {readonly node: "mul";readonly outputSchema: "number";} & {left: NumberNode<"2">;right: NumberNode<"3">;};}Invalid expressions don’t compile:
parser.parse("1+", {}); // ✗ compile errorparser.parse("1+'a'", {}); // ✗ compile error: 'a' ⊄ number (type of 'left')parser.parse("1+x", { x: "number" }); // ✓parser.safeParse("1+x", { x: "numbr" }); // ✗ compile error AT THE SCHEMA LEAFSchema leaves are arktype definitions, validated by
type.validate— a typo’d{ x: "numbr" }errors right at the leaf onsafeParse(and every entry point rejects bad schemas at runtime). Grammar constraints are validated even earlier: a typo’doperand("numbr")throws atcreateParsertime. -
Parse dynamic strings (runtime checked). Runtime-provided input goes through
safeParse(), which requires full consumption and returns structured errors instead of throwing:const result = parser.safeParse(userInput, { x: "number" });if (result.success) {console.log(result.ast);} else {console.error(result.error.message);// e.g. Expected a number expression at position 2, got unknown ('zz' is not in the schema)// e.g. Expected a number (type of 'left') expression at position 4, got string// e.g. Unexpected input at position 6: found "junk!!" (expected "*", "+" or "==")result.error.position; // 0-based offsetresult.error.expected; // tokens that would have been valid there} -
Evaluate.
evaluate()parses and evaluates in one step; the result type is inferred from the expression — including through polymorphic nodes:const seven =parser.const parser: Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">], [...], [...]], readonly [...], {}>evaluate("1+2*3", {}, {});Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [...], [...], [...]], readonly [...], {}>.evaluate<"1+2*3", {}>(input: "1+2*3", schema: {}, values: object): numberParse a string literal and evaluate it against runtime values.
Node eval() functions (from defineNode) compute the result. The values object is validated against the schema before evaluation.
const seven: numberconst ab =parser.const parser: Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">], [...], [...]], readonly [...], {}>evaluate("'a'+'b'", {}, {});Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [...], [...], [...]], readonly [...], {}>.evaluate<"'a'+'b'", {}>(input: "'a'+'b'", schema: {}, values: object): stringParse a string literal and evaluate it against runtime values.
Node eval() functions (from defineNode) compute the result. The values object is validated against the schema before evaluation.
const ab: stringconst answer =parser.const parser: Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">], [...], [...]], readonly [...], {}>evaluate("x*2", {Parser<[[NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">], [...], [...], [...]], readonly [...], {}>.evaluate<"x*2", {readonly x: "number";}>(input: "x*2", schema: validateObjectLiteral<{readonly x: "number";}, {}, bindThis<{readonly x: "number";}>>, values: NoInfer<{x: number;}>): numberParse a string literal and evaluate it against runtime values.
Node eval() functions (from defineNode) compute the result. The values object is validated against the schema before evaluation.
x: "number" }, {x: "number"x: 21 });x: numberconst answer: numberFor dynamic input, combine
safeParsewithevaluateAst:const parsed = parser.safeParse(userInput, schema);if (parsed.success) {const value = parser.evaluateAst(parsed.ast, values);}
Where to next?
Section titled “Where to next?”- Defining a grammar — pattern elements, precedence, associativity by tail shape, and grammar validation.
- Schemas & types — arktype definitions, binding references, and polymorphic nodes.
- Parsing & evaluation — the
entry points,
compile(), and the lazy evaluation model. - Error handling — structured errors and what throws when.
- Playground — try the runtime engine live in your browser.