Skip to content

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.
Terminal window
npm install stringent

Stringent is ESM-only, ships its own type definitions, and depends on arktype — constraints, result types, and schemas are all arktype definitions.

  1. Define your grammar. Each defineNode call declares one grammar rule: a pattern of elements, a precedence, and (usually) a result type and an evaluation function.

    import {
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ,
    const overlapping: <const TBinding extends string>(binding: TBinding) => OverlapsRef<TBinding>

    Create a symmetric (overlap) constraint referencing an earlier binding

    overlapping
    ,
    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.

    @paramnodes - Tuple of node schemas defining the grammar

    @paramoptions.scope - Extra type aliases available in constraints, resultTypes, and schemas (e.g. { Money: "number" })

    @example

    const parser = createParser([ternary, add, atoms] as const);
    const result = parser.safeParse(dynamic, { x: "number" });
    const sum = parser.evaluate("1+2", {}, {}); // 3

    createParser
    } from "stringent";
    // Leaf nodes live at the HIGHEST precedence level.
    // Single-element passthrough patterns take no resultType.
    const
    const numberLit: NodeSchema<"num", readonly [NumberSchema], 4, undefined>
    numberLit
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "num"
    name
    : "num",
    precedence: 4
    precedence
    : 4,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [NumberSchema], undefined>
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    PatternBuilder<readonly [], {}>.number(): NameableBuilder<readonly [], {}, NumberSchema>

    Append a number literal element

    number
    (),
    });
    const
    const stringLit: NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>
    stringLit
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "str"
    name
    : "str",
    precedence: 4
    precedence
    : 4,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [StringSchema<readonly ["\"", "'"]>], undefined>
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    PatternBuilder<readonly [], {}>.string<readonly ["\"", "'"]>(quotes: readonly ["\"", "'"]): NameableBuilder<readonly [], {}, StringSchema<readonly ["\"", "'"]>>

    Append a string literal element, e.g. p.string(['"', "'"]) — escapes are processed

    string
    (['"', "'"]),
    });
    // 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 text
    const
    const booleanLit: NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">
    booleanLit
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "bool"
    name
    : "bool",
    precedence: 4
    precedence
    : 4,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], "boolean">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("true", "false").
    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.

    as
    ("word")
    .
    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.)

    result
    ("boolean")
    .
    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.

    eval
    (({
    word: () => "true" | "false"
    word
    }) =>
    word: () => "true" | "false"
    word
    () === "true"),
    });
    const
    const variable: NodeSchema<"var", readonly [PathSchema], 4, undefined>
    variable
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "var"
    name
    : "var",
    precedence: 4
    precedence
    : 4,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [PathSchema], undefined>
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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
    (),
    });
    // path() matches identifiers and dotted paths: x, values.password
    // Polymorphic parens: the result type is whatever is inside
    const
    const parens: NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">
    parens
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "parens"
    name
    : "parens",
    precedence: 4
    precedence
    : 4,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], "inner">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("(")
    .
    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.

    expr
    ().
    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.

    as
    ("inner")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    (")")
    .
    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.)

    result
    ("inner")
    .
    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.

    eval
    (({
    inner: () => unknown
    inner
    }) =>
    inner: () => unknown
    inner
    ()),
    });
    // Overlap-typed equality: 1 == 'a' is a parse-time type error,
    // but operand order never matters (x == 1 and 1 == x both parse)
    const
    const eq: NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">
    eq
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "eq"
    name
    : "eq",
    precedence: 1
    precedence
    : 1,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], "boolean">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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().

    operand
    ().
    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.

    as
    ("left")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("==")
    .
    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 ^ ca ^ (b ^ c)).

    Constraint forms: rest("number"), rest("left"), rest("left | null"), rest(overlapping("left")), rest().

    rest
    (
    overlapping<"left">(binding: "left"): OverlapsRef<"left">

    Create a symmetric (overlap) constraint referencing an earlier binding

    overlapping
    ("left")).
    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.

    as
    ("right")
    .
    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.)

    result
    ("boolean")
    .
    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.

    eval
    (({
    left: () => unknown
    left
    ,
    right: () => unknown
    right
    }) =>
    left: () => unknown
    left
    () ===
    right: () => unknown
    right
    ()),
    });
    // 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.
    const
    const add: NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">
    add
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "add"
    name
    : "add",
    precedence: 2
    precedence
    : 2,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], "left">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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().

    operand
    ("number | string").
    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.

    as
    ("left")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("+")
    .
    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().

    operand
    ("left").
    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.

    as
    ("right")
    .
    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.)

    result
    ("left")
    .
    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.

    eval
    ((
    b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>
    b
    ) => {
    const
    const l: string | number
    l
    =
    b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>
    b
    .
    left: () => string | number
    left
    (),
    const r: string | number
    r
    =
    b: Thunked<InferEvaluatedBindings<readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">]>>
    b
    .
    right: () => string | number
    right
    ();
    return typeof
    const l: string | number
    l
    === "string" ? `${
    const l: string
    l
    }${
    var String: StringConstructor
    (value?: any) => string

    Allows manipulation and formatting of text strings and determination and location of substrings within strings.

    String
    (
    const r: string | number
    r
    )}` :
    var Number: NumberConstructor
    (value?: any) => number

    An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

    Number
    (
    const l: number
    l
    ) +
    var Number: NumberConstructor
    (value?: any) => number

    An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

    Number
    (
    const r: string | number
    r
    );
    }),
    });
    const
    const mul: NodeSchema<"mul", readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], 3, "number">
    mul
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "mul"
    name
    : "mul",
    precedence: 3
    precedence
    : 3,
    pattern: (p: PatternBuilder) => BuiltPattern<readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], "number">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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().

    operand
    ("number").
    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.

    as
    ("left")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("*")
    .
    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().

    operand
    ("number").
    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.

    as
    ("right")
    .
    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.)

    result
    ("number")
    .
    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.

    eval
    (({
    left: () => number
    left
    ,
    right: () => number
    right
    }) =>
    left: () => number
    left
    () *
    right: () => number
    right
    ()),
    });
    // A short-circuiting polymorphic ternary. The rest() tail makes the
    // level RIGHT-associative.
    const
    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">
    ternary
    =
    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.

    @example A polymorphic, short-circuiting ternary: const ternary = defineNode({ name: "ternary", precedence: 0, pattern: (p) => p .operand("boolean").as("cond") .constVal("?") .expr().as("then") .constVal(":") .rest("then").as("else") .result("then") .eval(({ cond, then, else: alt }) => (cond() ? then() : alt())), });

    defineNode
    ({
    name: "ternary"
    name
    : "ternary",
    precedence: 0
    precedence
    : 0,
    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">
    pattern
    : (
    p: PatternBuilder<readonly [], {}>
    p
    ) =>
    p: PatternBuilder<readonly [], {}>
    p
    .
    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().

    operand
    ("boolean").
    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.

    as
    ("cond")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    ("?")
    .
    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.

    expr
    ().
    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.

    as
    ("then")
    .
    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 — nullable or andy never 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:

    @example const booleanLit = defineNode({ name: "bool", precedence: 5, pattern: (p) => p.constVal("true", "false").as("word") .result("boolean") .eval(({ word }) => word() === "true"), });

    constVal
    (":")
    .
    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 ^ ca ^ (b ^ c)).

    Constraint forms: rest("number"), rest("left"), rest("left | null"), rest(overlapping("left")), rest().

    rest
    ("then").
    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.

    as
    ("else")
    .
    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.)

    result
    ("then")
    .
    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.

    eval
    (({
    cond: () => boolean
    cond
    ,
    then: () => unknown
    then
    ,
    else: () => unknown
    else
    :
    alt: () => unknown
    alt
    }) => (
    cond: () => boolean
    cond
    () ?
    then: () => unknown
    then
    () :
    alt: () => unknown
    alt
    ())),
    });
    const
    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 [...], {}>
    parser
    =
    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.

    @paramnodes - Tuple of node schemas defining the grammar

    @paramoptions.scope - Extra type aliases available in constraints, resultTypes, and schemas (e.g. { Money: "number" })

    @example

    const parser = createParser([ternary, add, atoms] as const);
    const result = parser.safeParse(dynamic, { x: "number" });
    const sum = parser.evaluate("1+2", {}, {}); // 3

    createParser
    (
    [
    const numberLit: NodeSchema<"num", readonly [NumberSchema], 4, undefined>
    numberLit
    ,
    const stringLit: NodeSchema<"str", readonly [StringSchema<readonly ["\"", "'"]>], 4, undefined>
    stringLit
    ,
    const booleanLit: NodeSchema<"bool", readonly [NamedSchema<ConstSchema<readonly ["true", "false"]>, "word">], 4, "boolean">
    booleanLit
    ,
    const variable: NodeSchema<"var", readonly [PathSchema], 4, undefined>
    variable
    ,
    const parens: NodeSchema<"parens", readonly [ConstSchema<readonly ["("]>, NamedSchema<ExprSchema<undefined, "expr">, "inner">, ConstSchema<readonly [")"]>], 4, "inner">
    parens
    ,
    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">
    ternary
    ,
    const eq: NodeSchema<"eq", readonly [NamedSchema<ExprSchema<undefined, "operand">, "left">, ConstSchema<readonly ["=="]>, NamedSchema<ExprSchema<OverlapsRef<"left">, "rest">, "right">], 1, "boolean">
    eq
    ,
    const add: NodeSchema<"add", readonly [NamedSchema<ExprSchema<"number | string", "operand">, "left">, ConstSchema<readonly ["+"]>, NamedSchema<ExprSchema<"left", "operand">, "right">], 2, "left">
    add
    ,
    const mul: NodeSchema<"mul", readonly [NamedSchema<ExprSchema<"number", "operand">, "left">, ConstSchema<readonly ["*"]>, NamedSchema<ExprSchema<"number", "operand">, "right">], 3, "number">
    mul
    ] as
    type 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<...>]
    const
    );
  2. 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. Hover ast below to see it:

    const [ast] =
    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 [...], {}>
    parser
    .
    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.

    parse
    ("1+2*3", {});
    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 error
    parser.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 LEAF

    Schema leaves are arktype definitions, validated by type.validate — a typo’d { x: "numbr" } errors right at the leaf on safeParse (and every entry point rejects bad schemas at runtime). Grammar constraints are validated even earlier: a typo’d operand("numbr") throws at createParser time.

  3. 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 offset
    result.error.expected; // tokens that would have been valid there
    }
  4. Evaluate. evaluate() parses and evaluates in one step; the result type is inferred from the expression — including through polymorphic nodes:

    const seven =
    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 [...], {}>
    parser
    .
    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): number

    Parse 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.

    evaluate
    ("1+2*3", {}, {});
    const seven: number
    const ab =
    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 [...], {}>
    parser
    .
    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): string

    Parse 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.

    evaluate
    ("'a'+'b'", {}, {});
    const ab: string
    const answer =
    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 [...], {}>
    parser
    .
    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;
    }>): number

    Parse 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.

    evaluate
    ("x*2", {
    x: "number"
    x
    : "number" }, {
    x: number
    x
    : 21 });
    const answer: number

    For dynamic input, combine safeParse with evaluateAst:

    const parsed = parser.safeParse(userInput, schema);
    if (parsed.success) {
    const value = parser.evaluateAst(parsed.ast, values);
    }
  • 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.