# Argspec design

Why TeXForm describes command signatures with an xparse-style notation, what it extends, and which signatures it cannot express.

https://texform.dev/docs/argspec-design

Read this if you want to understand why TeXForm describes argument signatures the way it does — why an xparse-style string rather than a structured table, what was added to xparse and why, and where the notation runs out of expressive power. For the notation itself and how to write one, start with [Reading argspecs](https://texform.dev/docs/argspec); this page is about the reasoning and the limits.

## Why a signature notation at all [#why-a-signature-notation-at-all]

The parser is not purely syntactic. To turn `\frac{a}{b}` into a syntax tree it has to know that `\frac` takes two mandatory arguments, and to turn `\sqrt[3]{x}` into the right shape it has to know that `\sqrt` takes one optional argument before its mandatory one. That argument shape is not recoverable from the surface syntax — `\frac x y` and `\frac{x}{y}` describe the same node, and a command that took one argument would bind them differently. So every known command and environment in the knowledge base carries a description of its argument structure, and the parser consults it while reading.

The question is what form that description takes. A structured table — one row per argument, with explicit fields — is unambiguous but verbose: the vast majority of commands take only mandatory and optional arguments, and writing five lines of structured data to say "two mandatory arguments" buries the signal. A signature is read and audited far more often than it is written, so the format optimizes for being scannable at a glance.

## Why xparse-style notation [#why-xparse-style-notation]

LaTeX3's [xparse](https://ctan.org/pkg/xparse) package already solved the same problem for document-level commands: it encodes a full argument skeleton as a short string of single-letter tokens. `m m` is two mandatory arguments; `o m` is optional-then-mandatory; `s o m` is a star, an optional, and a mandatory. The notation is compact, the letters are mnemonic, and it is already familiar to anyone who has defined a LaTeX command.

TeXForm borrows that convention because it inherits those properties for free. The argspec string for a command reads as the command's shape — `s m g` is visibly "optional star, mandatory argument, optional trailing group" — without a reader having to expand a table in their head. The base letters `m`, `o`, `s`, `g`, `r`, and `d` keep their xparse meanings, so the learning cost is mostly already paid.

This notation is xparse-inspired, not xparse-compatible. It is TeXForm's own signature language, parsed by TeXForm's own parser, and it diverges from xparse wherever describing math commands calls for it. The next sections cover where.

## What TeXForm adds to xparse [#what-texform-adds-to-xparse]

xparse describes *how* an argument is delimited, but not *what kind of value* it holds, because in LaTeX3 the value's interpretation is the macro's own business. A math parser does not have that luxury: it has to know whether an argument is a delimiter, a length, an integer, a name, or a column template, because each is read and validated differently. So TeXForm adds the following, each driven by a concrete need from the supported packages:

* **Value-kind annotations** (`:T`, `:O`, `:D`, `:N`, `:L`, `:I`, `:K`, `:C`). A `:` suffix declares how a slot's content is interpreted — text, operator-name content, delimiter, control-sequence name, dimension, integer, key–value, or column template — defaulting to math content when absent. `\label{sec:intro}` is `m:N`, because its argument is a name string that must reject escape sequences, not a math subtree; `\operatorname` is `s m:O`, because its name body parses as math content but must serialize as a compact operator-name run.
* **The nullable modifier** (`?`). Appended to an annotation, it marks a slot that is present but may carry an empty value, distinct from an optional slot that may be absent. `\genfrac`'s leading delimiter and thickness arguments are `m:D? m:D? m:L?`: each must be written, but each may be empty (`{}`), meaning "no delimiter" or "default rule thickness".
* **The required braced group** (`m{}`). xparse's `g` reads an optional `{...}`; `m{}` is its required counterpart. The distinction from `m` is that the braces are structural grouping, not part of the value, which matters when the argument is later serialized.
* **Delimiter-choice pair lists** (`r<…>`, `d<…>`). A plain `r` or `d` names one delimiter pair. The `<open,close>` list lets a single argument slot accept several delimiter shapes at once.

That last one is worth its own section, because it is the extension with the most design weight behind it.

## The delimiter-choice problem [#the-delimiter-choice-problem]

Several `physics` commands wrap a single argument in any of several delimiter shapes. `\qty` accepts `(x)`, `[x]`, `{x}`, or `|x|`, and all four mean the same thing: one argument, differently fenced. A plain xparse `d` or `r` can name only one pair, so the only xparse-shaped way to cover four shapes is four separate optional slots — and that is wrong. Four slots tell the consumer there are four independent arguments, when there is really one argument that can arrive in four shapes. A position-indexed reader cannot tell which slot the value landed in.

The pair-list form fixes this by mapping every listed shape to the *same* slot. `d<(,)><[,]><{,}><|,|>` is one optional argument; the parser tries each pair in order and reads with the first that matches, recording which pair won so the original fencing can be restored on serialization. This is genuinely outside xparse's vocabulary, and it is the reason the notation is described as xparse-*style* rather than an xparse subset.

## Known expressive limits [#known-expressive-limits]

An honest signature notation is one whose boundaries are documented. The following shapes cannot be expressed exactly today. None of them block parsing — each supported command has a working best-fit signature — but in these cases the signature is an approximation, and that is worth knowing if you are auditing behavior or writing your own.

### Value kinds are coarse [#value-kinds-are-coarse]

The value-kind annotations name broad categories, not specific accepted-value sets. Two annotations carry most of this weight and both are deliberately wide:

* `:N` covers any name-like argument but cannot narrow to a specific set of accepted literals.
* `:C` describes a full column template (`c|c`, `p{1em}`, `@{...}`) and cannot be restricted to, say, a short list of alignment letters.

So a command that accepts only a small fixed set of literal tokens still has to be modeled as `:N` or `:C`, which over-accepts: the signature says "a name" or "a column template" where the command really means "one of these few values". Commands whose true argument is a restricted enumeration — among them `\breakAlign`, `\mmlToken`, `\parbox`, and `\cfrac` — fall into this gap. They parse correctly, but their argspec describes a wider value space than the command actually accepts.

### One slot cannot mix visible delimiters with structural braces [#one-slot-cannot-mix-visible-delimiters-with-structural-braces]

The pair-list form treats every matched `<open,close>` pair as a *visible* delimiter — a fence that is part of the value and should survive serialization. That is the right model for `\qty`, where `\qty{x}` is meant to render with braces shown.

It is the wrong model for the `physics` matrix-quantity family, where the brace shape means something different from the others. For `\mqty`, the forms `\mqty(a)`, `\mqty[a]`, and `\mqty|a|` produce visible delimiters, but `\mqty{a}` uses the braces only as input grouping and renders with no outer delimiter at all. The closest single-slot signature is `s d<(,)><[,]><{,}><|,|>`, which correctly keeps all four shapes in one argument slot — but it forces the `{,}` branch to be treated as a visible `Paired` delimiter, so the signature cannot say "this branch triggers a read, but the braces themselves are not shown". Splitting the brace branch out as a separate `g` slot would express the structural grouping, but at the cost of breaking the single-slot guarantee the pair list exists to provide. The affected `physics` families include `MatrixQuantity` (`\mqty` and friends), `Eval`, `Differential`, and parts of the vector-operator family. Where a brace branch genuinely fences a visible delimiter — `\qty`, `\Re`, `\Im` — the pair-list model is exact.

### Some MathJax behaviors are intentionally not modeled [#some-mathjax-behaviors-are-intentionally-not-modeled]

A few behaviors in MathJax's own command handling are out of scope for a static signature, by design rather than by gap:

* **Stack-item operators.** The vector-operator family (`\grad`, `\curl`, `\div`, `\laplacian`, …) binds a following fenced group through MathJax's stack-item machinery, not by reading an argument. Their argspec is empty (`""`); the fence association is a transform concern, not a parse-time argument.
* **Bare-token reads.** MathJax's `\dd x` can swallow a following bare token as an argument. TeXForm does not model this: `\dd x` parses as `\dd` followed by a separate `x`.
* **Value-driven branching.** A signature cannot say "if the first argument is `\frac`, read another argument", so behaviors where one argument's content changes how later arguments are read are not expressible.

These are exclusions, not failures: the signature describes a fixed argument skeleton, and behaviors that depend on token lookahead beyond delimiter matching or on the content of earlier arguments fall outside what a skeleton can carry.

## Validation is part of the notation [#validation-is-part-of-the-notation]

Because signatures are written by hand — in the knowledge base and, for custom commands, by users — the parser that reads them rejects malformed strings precisely rather than guessing. Invalid combinations are caught at the point they occur: `!` on a required slot, `?` on a star or column, an unknown annotation letter, `m{x}` (only `m{}` is allowed), a value-kind annotation on `s`. The same parser backs both the compile-time macro that builds the built-in knowledge base and the runtime `validate_argspec` function, so a string that validates at runtime is read identically to one baked in at build time. That shared path is what makes [Reading argspecs](https://texform.dev/docs/argspec) a reference you can trust against the actual parser.

## Related [#related]

* [Reading argspecs](https://texform.dev/docs/argspec) — the notation reference, with one real command per parameter kind.
* [Inject custom commands](https://texform.dev/docs/inject-custom-commands) — writing an argspec and teaching it to the parser.
* [The knowledge base](https://texform.dev/docs/knowledge-base) — how the signatures for built-in commands are built and validated.

