Skip to main content

Module conditional

Module conditional 

Source
Expand description

Static recognition of TeX conditional control words — which \if…-named command opens a \fi-terminated conditional, and which \else/\or/\fi divides or closes one.

Shared by the grammar (which builds SyntaxKind::CONDITIONAL nodes behind a shape gate) and the linter’s ConditionalIndex (which derives branch paths for duplicate-label/duplicate-package), so the two read the same name sets and the same state machine — the same arrangement as super::lexer::expl_toggle.

What that buys is precise, and worth stating precisely: the two can never disagree about what an opener is. They can still reach different verdicts on a given token, because each feeds this scan a different stream on purpose. The parser walks every token and suppresses openers inside an expl3 region (in-region layout is the formatter’s); the linter walks COMMAND nodes and skips definition-command spans wholesale (\def\stopit{\fi} carries a \fi, it does not run one), which the parser has no need to do because its own brace anchor already refuses to pair across the body’s group. Those are deliberate per-consumer filters layered around a shared recognizer, not two recognizers.

Recognition is pair-and-trust: a lowercase-if-prefixed name opens a conditional unless it is a known brace-argument macro. That leaves two families to subtract, both measured over latex2e/latex3/pgf/latexindent:

  • [NOT_FI_TERMINATED] — \ifthenelse, \iftoggle, the etoolbox test family (102 occurrences). These take {true}{false} arguments and are never \fi-terminated. Subtracting them is load-bearing rather than cosmetic: shape alone does not merely fail on one, it mis-pairs. In latexindent’s test-cases/ifelsefi/issue-250.tex an \ifnumgreater nests inside a real \ifluatex, so trusting it would steal the enclosing conditional’s \fi.
  • [OPERAND_SKIPS] — \newif\if@foo (574 occurrences) and \let\ifpdf\iftrue, where the \ifX sits in an operand slot and is data being declared or aliased, not live control flow.

Both are curated compiled-in facts, on the same footing as the verbatim and math environment tables: static lexical knowledge, never the mutable signature database (AGENTS.md decision #8). A missing entry costs a desynced stack in the linter and a demoted (never a mis-built) node in the parser, so the set extends freely.

Structs§

OpenerScan
The running state that turns is_conditional_opener into a positional decision: the operand-slot countdown and the \ifcsname body.

Enums§

FlowWord
A control word that divides or closes a conditional.
Word
What a control word does to the conditional structure, once positional context is taken into account.

Functions§

flow_word
Classify a control word’s name (no leading backslash) as a conditional divider or closer. Stateless: \else/\or/\fi are never anything else.
is_conditional_opener
Pair-and-trust: a lowercase-if-prefixed name opens a conditional unless it is a known brace-argument macro. Reads the name, no leading backslash.
operand_skips
How many following control words name claims as operands, if any.