Help us shape the future of Go! Take the 2026 Go Developer Survey

syntax

package
v0.0.26 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package syntax turns shell source into tokens and a syntax tree.

It is written from docs/spec, which is written from POSIX, from vendor documentation, and from recorded runs of real shells. See CLEANROOM.md.

Index

Constants

View Source
const ArithValueMark = '\x00'

subscript reads `[…]` after a name.

The inside is read as an expression, because on an indexed array that is what it is: `a[i+1]` counts from the value of i. It is *not* refused when it will not read as one, and that is the whole point of the record this returns. A subscript is only an expression when the name is an indexed array; on an associative one it is a key, read exactly as `${m[k]}` reads it — and the parser cannot know which the name is, because the name need not exist yet or at all. So the text is kept, the expression is left nil, and the question is handed to whoever evaluates it, which is the only place the answer is knowable.

Refusing here is what `$(( m[.accept-line] ))` used to meet: a key that is not an expression, on a name the parser had no way to know was associative. Measured, bash 5.3 and zsh 5.9.2 both answer that with the element and not with a complaint.

emptyOK says the caller can carry a `[]` written with nothing between the brackets. Where it is false the empty pair is the parse failure it has always been; where it is true Empty says so and Index is nil, which is a question for whoever evaluates it. See ArithIndex.Empty. ArithValueMark stands in front of a byte that arrived in a **value** and must not be read again as subscript syntax.

An arithmetic expression is expanded before it is parsed — `$(( $x$y ))` with `1+` and `2` is 3 in every shell in the panel, so the text the parser reads is the text the expansions produced. That is right for the expression and wrong for a *subscript*: an associative array's key is a string, and a shell that has just expanded one does not hand the result back to the bracket scanner. Measured 2026-09-13, `key='x],b['; m[$key]=1; (( m[$key]++ ))` increments the element in bash 5.3.15 and under `sh`, where reading the expanded `m[x],b[++` back as syntax finds a subscript of `x`, a comma, and a second name with an empty subscript (#2581).

So the marks are put on by whoever expanded the text, over the bytes that came from a value and only inside brackets the *source* wrote. A bracket the script wrote still delimits, and a bracket a value carries does not — which is the same distinction markWrittenBars draws for a pattern's alternation, and drawn here for the same reason: the two cannot be told apart once the text exists.

NUL because no source text holds one and no shell value is supposed to. A mark in front of a mark is a NUL that was data, so a bare one can only be this.

Variables

ModifierLetters is what the shell that has them accepts, measured a letter at a time. Fourteen, and the fourteenth is not a letter: `:&` repeats the last substitution.

The value is what may follow the letter inside the same segment, because that is the only thing about a segment this table cannot say twice. Most take nothing at all; `h` and `t` take a count; `s` takes a whole substitution and `&` takes the one before it.

Functions

func EndsWithContinuation

func EndsWithContinuation(text string) bool

EndsWithContinuation reports whether text ends with a backslash joining it to a line that has not arrived yet.

A parser cannot answer this, and is right not to. `echo one \` at the end of a *file* is a finished command — the continuation joins it to nothing — while the same text at the end of what has been read so far is a command still being written. Which one it is depends on whether there is more input, which only the thing doing the reading knows.

func EscapeArithValue added in v0.0.13

func EscapeArithValue(s string) string

EscapeArithValue is UnmarkArithValue for a dialect that writes a value's bytes back with a backslash in front of them.

The marks say which bytes an expansion put inside brackets the script wrote, and one shell shows exactly those escaped when it quotes the expression back: measured 2026-09-22 on bash 5.3.20, `declare -A assoc; key='x],b[$(echo 9)'; (( 'assoc[$key]++' ))` names `'assoc[x\],b\[\$(echo 9)]++'`. So this is the same text UnmarkArithValue produces with the marks turned into backslashes rather than dropped; which of the two a dialect wants is interp.Diagnostics.ArithValueShownEscaped.

func FunctionKeywordBodyNeedsItsOwnLine added in v0.0.10

func FunctionKeywordBodyNeedsItsOwnLine(body Command) bool

FunctionKeywordBodyNeedsItsOwnLine reports whether a declaration written with the `function` keyword must break the line between its name list and its body.

The name list after the keyword is greedy, so a body written beside it is read back as more *names*. Measured 2026-09-19 against zsh 5.9.2, each body first on the line after `function foo` and then beside it, the script then printing `${#functions}` and calling `foo`:

{ echo hi; }              both define one function and print `hi`
( echo hi )               beside it: `unknown file attribute`, status 1
if true; then echo hi; fi beside it: parse error near `then`, status 1
while/for/repeat … do …   beside it: parse error near `do`, status 1
case x in x) echo hi;; …  beside it: parse error near `)`, status 1
(( 1 )) && echo hi        beside it: `no matches found`, status 1
[[ -n x ]] && echo hi     beside it: `bad pattern: [[`, status 1
echo hi                   beside it: **three** functions, nothing run, 0

The brace group is the one body that ends the list itself. Every other shape is either refused outright or — the last row, and the reason this is a rule rather than a preference — silently a different program at status 0: `function foo echo hi` defines `foo`, `echo` and `hi`, each with whatever followed as its body, and running the script prints nothing.

A newline ends the name list in every dialect that has one, which is what makes it the separator rather than the `;` only one grammar reads there.

Both printers in this tree ask this question and neither may answer it alone: Print had the guard and `internal/fmt/printer` never did, so the formatter rewrote that last row and nothing downstream could tell (#3746). It lives here for the reason SameProgram does — two copies of a rule the round trip and the formatter must agree on would eventually not agree.

The parenthesised forms do not ask: `()` ends the name list itself, so `foo() echo hi` and `function foo() echo hi` both define one function. The tree does not record the hybrid's parentheses, though — see FuncDecl — so a caller working from the tree alone cannot part `function foo()` from `function foo`, and a line break is right for both where a blank is right for only one.

func IsNameIn added in v0.0.21

func IsNameIn(s string, d *Dialect) bool

IsNameIn reports whether s is a name under the dialect's rule for one — the grammar's own, for an interpreter judging a builtin's operand by it so that the two cannot come to disagree about which characters a name holds.

func MarkedExpansion added in v0.0.6

func MarkedExpansion(src string) bool

MarkedExpansion reports whether text holds a `$` or a backtick a *value* carried, which is the half UnmarkedExpansion does not count.

Exported for the same reader and for the conservative half of its rule: a subscript holding both is read as it stands rather than half expanded.

func Print

func Print(f *File) string

Print renders a parsed file as source that parses to the same tree.

func PrintArrayElem added in v0.0.10

func PrintArrayElem(e *ArrayElem) string

PrintArrayElem renders one element of an array literal as it was written — an ordinary word, or a nested literal with its parentheses.

func PrintArrayElemWith added in v0.0.12

func PrintArrayElemWith(e *ArrayElem, l Layout) string

PrintArrayElemWith is PrintArrayElem with an arrangement, for the reason PrintWordWith has one.

func PrintCommand

func PrintCommand(c Command) string

PrintCommand renders one command, which is what a function's body is.

func PrintExprWith added in v0.0.21

func PrintExprWith(e Expr, l Layout) string

PrintExprWith renders one and-or expression with a chosen arrangement.

The companion to PrintWith for the two statement shapes that are **not commands**. A pipeline and an `&&`/`||` list are Expr and neither implements Command, so PrintWith cannot be handed either one — and a caller that needs the whole statement's text rather than one command's has nothing else to reach for. `RunningCommand` is that caller: the shell fires its DEBUG trap once for a pipeline and once for a list, and the parameter a dialect shows at those firings is the statement read back.

Unlike PrintWith this applies no body wrapping, because there is no arrangement in which a statement is a body.

func PrintFileWith

func PrintFileWith(f *File, l Layout) string

PrintFileWith renders a parsed file with a chosen arrangement, which is what PrintWith is to PrintCommand.

A whole file rather than one command, because the thing a prompt loop has in hand is a *line*, and a line can hold several statements. `true;false` typed at a zsh prompt reaches `preexec` as `true; false` in its second argument and as two lines in its third, from one input — so both forms are a file printed two ways, and neither is a command printed at all.

func PrintWith

func PrintWith(c Command, l Layout) string

PrintWith renders one command with a chosen arrangement.

func PrintWord

func PrintWord(w *Word) string

PrintWord renders one word as source that parses to the same word, quotes and all.

A diagnostic wants this: a grammar that names the word an unreadable expansion sits in has to be able to say the word, and by the time anything has failed the word has been taken apart into spans.

func PrintWordQuotingRun

func PrintWordQuotingRun(w *Word, i int) string

PrintWordQuotingRun renders the run of spans around index i that share its quoting, *without* the quote characters that surrounded the run.

The run rather than the word, because that is the unit one grammar's diagnostics name: `echo 'lit'"${x@QQ}"` blames `${x@QQ}` alone while `echo "pre${x@QQ}post"` blames all of `pre${x@QQ}post`. Both are one word, and what separates them is that the first changes quoting in the middle.

Literal text goes in as it stands. There are no quotes around the result to protect anything from, so escaping it would add characters that were never written — `"\"${x@QQ}\""` names `"${x@QQ}"`, measured.

func PrintWordQuotingRunWith added in v0.0.24

func PrintWordQuotingRunWith(w *Word, i int, l Layout) string

PrintWordQuotingRunWith is PrintWordQuotingRun with an arrangement, for a shell whose diagnostic names the run the way it lists it: braces as written, a `$'…'` as its value, a command substitution laid out again.

func PrintWordWith added in v0.0.12

func PrintWordWith(w *Word, l Layout) string

PrintWordWith is PrintWord with an arrangement, for the one caller that needs a word written some way other than as it stands.

Only the fields a *word* can reach mean anything here, which today is Layout.AnsiCQuotedWordIsItsValue: a shell's own `set -x` writes a `$'…'` element of an array literal as the text it stands for, and the decoder for that is the dialect's — see the field.

func QuotedRunBoundary added in v0.0.13

func QuotedRunBoundary(spans []Span, i int) (opens, known bool)

QuotedRunBoundary says whether the span at i begins a new pair of quotes rather than continuing the pair the span in front of it was in — and, in `known`, whether that could be worked out at all.

It is the difference between `"$e$@"` and `"$e""$@"`, and both a run and a printer need it. Those two words are the **same two spans** — a span records the quoting it sits in and not where one run of that quoting ends — so the boundary is read off the **positions** instead: spans inside one pair of quotes are contiguous in the source, and a gap between two of them is the closing quote and the opening one.

Two callers, and each was wrong without it. A shell that reads what an empty `"$@"` takes with it is reading one quoted *string* rather than the word, so the string the list is not in is a quoted null that survives — interp.Semantics.EmptyListTakesTheWord, where bash 5.3 answers `"$e$@"` with no argument at all and `"$e""$@"` with one. And a printer that joined the two runs wrote `"$e""$@"` back as `"$e$@"`, which is that different program (#4203).

Written this way rather than as a flag on Span because that flag would be a byte on a struct whose size is a committed budget rather than a free choice — see `syntax/nodesize_test.go`, where raising it is priced at 185KB of resident memory per byte and called a decision rather than a fix.

**The two callers want opposite answers where it cannot be worked out**, which is why `known` is a result rather than a default taken here. A run keeps the word, so *not knowing* has to read as a boundary there: that is the answer every column gives, and the worst it costs is a fix not applied. A printer writing an extra pair of quotes is a different program, so not knowing has to read as no boundary there — which is what it wrote before this existed. Only an extent computed too *long* could be wrong either way, and the lengths below are exact or short by construction.

func RemoveArithDoubleQuotes added in v0.0.13

func RemoveArithDoubleQuotes(s string) string

RemoveArithDoubleQuotes is removeUnmarkedDoubleQuotes for the caller that has to perform the removal **before** the text is expanded rather than after, which is where the one dialect with the removal performs it.

The quotes come off the text a script wrote inside `(( … ))`, and the expansions in it are performed over what is left — so a quote a value carries arrives after the removal has run and is a character of the result. Measured 2026-09-22 from a script file under `LC_ALL=C` on bash 5.3.20, with `i=1`:

$(( 1"0" + $i ))                   11
q='"'; $(( 1${q}0${q} + $i ))      refused, `1"0" + 1` quoted back

The same text either way once it is joined, so only the order of the two steps parts them. See ArithValueMark for the other half of the same rule.

func SameProgram

func SameProgram(a, b *File) (string, bool)

SameProgram reports whether two trees are the same program, and where they first differ if they are not.

This is the question a formatter has to be able to ask about its own output, and #1401 is where it was decided that asking it belongs here rather than in a test file: the formatter and the printer's round trip must mean the same thing by "the same program", and two copies of the definition would eventually not.

Positions are ignored, and so is anything above that keeps the source spelling for a diagnostic. Everything else must match.

func ShellWords

func ShellWords(src string, d Dialect, opt ShellSplit) []string

ShellWords reads src as a command line and returns the words it is made of, each one exactly as it was written — quotes, backslashes and all.

It is Lexer.Next and nothing else. The question "how would the shell split this" already has an answer in this package, and the answer is the lexer: a second scanner beside it would agree on `a b` and then disagree about `a"b c"d`, `$(f x)`, `a#b`, `2>&1` and every other place where a word boundary is not a blank. So this walks the token stream and keeps the source each token covers.

**What it reads is a value, not a program.** That difference is real and it is why this is a function rather than a parse:

  • Nothing is expanded, run or resolved. `$(f x)` is three tokens' worth of text kept as the one word it was written as, because the caller wants the word and not what it would produce.
  • No here-document body is ever read. A `<<` here is an operator with a word behind it; the lines that follow are more of the same text and not a body, which is measured on the shell whose flag this serves — `a <<EOF`, a body line and the delimiter come back as six separate words.
  • Input that ends inside a quote or a substitution is not an error. The rest of the text is the last word, unterminated as written.

The token stream this walks is not a program either, and must not be handed to a parser: under CommentsKept a comment arrives as a word.

**Argument position is the parser's to know, so the parser is what reads this.** A `(` that *starts* a token belongs to the word where an argument may stand — `a (b c) d` is three words and not six — and command position is the whole of the difference: `(b c) d` and `a; (b c) d` split the parenthesis off. The lexer has the flag, `inArgument`, and it is set from `parseSimple`, because deciding it needs to know that `a="x"` is an assignment and that `then` is a keyword — neither of which a token stream says.

So this drives a Parser and keeps the tokens it caused, rather than running the lexer alone and answering the question a second time. A state machine here would be that second answer: it gets `a="x" (b c)` and `if a; then (b c)` right only by restating the assignment and keyword rules, and then still misses `repeat 2 (b c)`, `coproc (b c)` and `foreach x (a b)`, which are three more of the parser's own (measured on the shell with the flag — all three split the parenthesis off, and `(a) (b)` does not). That is the shape #1331 and #1397 were both undoing (#1514).

The **tree is thrown away and so is any error**, which is what keeps this a split of a value rather than a parse of a program: input that ends inside a quote or a substitution is not a failure here, and text that is not a program at all still has to come back as the words it was written as. Where the parser stops early, the rest is lexed plainly and appended — there is no position to know past the point the grammar lost track, and the shell with the flag is no more definite there.

`noHeredocBodies` is the one thing the parser must be stopped from doing. It would otherwise queue a `<<` and let the lexer claim the lines after it as a body, which is input a splitter may not consume; see that field.

func SubscriptClosingBracket added in v0.0.8

func SubscriptClosingBracket(s string, quoting SubscriptQuoting, looseQuotes bool) int

closingBracket finds the `]` that closes the subscript s opens with, or -1.

Matched rather than *last*, which is what this used to take. A colon-less operator whose value holds another subscripted expansion — `${tags[@]+${tags[@]}}`, the standard way to expand a possibly-empty array under `set -u` — put a second `]` in the string, and taking the last one swallowed everything between. The simple `${a[@]+x}` worked, which is why it went unnoticed until a real script used the nested form.

Depth counting, because a subscript may itself hold one: `${a[b[0]]}`.

Quoting is stepped over where the dialect says it protects the bracket, so `${m['a]b']}` reads the key back that `m['a]b']=v` wrote — the write side has always quoted, being the lexer's word boundary, and the read side is a different scanner that used to stop at the quoted `]` and hand the rest to the operator scan, which called the whole expansion a bad substitution. See Dialect.SubscriptQuoteProtectsTheClosingBracket for which constructs each dialect counts.

An unterminated quote returns -1 over **source**, the same as an unclosed bracket: the run goes on past the end of this text and there is no closing `]` in what is left, so the caller leaves the brackets alone.

Over a builtin's **operand** it does not, which is the one place the two scans part — see SubscriptClosingBracket.

SubscriptClosingBracket is where the `]` that closes a subscript stands, counting from the `[` the text starts with, or -1 where there is none.

Exported for the same reason SubscriptComma and SubscriptExpressionEnd are: the question is asked in the parser, over source, and at the run, over the **string** a builtin's operand carries — `unset "a['x]y']"`, where the quotes reached the builtin because the shell's own quoting had already been taken off. Two copies of the scan is two answers to one question, and this family has grown a second helper that omitted the first one's fix before.

Which quoting holds a `]` back is the caller's to supply: the parser reads Dialect.SubscriptQuoteProtectsTheClosingBracket, and the builtin's operand reads an answer of its own, because the panel does not answer the two alike.

looseQuotes is the one thing beyond the quoting set that differs, and it is why this takes a flag rather than being the same call: with it an **unterminated** quote is an ordinary character rather than a run to the end of the text. It belongs to an operand whose brackets the *parser* read — `unset m[$k]` with `$k` holding `80's`, where the apostrophe came out of a value and is a byte of the key with nothing further along the line to close against. In source, and in an operand the shell was handed as one string, the same byte opens a run that goes on past the `]` and, measured, refuses the whole script for an unmatched quote. The unquoting half of the operand's answer, Runner.operandSubscriptUnquoted, has always read a lone quote as a character; this is the scanning half, told when to agree with it.

func SubscriptComma

func SubscriptComma(text string) (at int, extra bool)

SubscriptComma reports where the first comma outside any nesting in a subscript's text is, and whether another follows it.

Nested parentheses and brackets hold their own commas, so `f(1,2),3` has two halves and not three, and a flag group's own `(rn:1,2:)` argument holds none at all. Exported because the split is asked in two places — here, where a written pair with a flag group in it becomes two ends, and at the run, where a pair of plain ends is separated out of the subscript's expanded text — and two copies of it would be two answers to one question.

func SubscriptExpressionEnd

func SubscriptExpressionEnd(text string) int

SubscriptExpressionEnd is where a subscript's *expression* stops, or -1 where it runs to the end of the text.

The same walk SubscriptComma makes, over two characters instead of one, because the shell with ranges ends a subscript's expression at either of them. Measured on zsh 5.9.2, 2026-09-12, with `a=(p q r s)`:

i="2,3";   ${a[$i]}   q     the reader takes the 2 and stops
i="1+1,3"; ${a[$i]}   q     so it is not the comma *operator*, which is 3
i="2,";    ${a[$i]}   q     and the tail need not be an expression
i="2;3";   ${a[$i]}   q     nor is the comma the only separator
i="2,n=9"; ${a[$i]}   q, and `n` is still 0 — the tail is not evaluated
i="(1,2)"; ${a[$i]}   q     nested, so the comma operator applies
$(( 1,2 ))            2     and outside a subscript it always applies

So it is the *top level of a subscript* and nowhere else: the operator is there, and this reader stops before reaching it. Exported for the same reason SubscriptComma is — the question is asked in the parser and at the run, and two copies would be two answers.

func SubscriptedElement added in v0.0.1

func SubscriptedElement(w *Word) bool

SubscriptedElement reports whether a compound literal's element names the subscript its value goes to, rather than being an ordinary word.

func UnmarkArithValue added in v0.0.1

func UnmarkArithValue(s string) string

UnmarkArithValue takes the marks back off, leaving the text a value actually held. Exported for whoever put them on: text that never became a subscript still has to be shown without them.

func UnmarkedExpansion added in v0.0.6

func UnmarkedExpansion(src string) bool

UnmarkedExpansion reports whether text holds a `$` or a backtick the *script* wrote, as against one a value carried.

A marked byte is a character of a value and begins nothing. Measured 2026-09-15 and recorded on Parser.ParseArithExpanded: `k='$(cmd)'; a[$k]=V` runs nothing in any column, so a `$` that arrived inside a subscript's brackets is not an expansion waiting to happen. The same text arriving with the *brackets* — `e='a[$k]'; $(( $e ))` — has no mark on it and is expanded once, which is what every column with arrays does (#3303).

Exported for the interpreter, which asks the same question of a subscript before deciding whether to expand it. See ArithValueMark.

Types

type AliasAtAFunctionName added in v0.0.9

type AliasAtAFunctionName uint8

AliasAtAFunctionName is what a dialect does with an alias standing where a function name is being defined.

Measured 2026-09-18, script files under `env -i PATH=/usr/bin:/bin LC_ALL=C` with standard input on /dev/null. The body is `alias zz='typeset -n'` and then `zz() { :; }`, whose expansion cannot be a legal definition, with `printf 'after\n'` behind it. Body B is the same with a blank before the `(`; body C is `alias zz=echo`, whose expansion *is* a legal definition.

column         A                      B                C
bash 5.3.20    syntax error, 2        the same         after, 0
dash 0.5.12    syntax error, 2        the same         after, 0
ash 1.37.0     syntax error, 2        the same         after, 0
ksh93u+        after, 0               syntax error, 3  after, 0
zsh 5.9.2      its own refusal, 1     the same         the same

**bash has to be measured with its expansion switched on.** `shopt -s expand_aliases` is off in a script there, so a bash column taken without it expands nothing at all and prints `after` for every body — which reads exactly like a shell that suppresses, and is why body C cannot tell the two apart either. With the option on, bash expands.

Body C is what says zsh's answer is about the **name being an alias** rather than about the value: it refuses a definition whose expansion would have been perfectly legal.

The `function name { … }` spelling is outside all of this: the word after `function` is not a command word, so no column expands there and all five answer `after` at 0 (dash has no keyword and refuses the brace).

const (
	// AliasExpandsAtAFunctionName expands like any other command word. The
	// zero value, and bash's, dash's and BusyBox ash's answer.
	AliasExpandsAtAFunctionName AliasAtAFunctionName = iota

	// AliasSuppressedWhereTheParenIsAdjacent declines the expansion where the
	// `(` stands immediately after the word and expands where a blank
	// separates them. ksh93's answer, and the blank is what decides it.
	AliasSuppressedWhereTheParenIsAdjacent

	// AliasRefusesAFunctionName refuses the definition outright, before any
	// parse of the body, with a remark of its own in front of the parse
	// failure. zsh's answer. See RemarkFunctionNameIsAnAlias.
	AliasRefusesAFunctionName
)

type Aliases

type Aliases func(name string) (value string, ok bool)

Aliases answers whether a word names an alias, and what it stands for.

The parser holds no table of its own: the table belongs to the shell, is changed by the `alias` builtin while the script runs, and is therefore the caller's to keep. A nil Aliases expands nothing, which is what a shell that does not expand them in this context supplies.

The same type answers for all three kinds. Parser.GlobalAliases is asked about every word rather than only a command word, and Parser.SuffixAliases is asked about a command word's *extension* rather than about the whole word — so the argument is a suffix there, and nothing else about the seam changes.

type AnonFunc

type AnonFunc struct {
	Body Command
	// Args are the words after the body, which become the positional
	// parameters of the call.
	Args  []*Word
	Start Pos
	// Keyword records that the `function` word was used in place of the
	// empty parameter list, which is the other spelling.
	Keyword bool
	// Bare records the keyword standing entirely alone — no name, no
	// parameter list and no body — which one grammar takes and the rest
	// refuse. See [Dialect.BareFunctionKeyword].
	//
	// Body is an empty group standing where the body was not written, so
	// that nothing reading a body finds nothing; this is the flag that says
	// the group is a placeholder rather than a `{ }` somebody typed, and the
	// two are not the same command. The measured difference is the status:
	// `false; function` leaves 1 behind, where `false; function { }` leaves
	// 0, because the first runs nothing at all.
	Bare bool
	// contains filtered or unexported fields
}

AnonFunc is `() { … }` — a function with no name, defined and run where it stands, with the words after its body as its positional parameters.

A function rather than a group, and the difference is observable: `local` inside one is local, `$#` counts the words after the body, and a `return` leaves it. Only the *name* is missing, which is why its own frame reports one the shell invents.

The two spellings are two productions

`()` and `function` reach this one node and run through one path in the interpreter. **In the reference they are different productions**, and three issues in a row turned on which of the two was written — not on the body, not on the words, not on where any of it stood:

  • the words and redirections behind the body interleave under `()` and do not under `function` (#5079);
  • `function` alone takes the next command as a body where `()` does not (#5078);
  • an unbracketed body keeps the caller's line numbering under `()` and is renumbered under `function` (#5080).

Each was found by writing the row again with the header word changed and measuring it, and each would otherwise have shipped correct for one spelling and backwards for the other. So: **anything measured on one spelling is unmeasured on the other until a row says otherwise**, and a grid over this node wants the header as an axis before it wants anything else.

They do agree about some things, and the agreements are worth stating rather than assuming too: the frame's invented name, the file that frame reports (#5084), and the numbering of a body written in brackets are the same under both.

func (*AnonFunc) End

func (c *AnonFunc) End() Pos

End is the body's end, or the parentheses' own start where there is no body, which is FuncDecl.End's answer for the same reason: a nameless function whose body never came keeps its node — the parentheses were read and the error is what the caller acts on — so a nil body here is a normal product of a failed parse rather than a malformed tree. It was unreachable while `()` with nothing after it was read as an empty subshell, and the extent walk through an enclosing construct crashed on the first input that made one (#3961).

func (*AnonFunc) Pos

func (c *AnonFunc) Pos() Pos

type ArithAssign

type ArithAssign struct {
	Name string
	// Index is the subscript when the target is an array element, as in
	// `(( a[0] = 9 ))`, and nil when the target is a plain name.
	Index ArithExpr
	// Sub is the subscript as written, for the reason ArithIndex.Sub is.
	Sub string
	// SubMarked is Sub with the value marks still on it, for the reason
	// ArithIndex.SubMarked is: `(( a[$k] = 5 ))` stores under the key a
	// value's quote characters are part of, not under the one quote removal
	// would leave behind.
	SubMarked string
	// Flags is the group the subscript opened with, for the reason
	// ArithIndex.Flags is: `(( a[(r)20] = 9 ))` writes the element the same
	// search reads.
	Flags *SubscriptFlags
	// Empty says the brackets held nothing — `(( m[] = 4 ))` — which Index
	// and Sub cannot say between them, a plain name having neither either.
	// The same field ArithIndex carries and for the same reason: an
	// arithmetic expansion substitutes its parameters before it parses, so
	// `(( m[$w] = 4 ))` with an empty `$w` *is* this by the time the
	// expression exists.
	//
	// Carried rather than refused while parsing, which is a change: the pair
	// was a parse error in this one position while `$(( a[] ))` beside it was
	// carried, on the grounds that the three shells with the construct part
	// over the write as well as over the read. They do — and each of the
	// three writes is the same shell's *read* answer wearing a different
	// sentence, which is what makes it one axis and two wordings rather than
	// a refusal (#1764).
	Empty bool
	// UnclosedQuote is the same fact on the write side, carried so that a
	// target names the element a read of it names. See
	// ArithIndex.UnclosedQuote.
	UnclosedQuote bool
	Op            string // = += -= *= /= %= <<= >>= &= ^= |=
	Value         ArithExpr
	Start         Pos
}

func (*ArithAssign) End

func (n *ArithAssign) End() Pos

func (*ArithAssign) Pos

func (n *ArithAssign) Pos() Pos

type ArithBinary

type ArithBinary struct {
	Op   string
	X, Y ArithExpr
	// YStart is the byte offset, within the expression's own text, where the
	// right operand was written — after the operator and after the blanks
	// behind it.
	//
	// It is here because the tree cannot answer the question a diagnostic
	// asks. A shell that blames a division by zero blames the *divisor as
	// written*, parentheses and sign included — `1/((0))` names `((0))` and
	// `1/-0` names `-0` — and the tree records what an operand is rather than
	// the characters it was spelled with, so a parenthesised divisor has no
	// text of its own to name and a signed one has the wrong text. Searching
	// the expression for the operand's leaf text answers neither, and answers
	// `0/0` with the dividend. The offset is the only thing that does.
	YStart int
}

ArithBinary is any infix operator, including the sequence operator.

func (*ArithBinary) End

func (n *ArithBinary) End() Pos

func (*ArithBinary) Pos

func (n *ArithBinary) Pos() Pos

type ArithBracketScan added in v0.0.6

type ArithBracketScan struct {
	// Depth is the bracket nesting, counting only brackets that are not
	// content.
	Depth int
	// contains filtered or unexported fields
}

ArithBracketScan is the quoting state a subscript's bracket scan carries: which quotation it is inside, whether a backslash is holding the next byte, and how deep the brackets are.

A type rather than two locals because the same state has to be carried across the *literal spans* of a text that is still being expanded — an expansion can sit inside a quotation the source opened before it, and a `]` the source wrote inside one closes no subscript there either. Exported for that second reader: the interpreter decides which bytes to mark while it is walking the spans, and has to draw the boundary the parser will draw. One type rather than two, so the two readings cannot part.

Marks are the caller's to skip. Only the bytes a scan is shown reach here, and a byte behind a mark is content whatever it spells, so a caller passes it nothing it has already decided about. See ArithValueMark.

func (*ArithBracketScan) Content added in v0.0.6

func (s *ArithBracketScan) Content(b byte) bool

Content reports whether b is a character of the text rather than syntax, advancing the state over it.

It returns false for the two brackets and for nothing else, so a caller can switch on it and then on the bracket: a quote opens or closes a quotation and is content in either case, and so is every byte inside one.

func (*ArithBracketScan) Quote added in v0.0.11

func (s *ArithBracketScan) Quote() byte

Quote is the quotation the scan is inside, and 0 outside one.

Exported for the reader that has to tell an apostrophe's contents from a double quotation's: one dialect performs the expansion a double quotation holds inside a subscript and not the one an apostrophe holds, which is a question about *which* quotation rather than about being inside one. See interp.Semantics.WrittenSubscriptQuotationStopsItsExpansion.

func (*ArithBracketScan) Unclosed added in v0.0.6

func (s *ArithBracketScan) Unclosed() bool

Unclosed reports whether the text ran out inside a quotation.

type ArithCall

type ArithCall struct {
	Name string
	// Args are the arguments, each an expression of its own. `mf()` with
	// none is a call and not a name: zsh registers a math function with a
	// minimum of zero and takes `$(( mf() ))`.
	Args []ArithExpr
	// Text is the call exactly as it was written, from the first byte of
	// the name through the closing parenthesis.
	//
	// Kept because the one shell with the construct quotes it back verbatim
	// when the count of arguments is wrong: measured 2026-09-08,
	// `$(( mf( 5 , 6 ) ))` against a one-argument registration is
	// `wrong number of arguments: mf( 5 , 6 )`, spaces and all, so the
	// sentence is the source text rather than a rendering of the tree.
	Text string
	// ArgumentsAreText says the text between the parentheses is not an
	// argument list this reader could make sense of, so Args is empty and
	// Text is all there is.
	//
	// **The registration is what decides, and it does not exist yet.** A
	// string math function takes that text raw and unevaluated — `sf( a , b
	// c )` passes the one argument ` a , b c ` — and the registration that
	// says so is made by a builtin at run time, several hundred lines after
	// the program was read. So the reader cannot refuse here without
	// refusing a program the shell it models runs, and it cannot accept
	// silently either: a name that turns out *not* to be registered that way
	// still owes a complaint. Keeping the text and saying that it is text is
	// what lets the evaluator answer both.
	//
	// Both columns with the construct defer in exactly this way, so it is
	// not a dialect's question. Measured 2026-09-26 from a script file with
	// a `print` on the line in front: `$(( sqrt( a , b c ) ))` is
	// `arithmetic syntax error` at run time in ksh93u+ and `unknown
	// function: sqrt` in zsh 5.9.2, and both print the line before it — so
	// the file is read either way, and the name is resolved before the
	// argument text is weighed at all.
	ArgumentsAreText bool
	// Within is the whole expression the call was written in, and Offset the
	// byte position of the call's first byte inside it — so `Within[Offset:]`
	// is the text from the name to the end of the expression.
	//
	// Both are here because the other shell with the construct words its two
	// sentences over extents *larger* than the call: the tail for a name it
	// does not know, and the expression entire, blanks and all, for a call
	// whose argument count is wrong. Measured 2026-09-13 on ksh93u+
	// 2012-08-01:
	//
	//	$(( 1 + nosuchmf(1) + 2 ))   nosuchmf(1) + 2 : unknown function
	//	$((nosuchmf(1)))             nosuchmf(1): unknown function
	//	$(( atan(1,2) ))              atan(1,2) : function has wrong number
	//	                             of arguments
	//
	// Recorded by the parser rather than reconstructed by the evaluator,
	// which cannot: a subscript is parsed as an expression of its own, so
	// `$(( x[nosuchmf(1)] ))` blames `nosuchmf(1)` and not the brackets
	// around it, and only the parser knows which source each tree came from.
	// Both are slices of that source and neither copies it.
	Within string
	Offset int
	Start  Pos
	Stop   Pos
}

ArithCall is `name(args)` inside an arithmetic expression: a *math function*, whose value is produced by running a shell function.

Two shells in the panel have the construct — zsh through `functions -M`, and ksh93 through a built-in table it needs no registration for — so the grammar is a dialect's, Dialect.ArithFunctionCall, and where it is off the `(` after a name is a leftover operator, which is the complaint bash, dash and ash make about `mf(5)`. ksh93 was in that list until 2026-09-12 and does not belong in it: `$(( sqrt(4) ))` is `2` there. See Dialect.ArithFunctionCall for the run.

The name is not resolved here and does not have to exist: an unregistered name is a runtime failure with its own sentence, not a parse error, which is measured in both columns — `$(( nosuchmf(1) ))` is `unknown function: nosuchmf` in zsh 5.9.2 and `nosuchmf(1) : unknown function` in ksh93u+, and never a syntax complaint in either.

The two word it over different extents, which is why the sentence cannot be written from one of them. zsh names the name; ksh93 names the expression from the first byte of the name to the *end of the expression*, so `$(( 1 + nosuchmf(1) + 2 ))` is `nosuchmf(1) + 2 : unknown function` and a subscript, being its own expression, is `$(( x[nosuchmf(1)] ))` → `nosuchmf(1): unknown function`. Measured 2026-09-12; see interp.Diagnostics.MathFunctionUnknown, whose second verb is that tail and which Within and Offset below are what answer.

func (*ArithCall) End

func (n *ArithCall) End() Pos

func (*ArithCall) Pos

func (n *ArithCall) Pos() Pos

type ArithCharCode

type ArithCharCode struct {
	// Op is `#`, `##` or `”`, kept because it decides how Char is read:
	// `$((##\n))` is a newline and `$((#\n))` the letter n. `”` is the
	// character *constant* rather than an operator — `$(( 'a' ))` — and reads
	// its character the way `##` does, escapes and all.
	Op string
	// Name is the parameter whose first character is taken, empty when the
	// operand is a character or when there is no operand at all. A `#` with
	// nothing it can read is zero rather than a refusal, measured.
	Name string
	// Char is the character the operand spells out, as written — `a`, `\n`,
	// `\x41`. The escapes are decoded where Op says they are, which is done
	// against the dialect's table rather than here.
	Char string
	// Subscripted records a `[…]` written after the name. The shell reads it
	// and then finds nothing under the whole of it, so `$((#a[1]))` is zero
	// however `${a[1]}` reads — measured, and a fact about that shell rather
	// than a rule anything derives.
	Subscripted bool
	Start       Pos
	Stop        Pos
}

ArithCharCode is the character-code operator: `#name` is the code of the first character of that parameter's value, and `##c` the code of the character written out.

Not a unary operator, because its operand is not an expression: `#b` reads the *parameter* b rather than b's arithmetic value, and `##a` reads a character where an expression would read a name. It is a primary, and the two spellings differ in how the operand is read rather than in what the result means.

func (*ArithCharCode) End

func (n *ArithCharCode) End() Pos

func (*ArithCharCode) Pos

func (n *ArithCharCode) Pos() Pos

type ArithCharacterEscapes added in v0.0.18

type ArithCharacterEscapes uint8

ArithPrecedencePolicy is the order the binary arithmetic operators bind in.

Two orders, and the second is not a quirk to be worked around: one shell documents both of them and ships an option — `c_precedences` — that picks between them, which is as explicit as a disagreement gets.

The two differ in where the shifts and the bitwise operators sit, and in nothing else. Everything from `&&` down and everything from `*` up is the same ladder either way. ArithCharacterEscapes is how far the one character after a character-code operator reaches when it is written as a `\C` or `\M` escape.

See Dialect.ArithCharacterEscapes for why this is keyed on the dialect.

const (
	// ArithCharacterEscapesWithoutCaretMeta has neither escape, so a
	// backslash before a `C` or an `M` claims that one character and no
	// more. The zero value, and what bash and dash hold: neither spells a
	// character-code operator at all, so neither ever reaches it.
	ArithCharacterEscapesWithoutCaretMeta ArithCharacterEscapes = iota

	// ArithCharacterEscapesMaskedCaretMeta is zsh's span: `\C` and `\M`
	// each take a separating `-` that may be left out and then one further
	// character, which may itself be an escape — so `\C-a`, `\Ca` and
	// `\M-\C-a` are each one operand. Measured 2026-09-26 on zsh 5.9.2
	// (aarch64-apple-darwin25.4.0), `-f`: `$(( ##\C-a ))` is 1, `$(( ##\M-a ))`
	// is 225 and `$(( ##\M-\C-a ))` is 129.
	ArithCharacterEscapesMaskedCaretMeta

	// ArithCharacterEscapesFoldedCaret is ksh93's, and it is a different
	// vocabulary rather than the same one spelled loosely: `\C` takes
	// exactly one further character with **no dash in the spelling**, and
	// the two characters `\M-` are a complete escape taking nothing at all
	// while a `\M` without the dash is no escape.
	//
	// Measured 2026-09-26 on ksh93u+ 2012-08-01, through the character
	// constant, which is the operator this dialect has: `$(( '\Ca' ))` and
	// `$(( '\CA' ))` are both 1, `$(( '\C-' ))` is 109 — the `-` folded up
	// and exclusive-ored with 0x40 — and `$(( '\C-a' ))`, `$(( '\M-x' ))`
	// and `$(( '\Mx' ))` are each an arithmetic syntax error, because the
	// escape ends before the quote does and a character is left standing
	// where an operator belongs.
	ArithCharacterEscapesFoldedCaret
)

func (ArithCharacterEscapes) String added in v0.0.18

func (p ArithCharacterEscapes) String() string

type ArithCmdClause

type ArithCmdClause struct {
	// Expr is the source text, kept because it is what a diagnostic quotes.
	Expr string
	// Parsed is the expression tree.
	Parsed ArithExpr
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

ArithCmdClause is `(( expr ))` used as a command.

It exits 0 when the expression is non-zero, which is the reverse of the usual convention and is unanimous across the panel.

func (*ArithCmdClause) End

func (c *ArithCmdClause) End() Pos

func (*ArithCmdClause) Pos

func (c *ArithCmdClause) Pos() Pos

type ArithCond

type ArithCond struct {
	Cond, Then, Else ArithExpr
}

ArithCond is `c ? a : b`.

func (*ArithCond) End

func (n *ArithCond) End() Pos

func (*ArithCond) Pos

func (n *ArithCond) Pos() Pos

type ArithConditionalBranchLevel added in v0.0.18

type ArithConditionalBranchLevel uint8

ArithConditionalBranchLevel is how loose an expression one branch of `c ? t : e` may be. The three values are the three rungs of the ladder a dialect can stop a branch at, from the loosest down.

See Dialect.ArithConditionalThenLevel for the panel's measurement and for why the two branches need a field each.

const (
	// ArithConditionalBranchSequence reads a branch at the loosest level
	// there is, so the sequence operator may stand in it where the dialect
	// has one at all. C's `expression`, and the zero value — which is the
	// same reading as the level below it in a dialect without the operator,
	// since there is then nothing between the two.
	ArithConditionalBranchSequence ArithConditionalBranchLevel = iota

	// ArithConditionalBranchAssignment reads a branch at the assignment
	// level, so a bare store may begin one but a comma may not stand in it.
	ArithConditionalBranchAssignment

	// ArithConditionalBranchConditional reads a branch at the conditional
	// level, so a bare store cannot begin one. It is a level and not a ban:
	// a parenthesized store in the same position is taken, and what a bare
	// one earns is the refusal an assignment to any other non-place earns.
	ArithConditionalBranchConditional
)

func (ArithConditionalBranchLevel) String added in v0.0.18

type ArithDoubleQuotePolicy

type ArithDoubleQuotePolicy int

ArithDoubleQuotePolicy is what a `"` inside an arithmetic expression is.

Three readings rather than two, and the third is measured rather than invented: the four *columns* that read past a double quote at all do not agree about one standing in the middle of a token. `$(( 1"0" ))` is 10 in bash 5.3 and in the same build named `sh` — the quotes are gone before anything reads the text, so the two digits are one number — and an arithmetic syntax error in ksh93u+ and zsh 5.9.2, where the quote ends the number and leaves a second operand behind. Measured 2026-09-10 (#1223).

The other three columns take none of the three readings and refuse the quote outright, which is the first constant below: bash 3.2.57, dash and BusyBox ash, the last re-measured 2026-09-21 in the pinned image, where both `$(( "1" + 1 ))` and `$(( 1"0" ))` are `arithmetic syntax error`.

const (
	// ArithDoubleQuoteRefused is no part of any token: the quote is where a
	// value should be, and the expression is refused for wanting an operand.
	// bash 3.2.57, dash and BusyBox ash, and the core, which refuses what
	// the panel disagrees about.
	ArithDoubleQuoteRefused ArithDoubleQuotePolicy = iota
	// ArithDoubleQuoteSkipped passes over the byte wherever a token may
	// begin, so `"1" + 1` is 2 and `"n" + 1` reads the parameter n — but a
	// quote inside a token still ends it, and `1"0"` is two operands running
	// together. ksh93 and zsh.
	ArithDoubleQuoteSkipped
	// ArithDoubleQuoteRemoved takes the byte out of the text before the
	// expression is read, so `1"0"` is the number 10 and `n"a"me` is the
	// parameter `name`. bash 5.3, and the same build invoked as `sh`.
	//
	// It is also what that shell quotes back when the expression fails —
	// `$(( "1" "2" ))` is reported against `1 2` there — which falls out of
	// removing the bytes rather than having to be arranged.
	ArithDoubleQuoteRemoved
)

func (ArithDoubleQuotePolicy) String

func (p ArithDoubleQuotePolicy) String() string

type ArithExpr

type ArithExpr interface {
	Node
	// contains filtered or unexported methods
}

ArithExpr is a node in an arithmetic expression.

type ArithIndex

type ArithIndex struct {
	Name string
	// Index is the subscript read as an expression, and nil when the text
	// will not read as one — `m[.accept-line]`, whose brackets hold a key and
	// not a sum. That is not a parse failure, because whether it is a key or
	// an expression depends on an attribute of the name, which the parser
	// cannot see: see Sub, and subscript. Nil also when the brackets held
	// nothing: see Empty.
	Index ArithExpr
	// Sub is the subscript exactly as it was written, brackets excluded.
	//
	// Kept beside the parsed expression because a subscript is only an
	// expression when the name is an indexed array. On an associative one it
	// is a key — `m[k]` is the element stored under the two characters, not
	// the one at whatever `k` evaluates to — and which of the two a name is
	// cannot be known until it runs. Measured unanimous in the three shells
	// with the attribute, whitespace included: `m[ k ]` is a different key
	// from `m[k]`, in an expression exactly as in `${m[ k ]}`.
	Sub string
	// SubMarked is Sub with the value marks still on it, so a reader that
	// has to tell a quote the *script* wrote from one a value carried can.
	//
	// Sub is what a diagnostic quotes back and what an expression is read
	// out of, and neither of those wants a mark in it. An associative
	// array's key is the one reading that does: quote removal is performed
	// over a subscript there, and it has to leave a value's own quote
	// characters alone. Measured 2026-09-16 from a script file with
	// `typeset -A a; a["'q'"]=21; a[q]=22; k="'q'"`, `$(( a[$k] ))` is 21 in
	// bash 5.3.20 and in ksh93u+ 2012-08-01 — the value's quotes are two
	// characters of the key and not a quotation — where the same two
	// characters written in the source name the element under `q`.
	//
	// Empty when the brackets held nothing, exactly as Sub is. See
	// syntax.ArithValueMark for what the marks are and who puts them on.
	SubMarked string
	// Empty says the brackets were written with nothing at all between
	// them — `a[]` — which is what `a[$w]` becomes when `$w` holds the empty
	// string, because an arithmetic expansion substitutes its parameters
	// before the expression is read. Index is nil and Sub is empty then, and
	// neither of those alone would say it: a node with no subscript at all is
	// an ArithVar and never reaches here.
	//
	// Accepted by the grammar rather than refused, because every shell in the
	// panel with subscripts parses it and answers at run time — bash reports
	// a bad subscript and carries on with zero, ksh93 is silently zero, zsh
	// makes it depend on whether the name exists — and three run-time answers
	// are a semantics axis and not a parse error. It is refused in one
	// position still: an *assignment target*, `(( a[] = 9 ))`, where the same
	// three shells part again and part differently.
	Empty bool
	// Flags is the parenthesized flag group the subscript opened with, where
	// the dialect has them — `$(( a[(r)20] ))` is the element whose value is
	// `20`, exactly as `${a[(r)20]}` is.
	//
	// Index is nil when a group is present and Sub still holds the whole
	// subscript as written, group included, so a diagnostic quotes back what
	// the source said. The operand behind the group is Flags.Arg, and it is
	// a *literal* word: an arithmetic expression has already been expanded
	// once by the time it is parsed, so a `$` left in it is a `$` and not
	// the start of anything.
	//
	// The group is carried here rather than resolved in the parser for the
	// reason Sub is: what a group selects depends on whether the name is an
	// association, which the parser cannot see.
	Flags *SubscriptFlags
	// UnclosedQuote says the brackets were found only once the scan gave up
	// on a quotation that never closed.
	//
	// A subscript's brackets are scanned *through* quotations where the
	// dialect has them, so a `]` written inside quotes is a character of a
	// key and closes nothing — see Dialect.ArithSubscriptQuoting. A
	// quotation that never closes is not one, and the brackets are then
	// found by scanning again with the quoting ignored, which is how
	// `a[q'r]` comes to name the three-character key it looks like.
	//
	// The panel parts over whether that second scan is allowed to produce a
	// subscript at all, so the fact that it was needed is recorded here
	// rather than decided here: the parser cannot see the option a session
	// carries and cannot see which dialect's answer is wanted. See
	// interp.Semantics.ArithSubscriptQuotationMustClose.
	//
	// It is only ever reachable from text that arrived **already expanded**.
	// A quotation a script writes inside an arithmetic subscript is closed
	// before the word is lexed or the word itself is unterminated, so
	// `(( a[q'r]++ ))` written out is `unexpected EOF while looking for
	// matching \`”` in bash 5.3.20 and `” unmatched` in ksh93u+ — measured
	// 2026-09-20 — and never reaches a subscript scan at all.
	UnclosedQuote bool
	Start         Pos
	Stop          Pos
}

ArithAssign is `x = e` and its compound forms.

It is a separate node rather than a binary operator because its effect outlives the expression — `$((x=5))` leaves x set — which combined with short-circuiting makes evaluation order part of the specification. ArithIndex is `name[expr]` inside an arithmetic expression.

The subscript is an expression of its own — `a[i+1]` — and neither the name nor the subscript is written with a `$`, which is what makes this a node rather than an expansion: `${a[1]}` is substituted before the expression is read, and `a[1]` is read as part of it.

func (*ArithIndex) End

func (n *ArithIndex) End() Pos

func (*ArithIndex) Pos

func (n *ArithIndex) Pos() Pos

type ArithNum

type ArithNum struct {
	Text string
	// Tail is the expression's text from this numeral's first byte to the
	// end of it, so it is Text plus whatever was written after — the spaces
	// before a closing `))` included.
	//
	// Kept for one diagnostic, the way [ArithCall].Within is: the shell that
	// reads only part of a numeral too large for the machine word quotes the
	// *tail* back rather than the digits it read. Measured 2026-09-16 on zsh
	// 5.9.2, which is the only column that says anything at all:
	//
	//	$(( 9223372036854775808 + 1 ))   … digits: 9223372036854775808 + 1
	//	$(( 9223372036854775808 ))       … digits: 9223372036854775808
	//	$((9223372036854775808))         … digits: 9223372036854775808
	//
	// Recorded by the parser rather than found by the evaluator, which
	// cannot: a search for the digits blames the first of two identical
	// numerals twice, and the trailing blank in the middle row is exactly
	// the kind of difference a search cannot see.
	Tail  string
	Start Pos
	Stop  Pos
}

ArithNum is a numeric literal, kept as written.

The text is not converted here, because what it means is a dialect question: a leading zero is octal everywhere but zsh, where `0100` is one hundred rather than sixty-four. Converting at parse time would bake one answer into the tree.

func (*ArithNum) End

func (n *ArithNum) End() Pos

func (*ArithNum) Pos

func (n *ArithNum) Pos() Pos

type ArithOutput

type ArithOutput struct {
	// Base is the base the result is written in, and Based says one was
	// written at all: `[#_]` groups decimal digits and names no base, which
	// is not the same as naming zero — `[#0]` is a base out of range and is
	// refused. Two fields rather than a sentinel because the refusal has to
	// quote the number back.
	//
	// Whether a base is one the dialect can spell is not decided here: the
	// range is a property of the alphabet a shell renders in, so it is
	// checked where that alphabet is, and a base out of range is a *runtime*
	// failure rather than a parse one.
	Base  int
	Based bool
	// Prefixed says the `base#` is written in front of the digits, which is
	// the single `#` spelling: `[#16] 255` is `16#FF` and `[##16] 255` is
	// `FF`. Base ten writes no prefix under either spelling.
	Prefixed bool
	// Group is how many digits stand between `_` separators, and 0 for no
	// grouping. A bare `_` with no number after it is three.
	Group int
	// Text is the specifier as written, brackets included, for a diagnostic
	// that quotes it back.
	Text string
	// X is the expression, and nil when the text held nothing but the
	// specifier: `$(( [#16] ))` is `16#0`, not a failure.
	X     ArithExpr
	Start Pos
	Stop  Pos
}

ArithOutput is the bracketed output-format specifier — `[#16]`, `[##16]`, `[#16_4]`, `[#_]` — which says the base the expression's *result* is written in and how its digits are grouped.

One node, at the top of the tree, however many specifiers the text held and wherever they stood. That is not a simplification: the construct is lexical in the dialect that has it, so it takes effect from a branch that is never evaluated and from a position after the value it formats, and the textually last one wins. A node where the specifier was written would answer all three of those wrongly. See Dialect.ArithOutputFormat for the measurements.

The value of the node is the value of X, unchanged — the format is a side-channel to whoever writes the answer down and is not arithmetic. It reaches two of them: the text an arithmetic expansion produces, and the text an assignment inside the expression stores.

func (*ArithOutput) End

func (n *ArithOutput) End() Pos

func (*ArithOutput) Pos

func (n *ArithOutput) Pos() Pos

type ArithPrecedencePolicy added in v0.0.3

type ArithPrecedencePolicy int
const (
	// ArithPrecedenceAsInC is ISO C's order, which POSIX defers to and which
	// five of the six panel columns use. Tightest first, after the unary
	// operators:
	//
	//	**                     exponentiation
	//	* / %                  multiplication
	//	+ -                    addition
	//	<< >>                  shifts
	//	< > <= >=              comparison
	//	== !=                  equality
	//	&                      bitwise and
	//	^                      bitwise xor
	//	|                      bitwise or
	//	&&                     logical and
	//	||                     logical or
	ArithPrecedenceAsInC ArithPrecedencePolicy = iota

	// ArithPrecedenceShiftsAndBitwiseBindTighter is the other order, which
	// one shell calls its native mode. The shifts move to the *tightest*
	// binary level and the three bitwise operators move above `**`:
	//
	//	<< >>                  shifts
	//	&                      bitwise and
	//	^                      bitwise xor
	//	|                      bitwise or
	//	**                     exponentiation
	//	* / %                  multiplication
	//	+ -                    addition
	//	< > <= >=              comparison
	//	== !=                  equality
	//	&&                     logical and
	//	||                     logical or
	//
	// Note what that does to `**`, which is the part a reading of the four
	// rows in #2883 would miss: exponentiation is *looser* than the bitwise
	// operators here, so `2 ** 1 | 3` is `2 ** (1 | 3)` and answers 8 where
	// C's order answers 3. The relative order of the three bitwise
	// operators, and of everything from `*` down, is unchanged.
	//
	// The associativity is not part of this. `**` is right-associative and
	// everything else left-associative under both orders, measured: `2 ** 3
	// ** 2` is 512 in every column that has the operator.
	ArithPrecedenceShiftsAndBitwiseBindTighter
)

func (ArithPrecedencePolicy) String added in v0.0.3

func (p ArithPrecedencePolicy) String() string

type ArithUnary

type ArithUnary struct {
	Op string // + - ~ ! ++ --
	// Postfix distinguishes `x++` from `++x`, which differ in what they
	// evaluate to even though they have the same effect.
	Postfix bool
	X       ArithExpr
	Start   Pos
}

ArithUnary is a prefix or postfix operator.

func (*ArithUnary) End

func (n *ArithUnary) End() Pos

func (*ArithUnary) Pos

func (n *ArithUnary) Pos() Pos

type ArithVar

type ArithVar struct {
	Name string
	// Special is a special parameter standing as an operand — `?`, `$` or
	// `#` — which is a value and not a name a script may assign. See
	// Dialect.ArithSpecialParameterOperands.
	Special bool
	Start   Pos
	Stop    Pos
}

ArithVar is a bare name, which inside arithmetic is a variable reference. An unset one is zero rather than an error.

func (*ArithVar) End

func (n *ArithVar) End() Pos

func (*ArithVar) Pos

func (n *ArithVar) Pos() Pos

type ArrayElem added in v0.0.10

type ArrayElem struct {
	// Word is the element where it is an ordinary word, which is every
	// element in every dialect but one, and the `[sub]=` head where the
	// element names where its value goes.
	Word *Word
	// Nested is the literal standing where an element goes, and nil where the
	// element is a word. IsArray is true on it and Name is empty; its own
	// Elems may nest again, to any depth — `a=( ( (1 2) (3) ) (4) )` is
	// three levels and reads back as one.
	//
	// An [Assign] rather than a list of its own so that the inner literal is
	// the same node the outer one is, positions and all, and so that the
	// reading that tells an array literal from a compound variable's body has
	// one place to record its answer. See [Dialect.NestedArrayLiteral].
	Nested *Assign
}

ArrayElem is one element of `name=( … )`: a word, or a literal of its own.

A struct rather than a bare word because one dialect lets an element *be* a literal — `a=( (1 2) (3 4) )` is a two-element array whose elements are arrays, which is that shell's multi-dimensional array. A parallel list beside Elems would have been cheaper and is the shape this tree keeps getting wrong: a reader that forgets a second list compiles and is silently wrong, where a reader that has not been taught about a new field does not compile at all.

Both are set where the element names a subscript *and* the value it names is a literal — `a=( [0]=(1 2) )`, where the word is the `[0]=` head and the literal is what goes under the key.

func WordElem added in v0.0.10

func WordElem(w *Word) *ArrayElem

WordElem is one element that is an ordinary word, which is how every element list that cannot nest is built.

func WordElems added in v0.0.10

func WordElems(ws []*Word) []*ArrayElem

WordElems is a whole list of them, for the callers that have words already.

func (*ArrayElem) End added in v0.0.10

func (e *ArrayElem) End() Pos

func (*ArrayElem) Pos added in v0.0.10

func (e *ArrayElem) Pos() Pos

type ArraySemicolon

type ArraySemicolon uint8

ArraySemicolon is how far a dialect will take a `;` inside an array literal's parentheses. See Dialect.SemicolonInAnArrayLiteral.

A place rather than a bool, for the reason SeparatorSkip is one: the two shells that take a `;` there draw it in two different sets, and a single yes/no could not be given a value for either without accepting lines the other refuses.

Measured 2026-09-12, `env -i PATH=/usr/bin:/bin` with a scratch HOME and ZDOTDIR, `-n` over a script file and then a run printing `${#a[@]}` and the elements. dash and BusyBox ash have no array literal at all, so the `(` is already their error and the question does not reach them.

probe            bash 5.3 / 3.2 / as sh   ksh93            zsh 5.9.2
a=( x; )         `;' unexpected           runs, 1 element  runs, 1 element
a=( x y; )       `;' unexpected           runs, 2 elements runs, 2 elements
a=( x ⏎ ; )      `;' unexpected           runs             runs
a=( x; ⏎ )       `;' unexpected           runs             runs
a=( x; y )       `;' unexpected           `y' unexpected   runs, 2 elements
a=( x; y; )      `;' unexpected           `y' unexpected   runs, 2 elements
a=( x; ⏎ y )     `;' unexpected           `y' unexpected   runs, 2 elements
a=( ; )          `;' unexpected           `;' unexpected   runs, 0 elements
a=( x; ; )       `;' unexpected           `;' unexpected   runs, 1 element
a=( x;; y )      `;;' unexpected          `;;' unexpected  `;;' error
a=( x & )        `&' unexpected           `&' unexpected   `&' error
a=( x && y )     `&&' unexpected          `&&' unexpected  `&&' error

The last two rows are what say the `;` is specifically a separator rather than the parser being lenient about operators, and the `;;` row says the two-character token stays its own token in the shell that takes one.

const (
	// NoSemicolonInAnArrayLiteral is the core answer: a `;` between the
	// parentheses is a syntax error. Every bash column, and the two shells
	// with no array literal never reach the question.
	NoSemicolonInAnArrayLiteral ArraySemicolon = iota

	// OneSemicolonEndsTheArrayElements takes a single `;` after the last
	// element, where nothing but newlines and the closing `)` may follow it.
	// ksh93.
	//
	// It is a terminator and not a separator, which is the correction the
	// measurement above made to the filing: `a=( x; y )` is “ `y' unexpected “
	// there, so the `;` does not stand between two elements. It also needs an
	// element in front of it — `a=( ; )` is “ `;' unexpected “ — and it may
	// be written once — `a=( x; ; )` is “ `;' unexpected “ on the second.
	OneSemicolonEndsTheArrayElements

	// SemicolonSeparatesArrayElementsLikeANewline takes as many as are
	// written, anywhere between the parentheses, exactly where a newline
	// already stands. zsh 5.9.2, where `a=( ; )` is the empty array and
	// `a=( x; y )` holds two elements.
	SemicolonSeparatesArrayElementsLikeANewline
)

type Assign

type Assign struct {
	// Operand marks an assignment written *after* the command word rather
	// than in front of it — `local a=(x y)` rather than `a=1 cmd`. The two
	// are different things wearing one syntax: a prefix assignment is the
	// command's environment, and this one is an argument to a utility that
	// takes assignments.
	//
	// Only the array form is ever parsed this way. `local a=1` stays an
	// ordinary word, which is the path every shell's scalar case has always
	// taken here and which expands by its own rules.
	Operand bool

	Name  string
	Value *Word // nil for a bare `name=`
	// Elems is `name=( … )`, and IsArray distinguishes an empty array from a
	// bare `name=` — `a=()` and `a=` are different states, exactly as an
	// absent `for` list differs from an empty one.
	Elems   []*ArrayElem
	IsArray bool
	// Members is the body of a **compound variable** literal — `c=(a=1 b=2)`,
	// whose parentheses hold a list of declarations rather than a list of
	// elements. IsArray is true alongside it, because the two readings share
	// one spelling and only the first word inside tells them apart; see
	// [Dialect.CompoundVariableDeclarators] for what decides.
	//
	// Each item is the simple command it was written as, so `a=1 b=2` is one
	// item carrying two assignments and `typeset -i n=5` is one carrying the
	// declaration word, its options, and the assignment it makes. Nothing
	// else may stand there: a body item whose command word is not a
	// declarator is a syntax error while reading, which is the shell's own
	// answer.
	//
	// Nil where the literal was read as an array, which is every dialect but
	// one and every literal whose first word is not an assignment.
	Members []*SimpleCmd
	// Index is the subscript of `name[i]=value`, nil otherwise.
	Index *Word
	// IndexFlags is the parenthesized flag group that subscript opened with,
	// where the dialect has them — `a[(r)y]=Q` replaces the element whose
	// value is `y`. The operand behind the group is IndexFlags.Arg, and Index
	// still holds the whole subscript as written, so a reader that does not
	// know about groups sees what the source said.
	IndexFlags *SubscriptFlags
	// IndexRange is the subscript read as a **pair** whose ends carry flag
	// groups of their own — `a[(r)q,(r)r]=v` replaces the span from the
	// element holding `q` to the one holding `r` — and nil where neither end
	// opens with one. The same reading ParamExpr.IndexRange is on the read
	// side, asked of the same text by the same function; an unflagged pair
	// stays the arithmetic reader's to split, as it always was.
	IndexRange *SubscriptRange
	// Member is the dotted member path written after the subscript's closing
	// bracket — `.p` in `a[1].p=9`, `.q.r` in `a[1].q.r=4` — with its leading
	// dot and empty where none was written.
	//
	// It is the one place a subscript and a dotted name meet. A member of an
	// *unsubscripted* compound needs nothing here, because `.` is a name byte
	// where the dialect has the construct and the whole of `c.p` is the Name;
	// after a `]` there is no name left to run on, so the member has to be
	// carried beside the subscript and joined to it once the subscript has a
	// value. See [Dialect.DottedName], and [ParamExpr.Member], which is the
	// same field on the reading side.
	Member string
	// EmptySubscript says brackets were written after the name with nothing
	// at all between them — `a[]=v` — which Index cannot record: the
	// subscript came to no spans, so the node is otherwise identical to the
	// one a bare `a=v` produces.
	//
	// It is the *written* brackets and not a subscript that came out empty.
	// `a[$i]=v` with an empty `$i` is a subscript whose text expanded to
	// nothing, which is the arithmetic reader's question and a different
	// answer in every column; `a[""]=v` is a subscript holding the empty
	// string, which is a key in one dialect and zero in another. Only
	// brackets the source wrote empty set this.
	//
	// Every shell that has arrays refuses the construct, and they do not
	// agree on how — see interp.Semantics.EmptySubscriptToAnAssignment and
	// [Dialect.EmptyAssignSubscriptIsASyntaxError], which is the column that
	// refuses it while reading (#3949).
	EmptySubscript bool
	// IndexText is the subscript as it was written, before any expansion —
	// the text between the brackets, and empty where none were.
	//
	// A diagnostic that quotes a subscript back quotes what was *typed*:
	// `i=-9; a[$i]=q` is `a[$i]: bad array subscript` in the column that
	// reports it, not `a[-9]`. By the time that is known the word has been
	// expanded and nothing left in the tree remembers the spelling — the
	// same reason Redirect.Text exists, and the same shape.
	//
	// Only what stands between the brackets, so a reader adds the name and
	// the brackets itself, exactly as it does for the expanded reading.
	IndexText string
	// Leading holds the subscripts written *before* Index, in written order,
	// where the grammar lets an assignment's name carry several:
	// `a[1][2]=v` carries `1` here and `2` in Index. Nil is one subscript,
	// which is every assignment in every other dialect.
	//
	// Index is the last one and not the first, exactly as it is for
	// [ParamExpr.Leading], and for the same reason: every question anything
	// asks about a subscripted assignment — whether it is a range, whether
	// it names a key, what a refusal quotes back — is a question about the
	// *final* subscript, because that is the one the value lands under. The
	// leading ones only say what it lands *in*.
	//
	// See [Dialect.ChainedAssignSubscript].
	Leading []LeadingIndex
	// Append is `name+=value`, which adds to what is there rather than
	// replacing it — and adds to the *end* of an array rather than to its
	// first element.
	Append bool
	Start  Pos
	Stop   Pos
}

Assign is `name=value` in a command prefix or on its own.

func (*Assign) End

func (a *Assign) End() Pos

func (*Assign) Pos

func (a *Assign) Pos() Pos

func (*Assign) Subscript added in v0.0.1

func (a *Assign) Subscript() *Word

Subscript is the same accessor for an assignment's subscript: the operand behind a flag group where there is one, and the whole subscript where there is not.

The pair rather than one of them, because the two sides of `=` have to read a subscript by the same rule or the same text names one element on the way in and another on the way out — `m[(e)k]=Z` stored under the three characters `(e)k` while `${m[(e)k]}` read the key `k`, which is a wrong answer at status 0.

type BareNegationReach

type BareNegationReach uint8

BareNegationReach is how far a `!` written with no pipeline after it may stand. See Dialect.BareNegationReach.

A place rather than a bool, for the reason SeparatorSkip is one: the three shells that take a bare `!` draw its boundary in three different sets, and a single yes/no could not be given a value for any of them without accepting lines another refuses.

Measured 2026-09-12, `env -i PATH=/usr/bin:/bin` with a scratch HOME, over `-c` and a script file alike. `st=` is what `echo "st=$?"` printed after it.

after the `!`      dash   bash 3.2   bash 5.3   bash as sh   ksh93   zsh
`;`                error  error      st=1       st=1         st=1    st=1
a newline          error  error      st=1       st=1         st=1    st=1
the end of input   error  error      st=1       st=1         st=1    st=1
`&`                error  error      st=0       st=0         runs    error
`)` of a subshell  error  error      error      error        st=1    st=1
`}` of a group     error  error      error      error        runs    runs
`;;` of a case arm error  error      error      error        st=1    st=1
`&&`               error  error      error      error        runs    runs
`||`               error  error      error      error        runs    runs
`|`                error  error      error      error        error   error

bash-as-`sh` follows bash 5.3 and not bash 3.2, which #948 suspected might be POSIX mode rather than the version. It is the version: `bash --posix -c '!'` answers 1 on the same binary.

const (
	// NoBareNegation is the core answer: a `!` needs a pipeline after it.
	// dash and bash 3.2.
	NoBareNegation BareNegationReach = iota

	// BareNegationBeforeATerminator takes a `!` that a statement terminator
	// or the end of input follows — `;`, a newline, `&`, EOF — and nothing
	// else. bash 5.3 and that binary as `sh`: `{ ! ; echo "st=$?"; }` prints
	// `st=1` there where `{ ! }`, `( ! )` and `! && echo two` are all
	// refused.
	BareNegationBeforeATerminator

	// BareNegationWhereAListEnds takes it where the *list* ends instead —
	// a closer, a `case` terminator, or an and-or operator whose right-hand
	// side is where the `!` stood — as well as before `;`, a newline and the
	// end of input. Not before `&`, which is the one row that separates this
	// from the value below: zsh refuses `! & echo x` and takes `( ! )`,
	// where bash 5.3 does the opposite of both.
	//
	// The `&` exception is the same boundary [Dialect.OpenEndedAndOr] has in
	// that shell, where `true || & b` is a parse error and `( true || )`
	// runs.
	BareNegationWhereAListEnds

	// BareNegationAtEitherPlace takes both sets, which is ksh93: it is the
	// union rather than a third rule, and every row above says so.
	BareNegationAtEitherPlace
)

type BinaryExpr

type BinaryExpr struct {
	X     Expr
	Op    Kind // TokAndAnd or TokOrOr
	OpPos Pos
	Y     Expr
}

BinaryExpr is `x && y` or `x || y`.

The two operators share one precedence level and associate left, so `a || b && c` parses as `(a || b) && c`. Giving `&&` a tighter binding is C's rule and produces no output for `true || echo A && echo B`, which every shell prints B for.

func (*BinaryExpr) End

func (b *BinaryExpr) End() Pos

func (*BinaryExpr) Pos

func (b *BinaryExpr) Pos() Pos

type BraceProgramBodyEnd added in v0.0.2

type BraceProgramBodyEnd uint8

BraceProgramBodyEnd says where the body of `${ cmd;}` stops.

The body holds a command list, and the two shells that have the construct disagree about which `}` ends it. Measured 2026-09-13 over `-c`, ksh93u+ 2012-08-01 and bash 5.3.15 — the four columns without the construct are not in the table because they refuse every row of it:

written                      ksh93            bash 5.3
${ echo a}b;}                a}b              a}b
${ echo hi}                  `{' unmatched    unexpected EOF
${ echo } ;}                 `}' unexpected   }
${ echo a } b;}              `}' unexpected   a } b
A${ echo B }C                ABC              unexpected EOF
${ echo {a,b};}              `{' unmatched    a b
${ echo a;}                  a                a

Both readings refuse `${ echo hi}`, which is the row that matters most: a body with no terminator in front of the brace is not a closed substitution in either column, and reading it as one runs a command the author never wrote (#2711). The rest of the table is where the two part company (#2724).

The values carry the two rules; see BraceProgramBodyEnd's constants.

const (
	// BraceProgramBodyEndsWhereAListEnds ends the body exactly where the
	// list of a `{ …; }` group ends: at the reserved word `}`, which stands
	// only where a command may begin. bash 5.3, and the core, because it is
	// the rule the construct's own braces already have everywhere else in
	// the grammar.
	//
	// It is why `echo }` passes a literal brace to `echo` from inside a body
	// — rows three and four above — and why a body with no terminator runs
	// off the end of the input rather than closing at the first `}` it meets.
	BraceProgramBodyEndsWhereAListEnds BraceProgramBodyEnd = iota

	// BraceProgramBodyEndsAtATokenStart ends it at a `}` that *begins a
	// token*, which is argument position as well as command position, and
	// lets a `{` that begins one open a nested level that a token-start `}`
	// closes. ksh93u+.
	//
	// Rows three and four are the visible consequence: a `}` written as an
	// argument ends the body there rather than reaching `echo`, so what is
	// left over is a stray brace and the line is refused. Row six is the
	// other half — `{a,b}` opens a level whose `}` is mid-word and closes
	// nothing, so the body runs past the end of the input.
	//
	// Quotes and the older substitution shield a brace and the newer one
	// does not, which is measured rather than assumed: a body whose `}` sits
	// inside a backquoted substitution keeps it — `}x` comes back — and the
	// same body written `${ echo $(echo x)}` is “ `{' unmatched “, so the
	// `)` that closes a `$( )` leaves the cursor inside a word while the
	// `)` of a subshell — `${ (echo q)}` — begins the next token.
	BraceProgramBodyEndsAtATokenStart
)

type BraceQuotePolicy

type BraceQuotePolicy int

BraceQuotePolicy is what a single quote written inside a double-quoted `${ … }` does to the scan for the closing brace.

Three readings rather than two, because the panel splits three ways and the line runs through the *operand* rather than through the shell. See Dialect.QuoteProtectsTheClosingBrace for the measurement.

const (
	// BraceQuoteProtectsAPatternOnly reads the quote as quoting in a `#`,
	// `##`, `%`, `%%` or `/` operand and as an ordinary character in the
	// word operands, which is where a `}` it stands in front of closes the
	// expansion. Five of the seven columns, and the core: a pattern's quotes
	// are its own, and a word operand's belong to the double quote around
	// the whole expansion, where a single quote quotes nothing.
	BraceQuoteProtectsAPatternOnly BraceQuotePolicy = iota
	// BraceQuoteProtectsNothing reads it as an ordinary character in every
	// operand, so the expansion always ends at the first `}`. zsh 5.9.2.
	BraceQuoteProtectsNothing
	// BraceQuoteProtectsEveryOperand reads it as quoting wherever it is
	// written, so `"${v-'}'}"` runs to the second brace. bash 5.3.15 and
	// 3.2.57 — and not the same build invoked as `sh`, which answers with
	// the other five.
	BraceQuoteProtectsEveryOperand
)

func (BraceQuotePolicy) String

func (p BraceQuotePolicy) String() string

type BraceQuotePosixMove added in v0.0.2

type BraceQuotePosixMove uint8

BraceQuotePosixMove is where POSIX mode puts Dialect.QuoteProtectsTheClosingBrace.

Four values rather than three, because the first of them is *unmoved* and has to be the zero one: BraceQuotePolicy's zero is a real reading, and every dialect invoked as `sh` enters POSIX mode, so a plain second BraceQuotePolicy field would hand the standard's answer to a shell that never took a position on the question. See Dialect.QuoteProtectsTheClosingBraceInPosixMode for the panel.

const (
	// BraceQuoteUnmovedInPosixMode leaves the dialect's own reading in place.
	// zsh, whose `posixbuiltins` and whose `sh` name both leave the axis
	// alone, and the three shells with no POSIX mode to move it with.
	BraceQuoteUnmovedInPosixMode BraceQuotePosixMove = iota
	// BraceQuoteMovesToAPatternOnly is bash: the mode takes the word
	// operand's quote away and leaves a pattern operand's where it was.
	BraceQuoteMovesToAPatternOnly
	// BraceQuoteMovesToNothing and BraceQuoteMovesToEveryOperand are the
	// other two destinations. Nothing in the panel takes either, and they are
	// declared because the axis they move is a three-valued one: a move that
	// could only ever name one of the three readings would be recording the
	// shell that happens to have it rather than the question.
	BraceQuoteMovesToNothing
	BraceQuoteMovesToEveryOperand
)

func (BraceQuotePosixMove) Policy added in v0.0.2

Policy is the reading this move names, or base where it names none.

func (BraceQuotePosixMove) String added in v0.0.2

func (m BraceQuotePosixMove) String() string

type CarriedHeredoc added in v0.0.10

type CarriedHeredoc struct {
	// At is where the substitution opened, which is what matches this to a
	// span at expansion time — a position rather than a pointer, for the
	// reason [File.Substitutions] holds spans rather than pointers.
	At Pos
	// Redirs are the operators, in the order the text opened them, which is
	// the order their bodies follow the enclosing command in.
	Redirs []*Redirect
}

CarriedHeredoc is one substitution's carried here-document operators.

The elements are pointers into a tree nobody keeps, and deliberately: a body lands in `Redirect.Heredoc` at the newline that ends the enclosing command, which is after the span has been copied into the word holding it, and reading through the pointer is what makes the hand-off work at all. Nothing here writes to them.

type CaseBraceSpelling

type CaseBraceSpelling uint8

CaseBraceSpelling is how a dialect writes a `case` header's second spelling, `case x { … }`. See Dialect.CaseBraceBody, where the rows are.

A spelling rather than a bool because the two shells that have the construct disagree about whether the opener and the closer are paired, and a single yes/no could not be given a value for either without accepting lines the other refuses.

const (
	// NoCaseBraceBody is the core answer: a `case` header is `in` and the
	// clause ends at `esac`. dash and every bash column.
	NoCaseBraceBody CaseBraceSpelling = iota

	// CaseBraceBodyPairsWithItsOpener takes `case x { … }` and `case x in …
	// esac` and neither of the mixtures. ksh93u+, where `case x { … esac`
	// and `case x in … }` are both “ `case' unmatched “.
	CaseBraceBodyPairsWithItsOpener

	// CaseBraceBodyMixesWithTheKeyword takes all four combinations: either
	// opener closes with either word. zsh 5.9.2.
	CaseBraceBodyMixesWithTheKeyword
)

type CaseClause

type CaseClause struct {
	Word  *Word
	Items []*CaseItem
	// Header is `case $v in` as written. See ForClause.Header.
	Header string
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

CaseClause is `case word in … esac`.

func (*CaseClause) End

func (c *CaseClause) End() Pos

func (*CaseClause) Pos

func (c *CaseClause) Pos() Pos

type CaseItem

type CaseItem struct {
	Patterns []*Word
	Body     []*Stmt
	// Term is TokDSemi, TokSemiAmp, TokDSemiAmp or TokSemiPipe. They are
	// separate operators rather than one "case extension": `;&` is core,
	// `;;&` is bash only, and `;|` is zsh's spelling of `;;&` — kept as its
	// own Kind because the two spellings are mutually exclusive, so a tree
	// that folded them together could not be printed back in either shell.
	Term    Kind
	TermPos Pos
	Start   Pos
	// Grouped says the arm's list was written inside its parenthesis in a
	// dialect that reads that list as one pattern, which is how it is
	// listed: zsh 5.9.2 writes `( a | b )` back as `(a|b)` and `c | d)` as
	// `(c | d)`, and under `emulate sh`, where the parenthesis is only the
	// arm's, `(one|two)` as `(one | two)`. Measured 2026-10-02 (#5138). See
	// Dialect.CasePatternListSpansBlanks, the flag that option moves.
	Grouped bool
}

CaseItem is one `pattern | pattern) list ;;` arm.

func (*CaseItem) End

func (i *CaseItem) End() Pos

func (*CaseItem) Pos

func (i *CaseItem) Pos() Pos

type Command

type Command interface {
	Node
	// contains filtered or unexported methods
}

Command is a simple command, a compound command, or a function definition.

type CommentMode

type CommentMode uint8

CommentMode says what an unquoted `#` standing where a word could begin means to this lexer.

The zero value is the shell's own rule and is what a parse that says nothing gets. Two callers say something. ShellWords re-reads a *value* as a command line rather than reading a program, and there the question has three answers rather than one — see the note on that function. A front end reading a line a person typed has two of the three, through Dialect.Comments, because one shell in the panel reads a `#` at its prompt as an ordinary character.

CommentsKept is the one that stays ShellWords': a token stream carrying a comment as a word is not a program, and the parser is never handed one.

const (
	// CommentsSkipped is the ordinary rule: a `#` opens a comment, the
	// comment runs to the end of the line, and none of it is a token.
	CommentsSkipped CommentMode = iota
	// CommentsOrdinaryText has no comments at all: a `#` is a character of
	// the word it stands in, wherever it stands.
	CommentsOrdinaryText
	// CommentsKept makes a comment one word, `#` included, running to the
	// end of the line and not taking the newline with it.
	CommentsKept
)

type CondBinary

type CondBinary struct {
	Op   string
	X, Y *Word
}

CondBinary is a two-operand test.

The right operand is a *word* rather than a string because what it means depends on how it was written: unquoted it is a pattern for `==` and a regular expression for `=~`, and quoted it is a literal. Only the spans still know which.

func (*CondBinary) End

func (c *CondBinary) End() Pos

func (*CondBinary) Pos

func (c *CondBinary) Pos() Pos

type CondCompletion added in v0.0.10

type CondCompletion struct {
	// Op is the operator, written with its leading `-`.
	Op string
	// Words is what stood after it: one or two, never none — an operator
	// with nothing after it is an ordinary word and never reaches here.
	Words []*Word
	Start Pos
	// Stop is the end of the operator itself, kept so a node whose operand
	// list is somehow empty still has an end.
	Stop Pos
}

CondCompletion is one of the four completion-context tests — `-prefix`, `-suffix`, `-after` and `-between` — standing with the operands its arity allows.

A node of its own rather than a CondUnary because the operator is in front of its operands and there may be two of them, which neither of the other two shapes can hold: `[[ -prefix 1 '*=' ]]` is the pair's optional leading count, and `-between` takes a start pattern and an end pattern. See Dialect.CompletionConditions for the arities and where they were measured.

func (*CondCompletion) End added in v0.0.10

func (c *CondCompletion) End() Pos

func (*CondCompletion) Pos added in v0.0.10

func (c *CondCompletion) Pos() Pos

type CondExpr

type CondExpr interface {
	Node
	// contains filtered or unexported methods
}

CondExpr is a node in a condition.

type CondGroup

type CondGroup struct {
	X     CondExpr
	Start Pos
	Stop  Pos
}

CondGroup is `( c )`, which groups a condition rather than starting a subshell.

func (*CondGroup) End

func (c *CondGroup) End() Pos

func (*CondGroup) Pos

func (c *CondGroup) Pos() Pos

type CondLogic

type CondLogic struct {
	Op   string
	X, Y CondExpr
}

CondLogic is `a && b` or `a || b` between conditions.

func (*CondLogic) End

func (c *CondLogic) End() Pos

func (*CondLogic) Pos

func (c *CondLogic) Pos() Pos

type CondNot

type CondNot struct {
	X     CondExpr
	Start Pos
}

CondNot is `! c`.

func (*CondNot) End

func (c *CondNot) End() Pos

func (*CondNot) Pos

func (c *CondNot) Pos() Pos

type CondUnary

type CondUnary struct {
	Op    string
	X     *Word
	Start Pos
}

CondUnary is a one-operand test such as `-n s` or `-f path`.

func (*CondUnary) End

func (c *CondUnary) End() Pos

func (*CondUnary) Pos

func (c *CondUnary) Pos() Pos

type CondUnknown added in v0.0.13

type CondUnknown struct {
	// Op is the operator: a known one with the wrong arity, or a word that is
	// no condition here.
	Op string
	// Words is what stood after it, which is none of them for `[[ -n ]]`, one
	// for `[[ -Q x ]]` and two or more for `[[ -n x y ]]`. A known operator
	// with exactly one is an ordinary CondUnary and never reaches here.
	Words []*Word
	// Left is the operand that stood *before* the operator, where the name
	// the dialect has not got was written between two operands:
	// `[[ str -pcre-match pat ]]`. Nil for the prefix shapes, which is every
	// other way this node is reached.
	//
	// A field of its own rather than the first entry of Words, because what
	// the two hold is not the same thing. Words is "what stood after it" and
	// a printer writes them back in that order; an infix operand written in
	// front of the operator has to go back in front of it, and a node that
	// could not tell the two apart would write `-pcre-match str pat` for a
	// line somebody wrote the other way round.
	Left  *Word
	Start Pos
	// Stop is the end of the operator itself, so a node with no words at all
	// still has an end.
	Stop Pos
}

CondUnknown is a condition the grammar accepted and the interpreter refuses by name — `unknown condition: -n` — which is two shapes and one sentence: an operator this dialect has, standing with the wrong number of operands, and a `-word` it has no condition for at all.

One dialect resolves a condition's name when it runs and the rest refuse while reading — see Dialect.ConditionIsResolvedWhenItRuns for both measurements. The operator and every word that stood with it are kept, because what a shell says about this names the operator and a printer has to write the line back as it was.

func (*CondUnknown) End added in v0.0.13

func (c *CondUnknown) End() Pos

func (*CondUnknown) Pos added in v0.0.13

func (c *CondUnknown) Pos() Pos

type ContinuedHeredocDelimiter

type ContinuedHeredocDelimiter uint8

ContinuedHeredocDelimiter is how far a here-document body line assembled across a backslash-newline may go toward being the delimiter. See Dialect.HeredocDelimiterAcrossAContinuation.

A place rather than a bool, for the reason SeparatorSkip and ArraySemicolon are: the panel draws this in three sets, not two, and a single yes/no could not be given a value for any of them without taking a line one of the others reads as body.

That a body line ends in a backslash and continues at all is unanimous and is not this question — every column joins `A\` to the line under it and looks for the delimiter afterwards, which is what #2430 was. This is the narrower one: the *joined* text is spelled exactly like the delimiter, and the columns part over whether that ends the document.

Measured 2026-09-12 with `-c` and the null device on standard input, the body printed by `cat`:

delimiter ABC, body line    A\ ⏎ BC        \ ⏎ ABC
                            joins to ABC   joins to ABC
                            after text     before any
bash 5.3 / 3.2 / as `sh`    delimiter      delimiter
zsh 5.9.2                   delimiter      delimiter
dash                        body           delimiter
BusyBox ash                 body           delimiter
ksh93u+ 2012                body           body

So the second column is what separates dash and BusyBox ash from ksh93: a continuation standing *before* any text of the line still leaves the delimiter reachable there, and one standing after text does not.

One corner below this is measured and deliberately not modeled, because the panel parts three ways again and over tab-stripping rather than over the delimiter. `<<-EOF` with a body line of one tab and a backslash, the delimiter written under it with its own tab:

cat <<-EOF ⏎ →\ ⏎ →EOF ⏎ X ⏎ →EOF

bash strips the tabs of the *joined* text and so reads the line as `EOF` and ends the document there; zsh and ksh93 strip only the tabs the logical line opens with, leaving `<tab>EOF` as body, which is what this implementation does; dash and BusyBox ash keep the backslash-newline outright and join nothing. Two of the five dialects agree with what is here and the remaining three hold two further readings between them, so a rule covering them would be two more rules.

const (
	// NoHeredocDelimiterAcrossAContinuation is the core answer and the safe
	// one: a line that took a continuation is never the delimiter. ksh93u+.
	//
	// Safe because of which way the two readings fail. Reading a line as the
	// delimiter that the writer meant as body ends the document early and
	// hands the rest of the body to the parser as *commands*; reading a
	// delimiter as body only runs the document on, which is unfinished input
	// and says so.
	NoHeredocDelimiterAcrossAContinuation ContinuedHeredocDelimiter = iota

	// HeredocDelimiterAfterALeadingContinuation lets the delimiter be found
	// after continuations that stand before any text of the line, and not
	// after one that stands after text. dash and BusyBox ash.
	HeredocDelimiterAfterALeadingContinuation

	// HeredocDelimiterOnTheJoinedLine compares the whole joined text, however
	// many physical lines went into it. bash 5.3, bash 3.2, bash as `sh`, and
	// zsh.
	HeredocDelimiterOnTheJoinedLine
)

type CoprocClause

type CoprocClause struct {
	// Name is what the coprocess was called, or empty for the default. A
	// name can only be written before a compound command: with a simple one
	// the first word is the command itself.
	//
	// Set only where the word was written as a bare unquoted name. Anything
	// else the grammar admits there lands in NameWord instead.
	Name string
	// NameWord is the word standing where the name belongs when it was not
	// written as a bare name — `coproc $v { … }`, `coproc "q" { … }`, and
	// every word that is no name at all. The shell with this construct
	// **expands that word and judges the result when the clause runs**,
	// which is why the tree keeps the word rather than its text: measured
	// 2026-09-17 on bash 5.3.20, `v=q; coproc $v { … }` puts the near ends
	// in `q`, and `coproc @ { … }` reports that `@` is not an identifier and
	// runs nothing.
	//
	// Beside Name rather than replacing it, the shape [FuncDecl.NameWord]
	// already has: the bare-name case is nearly every coprocess anyone
	// writes, and a word there would make every reader expand before it
	// could ask what the name was.
	NameWord *Word
	Cmd      Command
	Coproc   Pos
	Stop     Pos
}

CoprocClause is `coproc [NAME] command`: the command runs in the background with a pipe on each of its named streams, and the shell keeps the near ends in the array the name names.

func (*CoprocClause) End

func (c *CoprocClause) End() Pos

func (*CoprocClause) Pos

func (c *CoprocClause) Pos() Pos

type DeclarationArrayWord added in v0.0.8

type DeclarationArrayWord uint8

DeclarationArrayWord is how a declaration utility's name must be *written* for a `name=( … )` operand behind it to be read as an array literal.

The grammar half of what [interp.Semantics.DeclarationCommandWord] answers for expansion, and it is a separate question because it decides where the *word ends* rather than how the operand expands: under one reading `'typeset' a=(x y)` is a syntax error and under the other it is an array. See Dialect.DeclarationArrayFromTheCommandWord.

const (
	// DeclarationArrayFromAnUnquotedLiteralWord reads the operand as an
	// array only where the command word is one unquoted literal with no
	// quoting of any kind in it. `'typeset'`, `\typeset`, `type"set"` and
	// `$cmd` all take the reading away. bash 5.3 and zsh 5.9.2, and the
	// core.
	DeclarationArrayFromAnUnquotedLiteralWord DeclarationArrayWord = iota

	// DeclarationArrayFromAWrittenWord keeps the reading through quoting —
	// `'typeset'`, `\typeset` and `type"set"` all still open an array — and
	// loses it only where part of the word is an expansion. ksh93u+.
	DeclarationArrayFromAWrittenWord
)

type Dialect

type Dialect struct {
	// AmpersandRedirect enables `&>`, which redirects both streams, and
	// AmpersandAppendRedirect its appending `&>>`. Two flags because the
	// panel takes them apart (#5135). Measured 2026-10-01, `( echo O; echo E
	// >&2 ) OP out`:
	//
	//	              &>          &>>
	//	bash 5.3.20   both lines  both lines
	//	zsh 5.9.2     both lines  both lines
	//	bash 3.2.57   both lines  syntax error near unexpected token `>'
	//	BusyBox ash   both lines  syntax error: unexpected redirection, 2
	//	  1.37.0
	//	ksh93u+, dash  neither: `&` then a redirection, as below
	//
	// This is the one to be careful with. Where it is off, `echo hi &>b` is
	// not an error: it is `echo hi &` — a background command — followed by
	// `>b`, which truncates the file. The command runs, its output goes
	// elsewhere, and nothing is reported. Accepting the union of dialects
	// here would silently pick one meaning for text that legitimately has
	// two.
	AmpersandRedirect       bool
	AmpersandAppendRedirect bool

	// BareNegationReach says where a `!` written with no pipeline after it
	// may stand, and how far the shell will look for one.
	//
	// It is a *pipeline with no commands*, which is the reading the status
	// settles: `true; !` and `false; !` both answer 1 wherever the line is
	// taken, so nothing ran and a success was inverted. It is not "the `!`
	// negates the next line's pipeline", which #948 read it as — that would
	// make `! ⏎ echo x; echo "st=$?"` print `st=1`, and it prints `st=0` in
	// all three shells that take the line.
	//
	// The values carry the measurements; see [BareNegationReach].
	BareNegationReach BareNegationReach

	// RepeatedNegationToggles lets a pipeline carry more than one `!`, each
	// inverting the one before it.
	//
	// bash 5.3, that binary as `sh`, and ksh93. Measured 2026-09-12, and it
	// really is a toggle rather than an idempotent mark:
	//
	//	! ! true    st=0     ! ! false   st=1
	//	! ! !       st=1     ! ! ! true  st=1
	//	! !         st=0
	//
	// dash and zsh refuse a second `!` outright, which is what makes this a
	// question of its own rather than part of [Dialect.BareNegationReach]:
	// zsh takes a bare `!` and refuses `! !`, so a dialect answering one
	// answers nothing about the other.
	//
	// The tree carries one flag and not a count, which the toggle is what
	// permits: an even number of them is no negation and an odd number is
	// one, so `! ! !` and `!` are the same program and print back the same.
	RepeatedNegationToggles bool

	// NegationAfterABarTogglesThePipeline takes a `!` written after a pipe
	// bar, where it inverts the negation of the **whole** pipeline, as one
	// written in front of it does. ksh93 alone; the rest refuse the word
	// there (#5256). Measured 2026-10-02 on ksh93u+ 2012-08-01 (`/bin/ksh`),
	// script files (#5272):
	//
	//	echo | ! true            st=1     echo | ! false         st=0
	//	echo | ! true | cat      st=1     echo | ! true | false  st=0
	//	! true | ! true          st=0     echo | ! ! true        st=0
	//	echo | !                 st=1     echo | ! | cat         `|' unexpected
	//	if echo | ! true; …      the else branch
	//	echo a | ! cat           prints a, st=1
	//
	// So each `!` toggles one flag on the pipeline, wherever it stands, and
	// one with no command after it ends the pipeline there.
	NegationAfterABarTogglesThePipeline bool

	// PipeBothStreams enables `|&`, a pipe that carries the left command's
	// standard error along with its standard output. Measured identical to
	// writing `2>&1` as the left command's *last* redirection — not its
	// first: `e 2>/dev/null |& cat` shows the error and `e 2>&1 2>/dev/null
	// | cat` does not, and `e >/dev/null |& cat` shows nothing where `e 2>&1
	// >/dev/null | cat` shows the error. Both discriminating shapes agree in
	// both shells that have it, so the parser writes the redirection out
	// rather than the interpreter growing a second way to point a stream.
	//
	// Two of the six panel columns accept this reading — bash 5.3 and zsh —
	// which is why it is not core. bash 3.2 has no `|&` at all, so this is a
	// version fact as much as a dialect one; dash has none either. ksh93
	// spells a *coprocess* with the same two characters, and it is not this
	// construct with another meaning but another slot in the grammar: ksh93
	// refuses `a | ; b` and accepts `a |& ; b`, so its `|&` terminates a
	// command the way `&` does rather than joining two. That reading is
	// [Dialect.CoprocPipeOperator], a flag of its own beside Coproc and
	// never a second value of this one.
	//
	// Where it is off, `a |& b` is not silently something else: the operator
	// table falls back to `|` and then `&`, which is exactly what bash 3.2
	// and dash lex, so the refusal lands on the `&` where theirs does.
	PipeBothStreams bool

	// CaseFallthrough enables `;&`, which runs the next case body. Absent
	// from dash, and from bash before 4.0 — so it cannot be reached through
	// macOS's /bin/sh.
	//
	// Where it is off the two characters lex apart, exactly as
	// PipeBothStreams' do, and the consequence is visible wherever the pair
	// stands somewhere the grammar refuses it: `;& echo hi` names `;` in
	// bash 3.2 and `;&` in bash 5.3. Turning this flag and PipeBothStreams
	// off the bash preset is the whole of what "bash 3.2 has neither" needs
	// the grammar to say, which is measured against that build in
	// dialect/bash's TestTheGrammarSaysWhenAShellHasNeitherPipeAmpersandNorCaseFallthrough
	// (#2406).
	CaseFallthrough bool

	// CStyleFor enables `for ((init; cond; post))`. Absent from dash, where
	// the parenthesis after `for` is a syntax error.
	CStyleFor bool

	// ForArithExtraSeparators lets a C-style `for` header hold more than the
	// two separators its three expressions need: `for ((;;;))`,
	// `for ((1;2;3;4))`.
	//
	// The header is still three expressions there — everything past the
	// second `;` is part of the third, semicolons and all — so the loop
	// parses and the leftover text is refused as arithmetic, at run time and
	// only if that expression is ever reached. `for ((;;;)); do break; done`
	// therefore runs and prints nothing, where `for ((;;;)); do :; done`
	// reaches the third expression on its first pass and stops.
	//
	// Measured 2026-09-12 on `for ((;;;)); do echo body; break; done`: ksh93u+
	// and zsh 5.9.2 print `body`, and bash 5.3.15, bash 3.2.57 and the same
	// bash under argv[0] of `sh` all refuse the script with “ `;'
	// unexpected “ and run none of it. So this is an acceptance the two add
	// rather than a refusal bash has, and it is off in Core for that reason
	// (#2225).
	//
	// *Fewer* than two separators is not this flag and has no flag: every
	// shell in the panel refuses `for (())`, `for ((;))` and `for ((i=0))`,
	// which is [ErrForArithHeader]. The two were one over-acceptance here
	// until they were measured apart, and the missing refusal was unbounded
	// rather than wrong — a header with no separators has no condition, an
	// absent condition is true, and `for (()); do echo x; done` printed for
	// as long as it was left alone.
	ForArithExtraSeparators bool

	// Select enables `select name [in words] do … done`, the menu loop.
	// Absent from dash, where `select` is an ordinary word and the `do` that
	// follows it is a syntax error.
	Select bool

	// ForMultipleNames lets a `for` or `foreach` name more than one variable,
	// so the loop takes that many words from its list on every pass:
	// `for key value ( a 1 b 2 ) { … }` runs twice with the pairs. zsh alone
	// has it, and it is how a script walks a serialized key/value table.
	//
	// Separate from ForBraceBody and from ShortForm, and measured that way
	// rather than assumed: all four combinations of the name count and the
	// body spelling parse in zsh independently. `for a b in x 1 y 2; do … done`
	// works, so does `for a b ( … ) { … }`, so does `for a b ( … ); do … done`,
	// and so does `for a b` with no list at all, which walks the positional
	// parameters in groups. This flag is the name count and nothing else.
	//
	// `select` does **not** take it — measured, `select a b (x y) { … }` is a
	// parse error in the shell that has every other spelling — so the loop
	// whose header is a `for`'s parts company here. `foreach` does take it.
	//
	// **Names are taken greedily, and that is subtractive.** Every word after
	// the first is another name until the header ends: at `in`, at `(`, at a
	// separator, at `do` or at `{`. So a short body may not follow the names
	// directly — `for a print -r -- "[$a]"` is a parse error near `-r` in
	// zsh, because `print` was read as a second name — where a header that
	// ended itself still takes one: `for a b ( 1 2 ) print "$a$b"` runs. A
	// dialect with ShortForm and not this flag keeps the older, wider
	// reading, in which a word after the name begins the body.
	//
	// A name must be a plain unquoted name. `for a "b" ( … )` and
	// `for a $n ( … )` are parse errors in zsh, so the words are not
	// expanded and not unquoted before being read as names.
	ForMultipleNames bool

	// ForNameMayBeQuoted lets a loop's variable be written with quoting or an
	// escape in it, the quoting being removed before the word is read as a
	// name: `for "i" in a b` binds `i`.
	//
	// ksh93u+ alone accepts it. Measured 2026-09-06, `env -i
	// PATH=/usr/bin:/bin` with a scratch HOME, from a script file and through
	// `-c`: `for "i"`, `for 'i'`, `for i""`, `for "i"x` and `for \i` all run
	// there and all five are refused by bash 5.3.15, the same binary as `sh`,
	// bash 3.2.57, dash and zsh 5.9.2. So the escape travels with the quotes
	// rather than being a question of its own, and the flag is one bit.
	//
	// It is the *quoting* half only. A name coming out of an expansion —
	// `for $n`, `for ${n}`, `for "$n"`, `for $(echo n)` — is refused by all
	// six columns including this one, so that half is core and lives in
	// [Parser.forName] rather than here (#1076).
	ForNameMayBeQuoted bool

	// ForNameMayBeAPositionalParameter lets a loop's variable be a run of
	// digits — `for 1 in a b` — which sets the positional parameter of that
	// number on each pass rather than a variable with a digit for a name.
	//
	// One shell in the panel has it, and seven of the completion functions it
	// ships are written with it. Measured 2026-09-15, each probe in a script
	// file of its own:
	//
	//	| probe                    | zsh 5.9.2 | bash 5.3 | bash-as-sh | bash 3.2 | ksh93u+ | dash | ash |
	//	| `for 1 in a b`           | `a` `b`   | refused  | refused    | refused  | refused | refused | refused |
	//	| `for 1 2 in a b c d`     | `a-b` `c-d` | refused | refused   | refused  | refused | refused | refused |
	//	| `set -- p q r; for 1;`   | `p` `q` `r` | refused | refused   | refused  | refused | refused | refused |
	//
	// The three bash columns call it “ `1': not a valid identifier “, ksh93
	// `invalid variable name`, dash and BusyBox ash `bad for loop variable`.
	// So no column but one has it and this is a dialect's grammar.
	//
	// **Digits and nothing else**, measured in the same run: `for 0`, `for
	// 12` and `for 01` are all taken, `for 1x` is “ parse error near `1x' “
	// and `for @` is “ parse error near `@' “. So it is a positional
	// parameter's *number* rather than "a name the core would refuse", which
	// is why the flag admits one shape and not a class.
	//
	// The parameter it sets is the real one: after `set -- p q; for 1 in a b;
	// do :; done`, `$1` is `b` and `$2` is still `q`.
	ForNameMayBeAPositionalParameter bool

	// ForNameCheckedWhenTheLoopRuns makes a `for` or `select` whose variable
	// is not a name **parse**, with the word carried on the clause and the
	// complaint raised when the loop is reached.
	//
	// A stage and not a wording, which is the point: `bash -n` accepts a
	// script this refused. Measured 2026-09-06 and re-measured 2026-09-07,
	// `env -i PATH=/usr/bin:/bin` with a scratch HOME, over a script file
	// holding `n=x`, `for $n in a b; do echo body; done` and
	// `echo "reached-after st=$?"`:
	//
	//	shell        -n over that file   a run
	//	bash 5.3.15  accepts, silent, 0  the complaint, then reached-after st=1
	//	bash-as-sh   accepts, silent, 0  the complaint, and stops at 2
	//	bash 3.2.57  accepts, silent, 0  the complaint, then reached-after st=1
	//	dash         refuses, 2          the complaint, and stops
	//	ksh93u+      accepts, silent, 0  the complaint, and stops at 1
	//	zsh 5.9.2    refuses, 1          the complaint, and stops
	//
	// So four of the six parse it and `for 1x` behaves identically in every
	// column — the expansion is not what makes the difference, which is why
	// this is one flag and not one per spelling.
	//
	// A syntax check is what a CI job runs, so a script that works reported
	// as broken is the visible cost; and stopping where bash carries on is
	// the worse of the two remaining directions, because the output goes
	// missing rather than coming out wrong (#1110).
	//
	// What happens when the loop *is* reached is not this flag —
	// interp.Semantics.ForNameWhenTheLoopRuns — because three answers among
	// the two shells that get here is a behavior question and not a grammar
	// one.
	ForNameCheckedWhenTheLoopRuns bool

	// ForNonWordIsANameError judges whatever stands in the loop-variable
	// position as a *name*, even when it is a token that could never be a
	// word at all — the end of the input, a newline, a `;`.
	//
	// dash alone does that, and it is the whole of the difference: it answers
	// `for`, `for` with a newline after it, `for ;` and `for ; in a b` with
	// the one sentence it gives every bad loop variable. The other three ask
	// the grammar first, so a token that is not a word is refused as a token
	// and never reaches the name check:
	//
	//	              dash                    bash            ksh93            zsh
	//	`for`         Bad for loop variable   `newline'       `for' unmatched  near `for'
	//	`for` NL do   Bad for loop variable   `newline'       `newline'        near `\n'
	//	`for ;`       Bad for loop variable   `;'             `;'              near `;'
	//	`for 1x in a` Bad for loop variable   not a valid identifier — the name check
	//
	// Measured 2026-09-07, `env -i PATH=/usr/bin:/bin` with a scratch HOME,
	// through `-c`, from a script file and on standard input; the panel gives
	// the same answer on all three routes, so this is not a route question
	// (#1319). The last row is the control: a word that is present and is not
	// a name is the [ErrForName] case in every column including this one, and
	// stays there.
	//
	// The status follows the classification rather than being set beside it:
	// a grammar failure carries the dialect's parse-error status — bash 2,
	// ksh93 3, zsh 1 — where ErrForName carries ForNameStatus, so calling a
	// bare `for` a bad name answered 1 in three dialects that answer 2, 3 and
	// 1 for every other refused line.
	ForNonWordIsANameError bool

	// EndOfInputIsANewlineWhereNoneCouldStand makes the input running out in a
	// position that takes no newline read as a newline standing there, rather
	// than as a construct left unfinished. Two positions have that shape: a
	// loop's variable, and the name after the `function` keyword.
	//
	// It is the other half of the three-way split [ForNonWordIsANameError]
	// begins, and the two together carry the three answers the panel gives a
	// bare `for`: dash calls it a bad loop variable, bash calls it an
	// unexpected `newline`, and ksh93 and zsh give the same unterminated
	// wording they give a bare `while` — “for' unmatched“ and “parse error
	// near `for'“.
	//
	// bash's answer is a fact about where its input ends. It terminates what
	// it reads with a newline, so a position that *accepts* newlines swallows
	// that one and reports the end of the file — a bare `while` is `unexpected
	// end of file from `while' command on line 1` there, the same shape as
	// every other unfinished construct. The loop-variable position accepts no
	// newline, so the newline is what is left over and the newline is what it
	// names. The same fact is why it puts the end of input on the line *after*
	// the text; see Error.EndLine, which is the half of it already recorded.
	//
	// Consulted only where a newline could not have stood, which is why it is
	// this flag rather than a claim about every failure: everywhere else the
	// newline is taken and the question never arises. It was named for the
	// loop while the loop was the only such position; the keyword's name is
	// the second, and the two answer alike — `function` as the whole of a
	// file that does not end in a newline is `newline'` in bash 5.3.20, in
	// that binary as `sh` and in bash 3.2.57, against `end of file` in
	// ksh93u+ and in BusyBox ash 1.37.0, measured 2026-09-19.
	EndOfInputIsANewlineWhereNoneCouldStand bool

	// ForBraceBody lets a `for` or `select` loop take a brace group where
	// `do … done` stands: `for ((;;)) { echo hi; break; }`, and equally
	// `for i in a b; { echo "$i"; }`. Absent from dash, which is the only
	// panel shell that refuses it.
	//
	// It belongs to those two loops and to nothing else. `while cond { … }`,
	// `until cond { … }` and `if cond { … }` are refused by every shell in
	// the panel, which is what makes this a production of the loop rather
	// than a general rule about bodies — and what makes it easy to miss.
	//
	// The separator before the brace is not optional in the list form, and
	// not for a reason about this flag: `for i in a b { … }` reads `{` as
	// another *item*, so the loop then meets `}` where `do` belongs. Only
	// the C-style header ends itself, which is why that one form takes the
	// brace with nothing between.
	//
	// A dialect with ShortForm widens the *body* half of this to any single
	// command, and adds `while` and `until` to the loops that may take one.
	ForBraceBody bool

	// ShortForm is the family of compound commands whose body may be written
	// without the words that ordinarily open and close it. One shell in the
	// panel has it; the other four refuse every shape below.
	//
	// It was called ShortLoop, and the name was the reason its coverage
	// stopped where it did: `if` is not a loop, and the rule the flag stands
	// for has nothing to do with looping (#827).
	//
	// Two productions, and they are one flag because they are one feature —
	// a loop header that has ended may be followed by its body directly:
	//
	//	while (( i < 2 )) echo $i        a body of one command
	//	while (( i < 2 )) { … }          which may be a brace group
	//	until [[ -n $x ]] { … }          and `until` equally
	//	for i (a b) { echo $i }          a parenthesized word list
	//	for i (a b) echo $i              with the same short body
	//	select x (a b) { … }             and `select`, whose header is a for's
	//	while false                      the body omitted altogether
	//
	// **The header has to end itself, and a separator is the opposite of
	// help.** `while true { … }` is a syntax error because `true` is a simple
	// command and `{` is another of its words; `while (( i < 2 )) { … }`
	// works because `(( … ))` closes. And `while true; { … }` is not this
	// production at all — the `;` continues the *condition list*, so the
	// brace group becomes the last command tested and the body is empty.
	// That is measurable rather than a reading: `i=0; while (( i<2 )); {
	// echo $i; i=$((i+1)) }` counts up forever, where the body reading
	// would print 0 and 1 and stop. The one-command body is what makes the
	// omitted body reachable, so the two are not separable.
	//
	// For a `for`, "ended" means the parenthesized list, or no `in` clause
	// at all. `for i in a b` still needs a separator or `do`, which is the
	// same rule that makes `for i in a b { … }` read `{` as another item.
	//
	// **An `if`'s arms choose the form one at a time.** The `{` is what
	// makes an arm short, and the first arm not written that way puts the
	// rest of the construct in the long form — where there is a `fi`, and it
	// is required. So `if (( 0 )) { echo A } else echo B; fi` runs and
	// `if (( 0 )) { echo A } else echo B` is refused for want of the `fi`,
	// while `else { echo B }` owes none and refuses one written anyway. A
	// newline before that brace decides nothing, an `else` having no
	// condition for one to end; a newline after an `elif`'s condition
	// decides everything, that being the long form's own rule. #1372 read
	// the refusal of an `else` with nothing after it as a rule about empty
	// arms, which would have refused the first shape here too.
	//
	// **And the end of the chain is not an arm.** A short `if` whose last
	// arm is a brace body and which has no `else` may be closed with one
	// `fi` written anyway: `if (( 1 )) { echo A } fi` runs, and so does the
	// same line with an `elif` chain in front of it. It is the one place the
	// production's two body spellings part — `if (( 1 )) (( 2 )) fi` is
	// refused — and it is `if` alone, the short loops taking no `done`. See
	// [Parser.redundantFi], which has the whole measured table. #2242.
	//
	// **A short body that took its separator took the construct's.** `if
	// (( 1 )) echo A; else echo B` is refused with or without a `fi`: the
	// `;` belongs to `echo A` and a short body's separator is the whole
	// statement's, so the `if` ended and nothing is left for the `else`. A
	// brace body takes no separator — not its own, and not one an inner
	// short form took inside it.
	//
	// A production of the grammar and not of the printer: a body that was
	// written short is printed as `do … done`, which parses to the same tree
	// under any dialect. Only an *omitted* body has no long spelling, so
	// that one is printed back short.
	//
	// **Two of the three body spellings are [Dialect.ShortFormBody]'s**, and
	// the split is measured rather than tidy: the shell that has this family
	// carries an option moving the one-command and omitted bodies and leaving
	// the brace-spelled one alone. This flag is what the family *is* — the
	// parenthesized item list, the brace body wherever the family reaches,
	// and the redundant `fi` — and the other one is which bodies may be
	// written without braces.
	ShortForm bool

	// ShortFormBody is the half of [Dialect.ShortForm] a running script can
	// move: a body written as **one command**, or omitted altogether. The
	// brace-spelled body is not this flag's, and neither is anything else the
	// family adds.
	//
	// A dialect with ShortForm and not this one keeps `while (( i < 2 )) { …
	// }`, `for i (a b) { … }`, `if (( 1 )) { … }` and `for i (a b); do …
	// done`, and refuses `while (( i < 2 )) echo $i`, `for i (a b) echo $i`,
	// `if (( 1 )) echo A`, `repeat 2 echo x` and `while false`.
	//
	// Measured on zsh 5.9.2 (aarch64-apple-darwin25.4.0) at
	// `/opt/homebrew/bin/zsh`, `-f` over a script file under `set -n`,
	// 2026-09-27, with the option moved on the line after an `emulate`, in
	// each of that shell's three emulations:
	//
	//	                             on      off
	//	for i in a b; echo $i        parses  refused
	//	for i (a b) echo $i          parses  refused
	//	if (( 1 )) echo hi           parses  refused
	//	while (( 0 )); :             parses  refused
	//	while (( 0 )) :              parses  refused
	//	until (( 1 )); :             parses  refused
	//	repeat 2 echo x              parses  refused
	//	select o (a b) :             parses  refused
	//	for i                        parses  refused
	//	for i in a b                 parses  refused
	//	while                        parses  refused
	//	while true                   parses  refused
	//	until                        parses  refused
	//	  and the brace half, which the same option leaves alone:
	//	if (( 1 )) { echo A; }       parses  parses
	//	while (( 0 )) { :; }         parses  parses
	//	until (( 1 )) { :; }         parses  parses
	//	repeat 2 { echo x; }         parses  parses
	//	select o (a b) { :; }        parses  parses
	//	for i (a b) { echo $i; }     parses  parses
	//	for i (a b); do echo $i; done parses parses
	//	if (( 1 )) { echo A; } fi    parses  parses
	//
	// **The three emulations answer every row of that table alike**, which is
	// what says the noun is the option and not the mode. A table keyed on the
	// mode gets the first thirteen wrong whenever a script moves the option by
	// hand, and one keyed on the flag as it stood before the split refuses the
	// brace rows along with them.
	//
	// The brace rows are written with a `;` before the `}` on purpose. Without
	// one, `{ echo A }` is a *brace group* that only some readings of the
	// grammar accept, which is a question about a group and not about a loop's
	// body — and reading the two together is what made this look like a split
	// between emulations before it was measured a separator apart.
	ShortFormBody bool
	// ShortRepeatBody gives `repeat` alone its short body back while
	// [Dialect.ShortFormBody] is off: zsh's `shortrepeat`. Measured
	// 2026-10-02 on zsh 5.9.2, `unsetopt shortloops; setopt shortrepeat;
	// eval 'repeat 3 print n'` prints three lines where `for f in a b;
	// print $f` is still refused (#5155).
	ShortRepeatBody bool
	// LoopBodyEndsInEnd reads a loop's body that is neither `do … done` nor
	// a brace group as a list closed by `end`, the way `foreach`'s is — and
	// takes the one-command body away. zsh's `cshjunkieloops`. Measured
	// 2026-10-02 on zsh 5.9.2 under `eval` after the option, E01options'
	// `CSH_JUNKIE_LOOPS option (for loop)` (#5155):
	//
	//	for f in a b; print $f; end        a, b
	//	for f (a b) print $f; print x; end a, x, b, x
	//	for ((i=0;i<2;i++)); print $i; end 0, 1
	//	repeat 1; print r; print s; end    r, s
	//	for f in a b; do print $f; done    a, b — the keyword body stands
	//	for f in a b; { print $f }         a, b — and so does the brace one
	//	for f (a b) print $f               parse error near `$f'
	//	repeat 1 print r                   parse error near `r'
	//	if true; then print t; end         parse error near `end' — loops only
	//
	// `while` and `until` take it too, and their condition list runs on to
	// the `end` where a `;` keeps it going: `while ((i++ < 2)) print w$i;
	// end` prints w1 and w2, and the same with a `;` before `print` never
	// stops, the `print` being the condition.
	LoopBodyEndsInEnd bool

	// Repeat is `repeat N`, a loop over a count rather than over a list or a
	// condition. One shell in the panel has it; the other four read the word
	// as an ordinary command name.
	//
	// It is a separate flag from ShortForm because the two are separate
	// questions — a shell could have the construct and spell its body only
	// as `do … done` — and because the word is a keyword only where a
	// command may begin: `repeat=5` is an ordinary assignment there.
	Repeat bool

	// Foreach is `foreach name (a b) … end`, the same loop a `for` is under
	// a different pair of words. One shell in the panel has it.
	//
	// The list is the parenthesized one ShortForm already reads, or an `in`
	// list, and `for name (a b); …; end` is refused — measured — so the
	// terminator belongs to the opening word rather than to the list.
	//
	// **`end` is not the whole of what it adds**, which this said until it
	// was measured against a shipped function that writes the other
	// spelling. All of `foreach c (a b); do … done`, `foreach c (a b) do …
	// done`, `foreach c (a b) { … }` and `foreach c in a b; do … done` run
	// on zsh 5.9.2 (2026-09-15). The closers pair rather than mixing:
	// `foreach c (a b); do … end` is refused there, as `for` closed by
	// `end` is.
	Foreach bool

	// TryAlways is `{ … } always { … }`: a brace group whose second half runs
	// however the first half ended. One shell in the panel has it; the other
	// five call the word a syntax error where it stands.
	//
	// **It is positional and not a reserved word, which is the whole of the
	// grammar.** Measured 2026-09-07 against zsh 5.9.2: `always` alone is
	// `command not found`, `always() { :; }` defines a function, `echo
	// always` prints it and `x=always` assigns it — so the word may not join
	// stopWords or reservedWords. It is read only where a brace group has
	// just closed, and nothing else there will do:
	//
	//	{ echo t; } always { echo a; }     the construct
	//	{ echo t; }; always { … }          `always` is a command name again
	//	{ echo t; }
	//	always { … }                       a newline is a separator too
	//	{ echo t; } "always" { … }         quoting takes the keyword away
	//	{ echo t; } > /dev/null always {}  a redirection ends the try half
	//	{ echo t; } always echo a          the second half must be a group
	//	{ echo t; } always {} always {}    and there is exactly one of them
	//	if true; then :; fi always { … }   no other compound command takes it
	//	for i in a; { :; } always { … }    including a loop's brace body
	//	f() { :; } always { … }            nor a function definition's
	//
	// Every one of those is a parse error in the shell that has the
	// construct, and the first two run there as two commands. So the flag
	// gates one production hanging off the brace group and never the lexer.
	//
	// Redirections belong to the whole construct rather than to either half:
	// `{ echo t; } always { echo a; } > /dev/null` prints nothing at all.
	// Nesting works in both halves.
	TryAlways bool

	// NamespaceBlock is `namespace NAME { list }`, the reserved word that opens a
	// name-resolution region.
	//
	// One column has it: ksh93u+ 2012-08-01. Measured 2026-09-19, each row its
	// own script file under `env -i PATH=/usr/bin:/bin LC_ALL=C` with standard
	// input on /dev/null, against `namespace ns { x=1; }`:
	//
	//	ksh93u+       silent, 0
	//	bash 5.3.20   syntax error near unexpected token `}', 2
	//	zsh 5.9.2     parse error near `}', 1
	//	dash 0.5.12   Syntax error: "}" unexpected, 2
	//
	// **The word is reserved where a command begins and nowhere else**, which is
	// what keeps it a dialect's word rather than a name taken away from the other
	// five: `namespace=5` assigns and `echo namespace` prints the operand, in
	// ksh93 as in every other column. So the flag is read from the command
	// dispatch and the word is not in the lexer's tables.
	//
	// The production is the word, then one word standing where the name belongs,
	// then a brace group. Measured on the same run:
	//
	//	namespace ns { echo inside; }; echo after   inside, after, 0
	//	namespace ns / { echo hi; }                 hi, 0 - the brace may follow a newline
	//	namespace ns; echo after                    syntax error naming `;', 3
	//	namespace; echo two                         the same
	//	namespace ns{ echo hi; }                    syntax error naming `echo', 3
	//	"namespace" ns { echo hi; }                 syntax error naming `}', 3
	//	if true; then namespace ns { echo in; }; fi in, 0 - it nests where a command stands
	//	namespace ns { echo a; } > out.txt          the block takes redirections
	//	namespace .ns { x=1; }                      `.ns: is not an identifier`, 1
	//
	// The last row is the stage split, and it is the reason
	// [NamespaceClause.Name] carries a word rather than the parse refusing one: a
	// name that is no name is a *runtime* refusal there, at the dialect's
	// ordinary fatal status, and a file holding one still reads.
	NamespaceBlock bool

	// IfClauseStepsOverWhatItCannotUse consumes a token standing where an
	// `if` or `elif` clause could still go on that no command could begin
	// with, and refuses the one after it instead.
	//
	// One shell in the panel. At the top level, with no substitution
	// anywhere, `if true; then ) echo X; fi` is “parse error near `echo' “
	// on zsh 5.9.2 and “syntax error near unexpected token `)' “ on bash
	// 5.3.20, which names the parenthesis as dash 0.5.12 does. See
	// [Parser.clauseStepsOverWhatItCannotUse] for the measured rows, for the
	// three tokens outside the set, and for the pipeline operators that are
	// in the reference's set and not in this one.
	//
	// It is a grammar flag rather than a wording, because the token is
	// *consumed*: `v=$(echo hi; if true; then)` is a substitution that never
	// closes in that shell, where the columns that refuse the parenthesis
	// end the body at it.
	IfClauseStepsOverWhatItCannotUse bool

	// AnonymousFunction is `() { … }` and `function { … }`: a function with
	// no name, defined and run where it stands, with the words after it as
	// its positional parameters. One shell in the panel has it; in the other
	// four a `(` where a command begins opens a subshell and `()` is a
	// syntax error.
	AnonymousFunction bool

	// BareFunctionKeyword takes the `function` keyword with no name, no
	// parameter list and no body at all. It is the anonymous function of
	// [Dialect.AnonymousFunction] with an empty body, so it is a command
	// that runs where it stands, and an empty body leaves `$?` as it found
	// it: `false; function; echo $?` prints 1 there.
	//
	// Measured 2026-09-19, script file under `env -i PATH=/usr/bin:/bin
	// LC_ALL=C` with standard input on the null device. `st=` is what an
	// `echo st=$?` on the next line printed.
	//
	//	written        bash 5.3.20  bash-as-sh  bash 3.2  ksh93u+  zsh 5.9.2  dash 0.5.12  BusyBox ash 1.37.0
	//	`function` ⏎   `newline'    `newline'   `newline' `newline' st=0      not found    unexpected newline
	//	`function;`    `;'          `;'         `;'       `;'       st=0      not found    unexpected ";"
	//	`function &`   `&'          `&'         `&'       `&'       `&'       not found    unexpected "&"
	//
	// So one column takes it and six refuse it, and the six name the token
	// they found. dash has no keyword at all and runs `function` as a
	// command name, which is the 127 its own row records; BusyBox ash has
	// the keyword — `function f { echo B; }` defines and calls there — and
	// is with the refusing columns rather than with dash, which is the row
	// #3732 left to measure and guessed the other way.
	//
	// The reach is a set rather than a bool's worth of "anywhere", and it is
	// nearly [BareNegationWhereAListEnds]'s with one difference each way.
	// Measured the same day, in the column that takes it: `;`, a newline,
	// the end of input, `|`, `|&`, `&&`, `||`, the `)` of a subshell, the
	// `}` of a brace group, a `case` arm's `;;`, and a redirection with no
	// command in front of it all take it — `{ function }`, `( function )`,
	// `function | cat`, `false || function` and `function > f` all run at
	// status 0. `&` does not, in any of its spellings: `function &` and
	// `function &!` are parse errors naming the operator, which is the same
	// boundary that shell draws for a bare `!`. And no other reserved word
	// does: `if true; then function fi`, `while false; do function done` and
	// `case x in x) function esac` are all parse errors, because `fi`,
	// `done` and `esac` are read there as the *name* this form does not
	// have. The `}` is the exception among the reserved words and it is not
	// a name anywhere: `function } { echo B }` is refused on the `}`.
	BareFunctionKeyword bool

	// AppendAssign enables `name+=value`, which appends rather than
	// replacing. Absent from dash, where `x+=b` is a command called `x+=b`.
	AppendAssign bool

	// PositionalAssignment lets an assignment's *name* half be a run of
	// decimal digits, so `1=abc` writes the first positional parameter.
	//
	// A grammar flag rather than an axis, because the difference is in what
	// the word *is* and not in what is then done with it. Measured
	// 2026-09-07 with `set -- x; 1=abc; echo $1`: bash 5.3.15, that binary
	// as `sh`, bash 3.2.57, dash and ksh93 all take the word as a command
	// name and answer `1=abc: command not found` at 127, and zsh 5.9.2
	// prints `abc`. The two readings are told apart by what the word is
	// *subjected to*, which is the discriminating probe: `set -- x; 1=*`
	// leaves `$1` holding a literal `*` in zsh — an assignment's value is
	// neither globbed nor split — where bash globs the whole word and
	// complains about `1=*`, and `v="a b"; 1=$v` leaves one parameter
	// holding `a b` in zsh where bash splits and complains about `1=a`. A
	// word that is expanded one way in one shell and another way in another
	// is a difference in the parse, so it belongs here.
	//
	// Only a bare run of digits. A subscript on one is not this construct:
	// `1[0]=v` is a command name in zsh too, and globs as one. And the
	// grammar is where it ends — a *declaration* still refuses the digits it
	// admits, because `local 1=abc`, `typeset 1=(a b)`, `export 1=x` and
	// `readonly 1` are each `not an identifier: 1` in the same shell. So the
	// declaration path reads the operand and the utility refuses the name,
	// which is where that complaint is already worded.
	//
	// Off, `1=abc` is a command name exactly as it was, which is what keeps
	// the five refusing columns byte-identical.
	//
	// The plugin manager in `~/.zi` writes it: `.zi-any-to-user-plugin` and
	// `.zi-formatter-pid` both assign their own positionals, so a startup
	// that reads them printed twelve `no such file or directory:
	// 1=username/reponame` lines and never reached a prompt (#1438).
	PositionalAssignment bool

	// DottedName makes `.` a name character, so `${.sh.version}` reads and
	// `.foo=1` is an assignment rather than a command name.
	//
	// **ksh93 alone.** It is one lexical rule and not two, which is the
	// question #2620 asked and the probes answered. Measured on ksh93u+
	// 2012-08-01, 2026-09-13, `-c`:
	//
	//	${.sh.version}   Version AJM 93u+ 2012-08-01
	//	${.foo}          empty, status 0 — no `.sh` about it
	//	${.}             empty, status 0 — the dot alone is a name
	//	${x.y}           empty, status 0, with `x=1` set
	//	.foo=1; ${.foo}  1 — an assignment, and it reads back
	//	typeset .x=3     accepted, and `${.x}` is 3
	//	for .x in 1 2    accepted, and the body sees `${.x}`
	//	$.foo            the four characters, with `.foo` set — *not* an
	//	                 expansion, so the dot is a rule about names and
	//	                 not about what follows a `$`
	//
	// So a dot is an ordinary name byte wherever a name is read, it may
	// *begin* one, and the `.sh` namespace and a compound variable's member
	// are the same grammar reached twice rather than two constructs. What
	// distinguishes them is only what the interpreter has stored under the
	// name.
	//
	// The other five columns call `${.sh.version}` a bad substitution at
	// *run* time — bash 5.3.15, that binary as `sh`, bash 3.2.57, zsh 5.9.2
	// and BusyBox ash all run the command before it — so this is a grammar
	// flag and not an axis: nobody else has a reading of the construct to
	// disagree with.
	DottedName bool

	// TildeGroup makes a `(` that stands immediately after a `~` part of the
	// word rather than the operator it otherwise is, which is what lets
	// ksh93's `~(…)` pattern-modifier prefix be written at all.
	//
	// **ksh93 alone**, and it is a rule about the *lexer* rather than about
	// patterns: the group belongs to the word wherever the word stands, and
	// only a word being matched as a pattern then reads the letters.
	// Measured on ksh93u+ 2012-08-01, 2026-09-13, `-c`:
	//
	//	echo ~(E)abc                 ~(E)abc     an ordinary word, literal
	//	x=~(Z)abc; echo "$x"         ~(Z)abc     and so is a value
	//	echo a~(x)b                  a~(x)b      mid-word too
	//	[[ abc == ~(E)a.c ]]         matches     a pattern reads them
	//	[[ abc == a~(E)b.? ]]        matches     mid-pattern as well
	//	case abc in ~(E)^a.c$)       matches
	//	s=aXbXc; ${s//~(E)X/-}       a-b-c
	//
	// Without it the `(` ends the word and the shell reports a syntax error
	// at the paren — which is what the other five columns do, unanimously
	// and at parse time: bash 5.3.15, that binary as `sh`, bash 3.2.57, dash
	// and BusyBox ash all refuse the file. zsh is the one that neither
	// refuses nor honors: there `~` is the exclusion operator and `~(E)abc`
	// is a pattern that reads and does not match, which is a reading of its
	// own and not this construct.
	//
	// So a grammar flag, and gated: a `cmd/bash` that took the `(` would
	// accept what real bash refuses. See interp's tildeModifier for which
	// letters are honored once the group has been read (#2621).
	//
	// **And the group turns the rest of its word raw.** Reading the group
	// alone left `[[ abcd == ~(E)(ab)cd ]]` a syntax error at the second
	// paren, which is a script that will not run on a route this tree ships:
	// two of the three metacharacters that make an extended regular
	// expression extended are also the shell's. From the end of the group to
	// the end of the word, the characters that would otherwise end a word do
	// not. See [Lexer.afterTildeGroup] for the measured set and the controls
	// that tell a pattern character from a shell one (#3808).
	TildeGroup bool

	// CurrentShellSubstitution reads `${ cmd;}` as a command substitution
	// that runs in the current shell. bash 5.3 and ksh93 have it; dash and
	// zsh call it a bad substitution.
	//
	// The space after the brace is load-bearing and is the whole of the
	// grammar: `${x}` is a parameter and `${ x}` is a command.
	CurrentShellSubstitution bool

	// BraceProgramBodyEnd says which `}` ends that body. The two shells that
	// have the construct disagree, and both of them refuse a body with no
	// terminator in front of the brace. The values carry the measurements;
	// see [BraceProgramBodyEnd].
	//
	// Read only where CurrentShellSubstitution is on — where it is off there
	// is no such body to end — and it governs [Dialect.ReplySubstitution]'s
	// body too, which is the same list with a `|` in front of it.
	BraceProgramBodyEnd BraceProgramBodyEnd

	// ReplySubstitution reads `${| cmd;}` as a command substitution that runs
	// in the current shell and expands to whatever the body left in `$REPLY`
	// rather than to what it printed. bash 5.3 has it and nothing else in the
	// panel does.
	//
	// **Not the same lexical rule as the form above, and measured rather than
	// assumed.** That one turns on a *blank* after the brace and this one
	// turns on a `|` adjacent to it, and the two do not mix — measured
	// 2026-09-13 on bash 5.3.15:
	//
	//	${|REPLY=hi; }    hi             no blank needed after the `|`
	//	${| REPLY=hi; }   hi             a blank after it is ordinary body
	//	${ | REPLY=hi; }  syntax error   `|' unexpected, looking for `}'
	//	${echo hi; }      bad substitution
	//	${|}              empty, status 0
	//
	// So the third row is the discriminating one: with a blank first the body
	// has already begun and a `|` opening it is a pipeline with nothing on its
	// left. A single flag reading "brace, then a blank *or* a pipe" would
	// accept it.
	//
	// Separate from CurrentShellSubstitution because the panel separates them:
	// ksh93 has the blank form and answers “ `|' unexpected “ to this one
	// (#2656).
	ReplySubstitution bool

	// SubshellSubstitution reads `${(list)}` as a command substitution whose
	// body is the parenthesized subshell. ksh93 has it and nothing else in
	// the panel does: bash 5.3 and bash 3.2 call it a bad substitution, dash
	// and BusyBox ash the same, and zsh reads the parenthesis as its
	// expansion flags and complains about the letters inside.
	//
	// **Its extent is the parenthesis and not a command list, which is
	// measured rather than inferred from the four lines that work.** #2615
	// filed it as the blank form above with a subshell for a body — the
	// reading the isolation invites, since `${(cd /tmp; pwd)}` leaves the
	// caller's directory alone exactly as a subshell would. A list would go
	// on past the `)`, and it does not; measured 2026-09-13 on ksh93u+
	// 2012-08-01, in a script so that `ksh -n` can say which stage refused:
	//
	//	${(echo a)}              a
	//	${(echo a);}             `}' unexpected      refused while reading
	//	${(echo a) ;}            `}' unexpected      refused while reading
	//	${(echo a); echo b;}     `}' unexpected      refused while reading
	//	${(echo a)b}             `b}' unexpected     refused at the run
	//	${(echo a}               `(' unmatched       refused while reading
	//
	// So the `}` has to sit directly behind the matching `)` with nothing
	// between them, not even a blank, and the read-time refusals are what
	// say so: a body that were a list would take `;` and a second command
	// the way the blank form does. The blank form is the contrast, and it is
	// a list — `${ (echo a); echo b;}` runs both there.
	//
	// A separate flag from CurrentShellSubstitution for the reason
	// ReplySubstitution is one: the panel separates them (bash 5.3 has the
	// blank form and refuses this) and so does the grammar (that body ends
	// where a list ends, this one where its parenthesis does).
	//
	// `${((` is **not** this construct and is deliberately left out: it is
	// ksh93's braced arithmetic, `${((1+2))}` is 3 there and `${((echo hi))}`
	// an arithmetic syntax error, so the two adjacent parens are the whole of
	// the discriminator — `${( (1+2) )}` runs `1+2` as a command and reports
	// it not found. That spelling is refused here as it was before this flag,
	// and it is a construct of its own rather than a corner of this one.
	SubshellSubstitution bool

	// FlagGroupRefusedAtExpansion defers the refusal of a `${(…)…}` an
	// expansion flag group was written in to the moment the word expands,
	// in a grammar that has no such group and refuses every other unreadable
	// expansion while reading.
	//
	// The two halves are what make it a flag of its own rather than a
	// reading of BadSubstitutionAtParseTime. Measured 2026-09-18 on ksh93u+
	// 2012-08-01, a script file under `env -i PATH=/usr/bin:/bin LC_ALL=C`
	// with standard input on /dev/null, each body written as
	// `echo one; echo "[<body>]"; echo two`:
	//
	//	${(f)x}     one, then `syntax error at line 2: `x}]' unexpected`, 3
	//	${(qq)x}    one, then the same shape
	//	${(j:|:)x}  one, then a refusal naming a token of its own
	//	${%%%}      refused before `one` runs
	//	${~x}       refused before `one` runs
	//	${=x}       refused before `one` runs
	//	${+x}       refused before `one` runs
	//
	// So the deferral is the *group* and not the brace: four other
	// unreadable expansions in the same shell stop the input where it is
	// read, and only the parenthesis waits. The control that says it is the
	// expansion rather than the line is a group in a branch never taken —
	// `if false; then echo "${(f)x}"; fi` writes nothing and exits 0 there,
	// where `if false; then echo "${%%%}"; fi` is still refused.
	//
	// The refusal itself does not move: the same failure the parser builds
	// today, kind, token and word tail alike, is carried on the node and
	// written when the word expands. See ParamExpr.RefusedAtExpansion, and
	// Error.FlagGroupWordTail for the text that shell quotes back.
	//
	// Read only where ParamExpansionFlags is off and
	// BadSubstitutionAtParseTime is on, which is the one state that refuses
	// a group while reading at all.
	FlagGroupRefusedAtExpansion bool

	// BracedArithmeticExpansion reads `${((expr))}` as an arithmetic
	// expansion. ksh93 has it and nothing else in the panel does: the three
	// bash columns and dash call it a bad substitution, BusyBox ash words
	// that as a syntax error, and zsh reads the parenthesis as its expansion
	// flags and complains about the letters inside.
	//
	// **Arithmetic and not [Dialect.SubshellSubstitution] with a subshell in
	// it**, which is measured rather than assumed. The discriminator is one
	// space: `${((echo hi))}` is `echo hi: arithmetic syntax error` there,
	// where a subshell body would have run the command and printed `hi`, and
	// `${( (1+2) )}` — the same characters with the parens apart — reports
	// `1+2: not found`, a command by that name. Measured 2026-09-13 on
	// ksh93u+ 2012-08-01:
	//
	//	${((1+2))}        3
	//	${(( 1+2 ))}      3
	//	${(((1+2)))}      3
	//	${((1+2))}x       3x
	//	${((echo hi))}    echo hi: arithmetic syntax error
	//	${((1+2)) }       `end of file' unexpected
	//
	// The last row is why the `}` is required directly behind the `))` with
	// nothing between them, which is the same shape [Dialect.SubshellSubstitution]
	// has and the reason the two adjacent parens can settle the spelling
	// before anything else is read (#2725).
	BracedArithmeticExpansion bool

	// ConditionOperandMayOpenWithAGroup lets a `(` at the front of a
	// condition's **third word** belong to that word rather than being an
	// operator. `[[ 9 -gt ( 1 + 2 ) ]]` and `[[ -prefix 1 (f|ht)tp:// ]]`
	// are the two shapes, and four of the completion functions one shell
	// ships are written with them.
	//
	// Measured 2026-09-15, each probe in a script file of its own:
	//
	//	| probe                 | zsh 5.9.2 | bash 5.3 | bash-as-sh | bash 3.2 | ksh93u+ | dash | ash |
	//	| `[[ 9 -gt ( 1 ) ]]`   | true      | refused  | refused    | refused  | refused | refused | refused |
	//	| `[[ 2 -gt ( 1 + 2 ) ]]` | false   | refused  | refused    | refused  | refused | refused | refused |
	//	| `[[ -pfx 1 (a\|b)c ]]` | parsed   | refused  | refused    | refused  | refused | refused | refused |
	//
	// The three bash columns say “ unexpected argument `(' to conditional
	// binary operator “, ksh93 “ `(' unexpected “, and dash and BusyBox
	// ash — which have no `[[ ]]` at all — name the `(` where they wanted a
	// `then`. One column against six, so this is a dialect's grammar.
	//
	// **The position is the whole of the rule**, measured in the same run:
	// `[[ ( 1 -gt 0 ) ]]` is the condition's own grouping paren at the first
	// word, `[[ -n ( a ) ]]` and `[[ -pfx ( a ) ]]` are “ parse error near
	// `(' “ at the second, `[[ -pfx 1 ( a ) ]]` parses at the third, and
	// `[[ -pfx 1 2 ( a ) ]]` is refused again at the fourth. A `!` or a
	// connective starts the count over.
	//
	// The group is not a *pattern* — that is [Lexer.inPattern], which is set
	// for `==`, `=` and `!=` and was already right for them. What this adds
	// is the same lexing for every other operator, and what becomes of the
	// word afterwards is the operator's business: `[[ 9 -gt ( 1 -gt 0 ) ]]`
	// hands `( 1 -gt 0 )` to the arithmetic evaluator, which complains about
	// it.
	ConditionOperandMayOpenWithAGroup bool

	// PidBraceGroupIsText makes a `{ … }` written immediately after `$$` a
	// run of characters: a blank, a newline or an operator inside it is text
	// rather than a separator, and the braces are a brace *list* nowhere.
	//
	// One shell in the panel has it, and one of the completion functions it
	// ships is written with it — by accident, on the evidence, since the line
	// carries `$${(s<,>)…}` where `${(s<,>)…}` was plainly meant and "works"
	// only because the stray `$` turns the rest into text. Measured
	// 2026-09-15, each probe in a script file of its own, printed one
	// argument per `[%s]` so the word count is visible:
	//
	//	| probe            | zsh 5.9.2    | bash 5.3 | bash-as-sh | bash 3.2 | ksh93u+ | dash | ash |
	//	| `$${a b}`        | `{a b}`      | `{a` `b}` | `{a` `b}` | `{a` `b}` | `{a` `b}` | `{a` `b}` | `{a` `b}` |
	//	| `$${a;b}`        | `{a;b}`      | refused  | refused    | refused  | refused | refused | refused |
	//	| `$${a>b}`        | `{a>b}`      | redirects | redirects | redirects | redirects | redirects | redirects |
	//	| `$${a{b,c}d}`    | `{abd}` `{acd}` | `{a{b,c}d}` | `{a{b,c}d}` | `{a{b,c}d}` | `{a{b,c}d}` | `{a{b,c}d}` | `{a{b,c}d}` |
	//
	// Six columns against one, so this is a dialect's grammar rather than a
	// bug. The `>` row is what says the run is lexical and not a quoting
	// rule: every other column writes the word into a file called `b}`.
	//
	// **The outer pair alone is text**, which the fourth row is there to
	// pin: a brace pair *inside* the run still expands, so a reading that
	// emitted the whole run as one literal span would be wrong in the other
	// direction. Everything else inside expands as it always did — `$${a$(echo
	// X)b}` is `{aXb}` and `v=V; $${x${v}y}` is `{xVy}` — so this is about
	// the braces and the separators, not about expansion.
	//
	// **And the outer pair is text for the list reading only.** A range
	// written straight into it still fires: `$${1..3}`, `$${a..c}` and
	// `$${1..5..2}` all expand there, where `$${a,b}`, `$${1,2}`,
	// `$${a,1..3}` and `$${1..2 3}` are all one word. That is [Span.PidBrace]
	// and the brace expander rather than anything here, but it belongs beside
	// the rest of the rule: a reading that made the whole pair inert passes
	// the table above and loses the ranges, and did.
	//
	// **`$$` and nothing else, touching the brace.** `$!{a,b}`, `$?{a,b}`,
	// `$-{a,b}` and `$#{a,b}` all brace-expand there, and so do `x{a,b}`,
	// `$x{a,b}` and `${x}{a,b}`; `$$x{a b}` and `$$ {a b}` are refused along
	// with the rest of the panel, because the `}` left over is the reserved
	// word. It survives `emulate sh` and `emulate ksh` and the `sh` argv0,
	// so it is the lexer rather than an option.
	//
	// An unmatched one is refused rather than left as text — `closing brace
	// expected`, reported at the end of the input, because the run swallows
	// every line after it looking for the match.
	PidBraceGroupIsText bool

	// CaseHeaderSpansSeparators lets a `;` stand in a `case` header wherever
	// a newline may: between the subject and the `in`, and after the `in`.
	//
	// One shell in the panel takes it, and nine of the completion functions
	// zsh ships write the first spelling. Measured 2026-09-15, each probe in
	// a script file of its own, against a `case` with one arm:
	//
	//	| probe          | zsh 5.9.2 | bash 5.3 | bash-as-sh | bash 3.2 | ksh93u+ | dash | ash |
	//	| `case x; in`   | runs      | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x ; ; in`| runs      | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x;⏎in`   | runs      | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x in;`   | runs      | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x;; in`  | refused   | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x in ;;` | refused   | refused  | refused    | refused  | refused | refused | refused |
	//	| `case x & in`  | refused   | refused  | refused    | refused  | refused | refused | refused |
	//
	// So it is the `;` and not the whole separator family: `;;` is the arm
	// terminator and `&` is a job-control operator, and neither becomes a
	// newline here. The last three rows are why this is one flag and not
	// "anything that ends a statement".
	//
	// Separate from [Dialect.SeparatorWhereACommandBelongs], which is about a
	// `;` where a *command* belongs: no command belongs in a `case` header,
	// so that flag says nothing about this and a shell could have either
	// without the other.
	CaseHeaderSpansSeparators bool

	// CasePatternAcceptsOperator lets an operator stand where a case pattern
	// belongs, which produces an arm with no patterns at all.
	//
	// Measured rather than inferred from the error it causes, because the
	// error is not the whole of it: `case a in & ) echo hit;; *) echo miss;;
	// esac` *parses and runs* in dash, and prints miss — the `&` is consumed
	// and the arm it opens matches nothing, not even `&` and not even the
	// empty string. `&a )` then fails at the word and `a& )` at the `&`, so
	// what dash accepts is one operator where the pattern list would start
	// and nothing else.
	//
	// The other three reject it outright, which is why `;;&` in a dialect
	// without that terminator reaches a different diagnosis there: dash is
	// already past the `&` and complaining about the `esac` where the `)`
	// should be.
	CasePatternAcceptsOperator bool

	// CasePatternMayBeEmpty lets a `case` arm's pattern list carry a pattern
	// written as nothing, which then matches only the empty string:
	// `(|https|git|ftp)` is the idiom for "one of these schemes, or none",
	// and it is what a widely installed zsh library's own startup path is
	// written with.
	//
	// Measured 2026-09-06 with `-n` over a script file, because the question
	// is whether it parses. zsh 5.9.2 accepts it; bash 5.3.15, the same
	// binary as `sh`, bash 3.2.57, ksh93u+ and dash all refuse, at three
	// statuses and in three wordings, and each blames a different token
	// depending on where the emptiness is.
	//
	// Every place a separator can put one is allowed: `(|a|b)`, `(a||b)`,
	// `(a|b|)`, `(|)` and `(||)` all parse there, with or without the arm's
	// optional open paren.
	//
	// So is the whole list, where the arm's parentheses are there to hold
	// it. `case "" in ( ) echo em;; (*) echo star;; esac` prints `em` on zsh
	// 5.9.2 and prints `star` for a subject of one blank, measured
	// 2026-09-12 — the empty string, not the blank, that dialect trimming
	// blanks either side of a list. Written *without* the parentheses there
	// is nowhere for an empty list to be and `case a in ) …` is refused.
	//
	// `()` is refused as well, and this file said for a while that the
	// reason was the missing separator. It is not: the pair is one token to
	// the same dialect — see [Dialect.EmptyParensAreOneToken] — so the `(`
	// never opens an arm, and the two characters with a blank between them
	// are the line above (#1111).
	//
	// It is a grammar flag and not a matching rule. A group with an arm that
	// matches nothing already stands for nothing in every shell that has the
	// construct at all — `@(|a)b` matches `b` in bash and ksh93 alike — so
	// what divides the panel here is only whether the pattern *list* may
	// have such an alternative written into it.
	//
	// **The wholly empty list is [Dialect.CasePatternListMayBeEmpty]**, and
	// it was part of this flag until #4817. The two parted because the
	// dialect that has them moves one and not the other; the measurement is
	// under that field.
	CasePatternMayBeEmpty bool

	// CasePatternListMayBeEmpty lets a `case` arm's parenthesized pattern
	// list be written as nothing at all: `( )`, which then matches only the
	// empty string.
	//
	// It was part of [Dialect.CasePatternMayBeEmpty] until the pair was
	// measured apart. The dialect that has both carries an option — the one
	// that also decides whether a blank inside the list is a character of a
	// pattern, see [Dialect.CasePatternListSpansBlanks] — and it takes this
	// reading away and leaves the empty *alternative* standing. Measured on
	// `/opt/homebrew/bin/zsh` — `zsh 5.9.2 (aarch64-apple-darwin25.4.0)` —
	// `-f` over a script file under `set -n`, 2026-09-27, with `shglob`
	// moved on the line after an `emulate`:
	//
	//	                                        off      on
	//	case "" in ( ) echo em;; esac           parses   refused
	//	case "a b" in (a b) echo m;; esac       parses   refused
	//	case a in (a|b|) echo t;; esac          parses   parses
	//	case "" in (|https|git) echo e;; esac   parses   parses
	//	case a in (|a) echo e;; esac            parses   parses
	//	case a in (a||b) echo e;; esac          parses   parses
	//	case a in (|) echo e;; esac             parses   parses
	//	case a in (||) echo e;; esac            parses   parses
	//
	// The `(|)` row is the one that says the noun is the *list* and not the
	// emptiness: it holds two empty alternatives and nothing else, and it
	// survives the option that refuses `( )`. A field that moved both
	// refuses six of these eight under `emulate sh` and `emulate ksh`, where
	// the reference takes them (#4817).
	CasePatternListMayBeEmpty bool

	// CaseTerminatorIsAPatternAfterTheHeader makes `esac` an ordinary word
	// where the *first* arm's pattern list begins, so a `case` whose subject
	// list is written on one line has no terminator until a newline has been
	// read.
	//
	// "The header" is either opener: this shell writes `case x { … }` as well
	// as `case x in … esac` — see [Dialect.CaseBraceBody] — and the reading
	// follows the position rather than the word. `case esac { esac) echo
	// hit;; }` prints `hit` there too.
	//
	// ksh93u+ alone, and it is not the rule #773 filed it as. That issue read
	// `case x in esac` being refused there as "a case must have an arm", and
	// the discriminator says otherwise. Measured 2026-09-12, `env -i
	// PATH=/usr/bin:/bin` with a scratch HOME, over `-c` and a script file
	// alike:
	//
	//	case x in esac                    `case' unmatched     — a pattern,
	//	                                  and then no `)`
	//	case x in esac; echo done         `;' unexpected       — the same
	//	case x in⏎esac                    runs                 — a newline
	//	                                  makes it the terminator again
	//	case esac in esac) echo hit;; esac
	//	                                  prints `hit`         — so it really
	//	                                  is being read as a pattern
	//	case x in y) ;; esac) echo hit;; esac
	//	                                  `)' unexpected       — only the
	//	                                  first arm's position, not after a
	//	                                  `;;`
	//	case x in \⏎esac                  `newline' unexpected — a line
	//	                                  continuation is not a newline
	//	case x in # c⏎esac                runs                 — a comment
	//	                                  ends the line and the newline counts
	//	case esac in (esac) echo hit;; esac
	//	                                  prints `hit` in all six — a paren in
	//	                                  front takes the reservation away
	//	                                  everywhere and needs no flag
	//
	// The fourth row is what makes this additive rather than a refusal: the
	// shell *accepts* a program the other five refuse, and the one-line
	// `case x in esac` is refused as a consequence of that acceptance rather
	// than as a rule of its own. Writing it the other way round — a flag that
	// simply refused an armless `case` — would have refused `case x in⏎esac`
	// too, which ksh93 runs.
	CaseTerminatorIsAPatternAfterTheHeader bool

	// CaseBraceBody lets a `case` be written with braces in place of `in` …
	// `esac`: `case x { x) echo hit;; }`.
	//
	// **Two shells have it and they draw it differently**, which is why this
	// is a spelling rather than a bool. Measured 2026-09-12 under `env -i
	// PATH=/usr/bin:/bin` with a scratch HOME, ksh93u+ and zsh 5.9.2; dash,
	// bash 5.3, that binary as `sh` and bash 3.2 refuse every row.
	//
	//	probe                              ksh93            zsh
	//	case x { x) echo hit;; }           hit              hit
	//	case x { }                         runs             runs
	//	case x { (x) echo hit;; }          hit              hit
	//	case x { x) echo hit;; esac        `case' unmatched hit
	//	case x in x) echo hit;; }          `case' unmatched hit
	//	case esac { esac) echo hit;; }     hit              parse error at `)`
	//	case x { x) echo hit }             `case' unmatched hit
	//	case x {x) echo hit;; }            `{x' unexpected  hit
	//
	// Rows 4 and 5 are the split this field records: ksh93 **pairs** the two
	// words and zsh takes either closer after either opener. Rows 6, 7 and 8
	// are the other three flags showing through rather than anything of this
	// one's — [Dialect.CaseTerminatorIsAPatternAfterTheHeader],
	// [Dialect.CloseBraceAlwaysReserved] and [Dialect.OpenBraceNeedsNoBlank],
	// each of which one of the two shells has and the other does not.
	CaseBraceBody CaseBraceSpelling

	// CasePatternListSpansNewlines makes a newline inside a `case` arm's
	// **parenthesized** pattern list an ordinary character of the pattern
	// rather than the end of a word or a statement.
	//
	// zsh alone. Measured 2026-09-07 over a script file, with a scratch HOME
	// and ZDOTDIR:
	//
	//	case a in (a|      zsh: `m`, status 0
	//	b) echo m;; *) echo no;; esac
	//
	// and every other shell in the panel refuses the line — bash 5.3.15, the
	// same binary as `sh`, and bash 3.2.57 blame the newline, dash blames the
	// word on the next line, ksh93u+ blames the newline at line 2. Three
	// wordings and three statuses.
	//
	// What zsh matched is the *first* alternative, and the second is two
	// characters — a newline and a `b`. Four probes say so and no fewer will:
	// subject `a` gives `m`, subject `b` gives `no`, subject `""` gives `no`,
	// and a subject holding a newline before the `b` gives `m`. `functions`
	// on a function carrying the arm prints the newline back inside the
	// pattern, which is the same fact from the writing side.
	//
	// It is not a rule about the `|`. A newline anywhere inside the list is
	// text: `(a` newline `)` is the two-character pattern, so subject `a`
	// does *not* match it, and `(a` newline `|b)` puts the newline on the end
	// of the first alternative. And it needs the arm's **paren**: `case a in
	// a|` newline `b)` is a parse error in zsh too, so what opens this is the
	// parenthesis and not the position.
	//
	// This is #1083's `|` seen from the other side and a bigger claim than
	// that one. `CasePatternMayBeEmpty` lets an alternative be *written* as
	// nothing, which is a rule about the list; this says the separator does
	// not end the word at all, which reaches the lexer.
	CasePatternListSpansNewlines bool

	// CasePatternListPipeIsOnlyASeparator keeps a `|` inside a `case` arm's
	// **parenthesized** pattern list from joining the character after it into
	// a two-byte operator: the pipe is the alternation separator there and
	// nothing else.
	//
	// zsh alone, and only one operator can be written where it shows —
	// `|&`, which every other member of the panel that has the operator
	// lexes whole. Measured 2026-09-12, `-n` over a script file:
	//
	//	case a in (a|&b) …    zsh `&`      bash 5.3 / ksh93 `|&`
	//	case a in (a|&|b) …   zsh `&|`     bash 5.3 / ksh93 `|&`
	//	case a in (a|&&b) …   zsh `&&`     — the discriminator
	//	case a in a|&|b) …    zsh `|&`     — the control
	//
	// The third row is what says the rule belongs to the `|` and not to the
	// `&`: an `&&` after the separator is still one token, so it is the pipe
	// that stops reading rather than the ampersand that starts. The fourth is
	// what confines it to the parentheses — written without the arm's paren,
	// the same characters lex as `|&` in zsh too, which is the answer every
	// other column gives everywhere.
	//
	// It is a lexical rule and shows only as the token a refusal names: no
	// line that parses is read differently, because `|&` cannot stand in a
	// pattern list under either reading.
	CasePatternListPipeIsOnlyASeparator bool

	// CasePatternListSpansBlanks makes a blank inside a `case` arm's
	// **parenthesized** pattern list an ordinary character of the pattern
	// rather than the end of a word — so `(a b)` is the three-character
	// pattern and not two words, one of which nothing can be done with.
	//
	// zsh alone, and the line it is needed for is `VCS_INFO_get_data_git`,
	// which every prompt drawing a git segment autoloads: line 234 of it is
	// `(”(x|exec) *)`, a group, a blank and more pattern.
	//
	// Measured on zsh 5.9.2, 2026-09-10, `-c` under `env -i`, against
	// bash 5.3.15 (which is also `sh`), bash 3.2.57, ksh93u+ and dash:
	//
	//	case 'a b' in (a b) echo hit;; (*) echo no;; esac
	//
	//	zsh 5.9.2   `hit`, status 0
	//	bash 5.3    “ syntax error near unexpected token `b' “, status 2
	//	bash 3.2    the same sentence, status 2
	//	ksh93       “ syntax error at line 1: `b' unexpected “, status 3
	//	dash        `Syntax error: word unexpected (expecting ")")`, 2
	//
	// The paren is what licenses it, exactly as it licenses the newline
	// above: `case 'a b' in a b) …` is “ parse error near `b' “ in zsh too,
	// so this is a rule about the parenthesized form and not about the
	// position. That control is what separates it from "a word may follow a
	// pattern".
	//
	// The blanks are **taken verbatim**, which four probes say and no fewer
	// will: `(a b)` misses `a  b` and misses `ab`, `(a  b)` matches `a  b`,
	// and `(a<tab>b)` misses `a b` while matching `a<tab>b`. So a run is not
	// collapsed and a tab is not a space — the pattern is the source text.
	//
	// And they are text only where the pattern *continues* after them. A run
	// of blanks in front of the `|` that separates two alternatives, or in
	// front of the `)` that closes the list, still separates nothing and is
	// dropped: `( a b )` matches `a b` and misses ` a b ` and `a b `, and
	// `(a b |z)` matches `a b`. That is why this is not `isBlank` losing its
	// meaning inside the list — `(a | b)` is still two alternatives, and it
	// is two in every shell in the panel.
	//
	// The two flags compose the way the shell does. Where a newline is text
	// as well, a blank beside one is text too: `(a` blank newline blank `b)`
	// matches exactly that subject and misses `a` newline `b`.
	//
	// An operator is still an operator. `(a >b)` is “ parse error near `>' “
	// in zsh and the blank before it is no part of any pattern, so this
	// admits the words a pattern can hold and nothing else.
	CasePatternListSpansBlanks bool

	// CaseWordRunsOutAsANewline reads the end of the input where a `case`
	// wants a **word** — its subject, and an arm's pattern — as the
	// **newline** that would have ended the line, rather than as the input
	// running out inside an unfinished `case`.
	//
	// It decides only the wording of a failure, and the failure is the same
	// one either way: a pattern was begun and its `)` never arrived. What
	// differs is whether the shell blames the construct that never closed or
	// the token that stood where the `)` belonged.
	//
	// bash alone, and the way to see that it is bash's own reading rather
	// than an accident of where the text stopped is to write the same script
	// twice, once with a trailing newline and once without. Measured
	// 2026-09-13, `env -i PATH=/usr/bin:/bin` with a scratch HOME, over `-c`,
	// on `case x in x) :;; zzz` — the pattern of a second arm, begun and
	// unfinished:
	//
	//	shell        without a trailing newline              with one
	//	bash 5.3     `newline' unexpected, line 1            the same, line 1
	//	bash 3.2     the same                                the same
	//	bash as sh   the same                                the same
	//	dash         end of file unexpected (expecting ")")  newline unexpected …
	//	ash          unexpected end of file (expecting ")")  unexpected newline …
	//	ksh93        `case' unmatched                        `newline' unexpected
	//	zsh          parse error near `zzz'                  parse error near `\n'
	//
	// Five of the six columns answer differently in the two spellings, which
	// is what says they read the end of the input as the end of the input.
	// bash gives one answer to both, echoes the same source line for both,
	// and numbers both at the line the pattern is on rather than at the line
	// after — so to bash the run-out *is* the newline.
	//
	// This shell already agrees with bash where the newline is really there:
	// the token stands, the pattern position refuses it, and the bytes match.
	// So the flag is not a second wording but the same one reached from the
	// other spelling, which is why it is a token substitution rather than a
	// message.
	//
	// **It is where a word belongs and nothing wider.** `for i in a b`, `if
	// true`, `while true`, `{ echo a`, `select i in a` and an arm's *body*
	// running out are all the unterminated shape in bash too, measured in the
	// same run. A flag that made the end of input a newline generally would
	// encode a rule bash does not have.
	//
	// The **subject** is the second position and was measured later, on
	// 2026-09-19 while routing that refusal through the ordinary
	// unexpected-token sentence (#3758). This comment said "the pattern
	// position and nothing wider" until then, on a sweep that had asked
	// every neighboring construct and not the one word inside `case` it had
	// not reached. A file holding `case` and nothing else, script route,
	// `env -i PATH=/usr/bin:/bin LC_ALL=C` with standard input on the null
	// device:
	//
	//	shell        without a trailing newline              with one
	//	bash 5.3     `newline' unexpected, line 1, echoing `case'   the same
	//	bash 3.2     the same                                the same
	//	bash as sh   the same                                the same
	//	dash         end of file unexpected (expecting word) newline unexpected …
	//	ash          unexpected end of file (expecting word) unexpected newline …
	//	ksh93        `case' unmatched                        `newline' unexpected
	//	zsh          parse error near `case'                 parse error near `\n'
	//
	// The same six-column split as the pattern, read the same way: bash
	// answers both spellings alike and the rest do not.
	CaseWordRunsOutAsANewline bool

	// FuncDefAtParen commits to a function definition as soon as a name is
	// followed by `(`, rather than requiring the `()` pair.
	//
	// It decides *which token* a malformed one is blamed on, which is why it
	// is a grammar flag and not a wording: `f ( x )` is "x" in bash and dash,
	// which are already inside a definition looking for `)`, and "(" in
	// ksh93, which never entered one. Reached most often through a construct
	// a dialect does not have — `[[ ( -n x ) ]]` is a definition of a
	// function called `[[` to a shell without `[[`.
	FuncDefAtParen bool

	// ParenAfterANameIsRefusedWhereTheNextTokenStands numbers the refusal of
	// a `(` that follows a command's only word at the line of the token
	// after the `(`, newlines skipped, rather than at the `(`'s own line.
	// The token blamed is still the `(`.
	//
	// ksh93 alone, and only where the word could have been a name: one word,
	// written with no expansion in it — quoted or not — and nothing else in
	// front of the `(`. Measured 2026-10-04 on ksh93u+ 2012-08-01 from a
	// script file, each shape followed by two blank lines and an `x`:
	//
	//	echo (       line 3      "echo" (     line 3      a-b (     line 3
	//	$x (         line 1      a=b c (      line 1      c >f (    line 1
	//	echo ( x     line 1      echo (;      line 1
	//
	// and `echo (#i)ab*` with nothing after it is line 2, the end of the
	// input: what follows the `(` there is a comment.
	ParenAfterANameIsRefusedWhereTheNextTokenStands bool

	// WordAtParenIsARefusedName reads a quoted word, or one holding an
	// expansion, as the name of a definition where the dialect commits at the
	// paren ([Dialect.FuncDefAtParen]), and refuses that name once the
	// parentheses close — where without it the `(` after such a word is
	// refused where it stands. An assignment word is still an assignment.
	// Measured 2026-10-04 on dash 0.5.12: `"f"() { :; }`, `”() { :; }`,
	// `"f g"() { :; }` and `$f() { :; }` are each `Bad function name`, and
	// `"a"=(x)` is `word unexpected (expecting ")")`.
	WordAtParenIsARefusedName bool

	// ParenAfterAWordEndsTheCommand ends a simple command at a `(` standing
	// after one of its words, and leaves the refusal to whatever was reading
	// the list the command is in — so the closer *that* construct wanted is
	// what the complaint expects. Measured 2026-10-04 on dash 0.5.12 and in
	// the pinned BusyBox ash image:
	//
	//	{ a=(x); }              "(" unexpected (expecting "}")
	//	if a=(x); then :; fi    "(" unexpected (expecting "then")
	//	( a=(x) )               "(" unexpected (expecting ")")
	//	a=(x)                   "(" unexpected
	//
	// The other dialects that print an expectation have none here.
	ParenAfterAWordEndsTheCommand bool

	// CaseRunsOutAtAPattern reads the end of the input, where a `case` arm's
	// pattern would begin, as a pattern that never reached its `)` — and not
	// as a `case` waiting for its `esac`. Measured 2026-10-04 on dash 0.5.12
	// and in the pinned BusyBox ash image: `case x in` and `case x in x) :
	// ;;` are both `end of file unexpected (expecting ")")`. The other three
	// name the construct or the last token, and say the same either way.
	CaseRunsOutAtAPattern bool

	// CommentRunningOutCountsANewline numbers the end of the input one line
	// further on where a comment runs into it with no newline of its own, as
	// though the comment had been ended by one. ksh93: measured 2026-10-04
	// on ksh93u+ 2012-08-01 under `-c`, `a=(` and `a=( x` are refused at
	// line 1 and `a=( # c`, `a=( x #c` and `a=( (#i)` at line 2 — the last
	// because `#i)` is a comment there.
	CommentRunningOutCountsANewline bool

	// FuncBodyMustBeCompound refuses `f() echo hi`: bash alone wants a
	// compound command after the parens, where dash, ksh93 and zsh take a
	// simple command as a one-command body and run it.
	//
	// The `function` keyword's body is held to the same rule where the
	// dialect has both — measured 2026-09-11 over a file holding `function a`
	// and `echo B`, bash 5.3.15, bash 3.2.57 and bash-as-sh all answer
	// “syntax error near unexpected token `echo' “ at status 2, which is
	// the sentence and the status the parenthesized form gets from them. It
	// is not the whole of the keyword form's question, though, because the
	// shell that originated the keyword is stricter still: see
	// [Dialect.FunctionKeywordBodyMustBeBraceGroup].
	FuncBodyMustBeCompound bool

	// FunctionKeywordBodyMustBeBraceGroup refuses every body after the
	// `function` keyword but a brace group — ksh93's rule, and narrower than
	// [Dialect.FuncBodyMustBeCompound] rather than a spelling of it: a
	// compound command is enough for the dialect that wants one, and this one
	// wants the braces.
	//
	// Measured 2026-09-11 over a file holding `function a`, the body, and a
	// call, ksh93u+ against the three bash columns:
	//
	//	body            bash              ksh93
	//	echo B          at `echo', st 2   at `echo', st 3
	//	(( 1 ))         runs              at `((', st 3
	//	( echo B )      runs              at `(', st 3
	//	for … done      runs              at `for', st 3
	//	{ echo B; }     runs              runs
	//
	// It is the keyword's question alone. The same shell takes `f() echo hi`
	// through the parenthesized form and refuses only what that body
	// redirects, which is [Dialect.FuncBodyTakesNoRedirection] — so the two
	// spellings of a definition are not one rule there, and a flag that tried
	// to be both would have to pick one of the two answers and be wrong about
	// the other.
	FunctionKeywordBodyMustBeBraceGroup bool

	// FuncBodyTakesNoRedirection refuses a redirection in a function body
	// that is not compound: ksh93 takes `f() echo hi` and refuses `f() >out`,
	// `f() echo hi >out` and `f() x=1 >out`, blaming the operator itself —
	// “ `>' unexpected “, and “ `>&' “ for `2>&1`. A braced body is not
	// this rule and `f() { :; } >out` is accepted there.
	//
	// It is narrower than FuncBodyMustBeCompound rather than a weaker form of
	// it, which is what makes it a second flag: the shell that wants a
	// compound body refuses the simple command outright, and this one takes
	// the command and refuses only what it redirects. Two of the four accept
	// both, so the panel is three ways here and not two.
	FuncBodyTakesNoRedirection bool

	// EmptyParensAreOneToken lexes `()` as a single token where the rest of
	// the panel reads two, which is visible only when a diagnostic names the
	// last token it read: zsh answers `f()` with “parse error near `()' “
	// where naming the closing paren alone would say “ `)' “.
	//
	// A granularity fact rather than a wording one, which is why it is here
	// and not in Diagnostics — the two halves of `f( )` are not this token in
	// that shell either, and it declines to read that as a definition at all.
	//
	// **It is the pair wherever a refusal falls on the first of them**, and
	// not only inside the production that consumes them. An assignment in
	// front of the name puts the parenthesis outside the definition path —
	// the word list is read as a command's arguments and the `(` after them
	// is refused by the ordinary rule — and the pair is still named there.
	// Measured 2026-09-12 on zsh 5.9.2:
	//
	//	x=1 f () { echo X; }     parse error near `()'
	//	x=1 a b () { echo X; }   parse error near `()'
	//	x=1 f ( ) { echo X; }    parse error near `}'    — a blank inside,
	//	                                                   so two tokens and
	//	                                                   a subshell
	//
	// The third row is why the join reads the source for an *adjacent* `)`
	// rather than skipping blanks the way the definition path's lookahead
	// does: one blank and the two characters are not this token (#1846).
	EmptyParensAreOneToken bool

	// FunctionNamePunctuation lets a POSIX-form or keyword-form function
	// name carry punctuation — `f-g()`, `a.b()`, `:zi-reload-and-run()` —
	// which every panel shell but dash parses. dash refuses the name
	// outright (`Bad function name`), and what a shell that parsed one
	// *does* with it is the interpreter's question: ksh93 parses every name
	// below and stops the script at the definition.
	//
	// The characters are `!#%+,-./:@]^`, plus every byte above ASCII, and
	// the set is measured rather than chosen. Each of the ninety-three
	// printable ASCII punctuation marks was put at the front, the middle and
	// the end of a name, in both definition forms, in a *file* read with
	// `-n` — a command string is the wrong instrument here, because a `-c`
	// argument that begins with `-` never reaches the grammar at all. Those
	// twelve are the ones all five of bash 5.3, bash 3.2, bash-as-sh, ksh93
	// and zsh accept in every position.
	//
	// What was left out, and why, because the omissions are the interesting
	// half:
	//
	//   - `=` is excluded everywhere and by everyone, above.
	//   - `$`, `\`, `'`, `"` and a backquote are quoting or expansion
	//     operators, so the word they appear in is not one literal span and
	//     never reaches this test.
	//   - `*`, `?`, `[`, `{` and `~` split the panel: bash and zsh parse
	//     them and ksh93 refuses. `]` does not split, which is the shape of
	//     the disagreement — it is the *opening* of a pattern that ksh93
	//     will not have in a name. They are not in the corpus either,
	//     because the case that would record them cannot be run: zsh parses
	//     `f*g(){ :; }` and then expands the word, so what it reports is
	//     `no matches found` rather than anything about a name.
	//   - `}` splits the other way: zsh alone refuses `f}`, because a close
	//     brace is reserved wherever a word may stand there — the same fact
	//     [Dialect.CloseBraceAlwaysReserved] records.
	//   - A leading `#` never arrives, being a comment, though `f#g` is
	//     unanimous and is in the set.
	//
	// Position turned out not to matter to any shell once the invocation
	// artifact above was removed, so this is a character class and not a
	// grammar of names.
	FunctionNamePunctuation bool

	// FunctionNameMayHoldAnEquals lets a POSIX-form function name carry `=`
	// where the word is not an assignment — where what stands before its
	// first `=` is not a name. Measured 2026-10-04 on bash 5.3.20:
	// `2=() { echo hi; }; echo ok` and `1x=() { echo in; }; 1x=` define and
	// call the functions, and so `1=(a b)` is a definition whose `(` wants a
	// `)` — `syntax error near unexpected token `a'` — and `2=()` one whose
	// body never came (`;`). `a=b=() …` and `f=g() …` stay assignments,
	// refused at the `(`. bash only; the other columns either assign to a
	// positional (zsh) or refuse the parenthesis.
	FunctionNameMayHoldAnEquals bool

	// FunctionKeywordNameIsAnyWord makes the word after the `function`
	// keyword a name whatever its text is: `function ” { … }`,
	// `function 'a b' { … }`, `function 'a;b' { … }`, `function '@#%' { … }`
	// are all definitions, callable by those names, listed by `functions`
	// and `typeset -f` and removed by `unfunction`.
	//
	// One shell in the panel does this and no other, and the split is not
	// only over whether it works — it is over *when* they say so, which is
	// why the answer is here rather than in Diagnostics. Measured 2026-09-08,
	// `sh -c "function ” { echo b; }; echo done"`, and the same seven
	// answers come back for every name below:
	//
	//	zsh 5.9.2   done, status 0, nothing on stderr, and `functions`
	//	            lists the definition under that name
	//	bash 5.3    `not a valid identifier` on stderr naming the quoted
	//	            word, then `done` — refused where it runs, and the
	//	            script carries on at status 0
	//	bash 3.2    the same two lines
	//	bash-as-sh  the same diagnostic, then nothing: fatal, status 2
	//	ksh93       `: invalid function name`, status 1, nothing after
	//	dash        `Syntax error: "}" unexpected` — no `function` keyword
	//	            at all, so the refusal is about the brace
	//	BusyBox ash `done`, status 0, nothing on stderr — and nothing
	//	            defined either: `function 'a b' { echo B; }` is
	//	            followed by `a b: not found`, where `function f { … }`
	//	            defines perfectly well. It has the keyword and drops
	//	            the definition without a word (re-measured 2026-09-21)
	//
	// So four of the seven parse the construct and refuse the *name*, dash
	// alone refuses to parse it, and ash parses it and quietly defines
	// nothing. This parser has no separate definition-time name check, and
	// building one for a construct four columns refuse and one accepts would
	// be a lot of machinery to arrive at the same diagnostic those four
	// already print. The flag says whether the word is a name; the
	// four that refuse it keep refusing it here, at their own wording, which
	// is where they refused it before this flag existed.
	//
	// The rule is that there is no rule, which is what makes this a flag
	// rather than a wider [Dialect.FunctionNamePunctuation]: the shell reads
	// a word and the word is the name. Measured over the thirty-two printable
	// ASCII punctuation marks in `function a<c>b { :; }`, both quoted and
	// bare — quoted, every one of them defines; bare, the ones that define
	// are `! # $ % + , - . / : < = > @ ] ^ _ \ { } ~`, and the rest fail for
	// reasons that are not about names at all. `& ( ) ; |` are operators, so
	// the word ended before them; `"`, `'` and a backquote open quoting that
	// never closes; and `* ? [` are **matched against the filesystem** —
	// `no matches found: a*b`, `no matches found: a?b`, `bad pattern: a[b`.
	//
	// That last group is the one exception this parser keeps. A name whose
	// unquoted literal text holds `*`, `?` or `[` is still refused, because
	// matching a function name against the filesystem is not implemented and
	// a definition of a function literally called `a*b` would be a plausible
	// wrong answer where a refusal is a visible one. The wording is the
	// keyword's own rather than that shell's, which is a diagnostics gap and
	// not a semantic one. Quoted, the same characters are ordinary text and
	// are taken: `function 'a*b' { :; }` defines it and `functions` writes it
	// back as `'a*b'`.
	//
	// The neighboring readings, all measured on zsh 5.9.2, because they are
	// what says this is a *name* rather than a hole in a check:
	//
	//	function "" { echo b; }   the same definition, either spelling
	//	function ” () { … }      the hybrid form takes it too
	//	n=”; function "$n" { … } an expanded empty name defines it too
	//	n=”; function $n { … }   defines nothing at all, silently, at
	//	                          status 0 — an unquoted empty expansion is
	//	                          no word, and a keyword with no name words
	//	                          left defines no functions
	//	e=(a b); function $e {…}  defines `a` and `b`, both with that body
	//
	// The last two are why this is about the name and not about the *word
	// list* being empty: `function` with no name words at all is
	// [Dialect.AnonymousFunction], and a name list that expands to nothing is
	// a third thing again, which this parser does not read.
	//
	// The keyword form only. The POSIX `name()` form refuses a quoted word
	// before any name test is reached, and its panel is a different one —
	// ksh93 reads `'q'() { … }` as a definition too and then refuses the
	// empty name where zsh takes it (#1561).
	FunctionKeywordNameIsAnyWord bool

	// FunctionNameIsAnyWord is that flag for the POSIX `name()` form:
	// `'a b'() { … }`, `a\ b() { … }`, `”() { … }`, `'a;b'() { … }` are
	// definitions, callable by those names, listed by `functions` and
	// `typeset -f` and removed by `unfunction`.
	//
	// Separate from the keyword flag because the panel is a different one and
	// the two came apart in this parser: the keyword form has taken any word
	// since #1548, and `name()` refused a quoted word before any name test
	// was reached — so `function a\ b { … }` defined a function here and
	// `a\ b() { … }`, the same name, was “ parse error near `(' “. One
	// shell writing both spellings could not be read.
	//
	// Measured 2026-09-10 from a script file under `env -i`, `a\ b() { echo
	// b; }; echo after`:
	//
	//	zsh 5.9.2   `after`, status 0, nothing on stderr, and `typeset -f`
	//	            lists the body under `'a b'`
	//	bash 5.3    “ `a\ b': not a valid identifier “ on stderr, then
	//	            `after` — refused where it runs, script carries on at 0
	//	bash 3.2    the same two lines
	//	bash-as-sh  the same diagnostic and nothing after: fatal, status 2
	//	ksh93       `a b: invalid function name`, status 1, nothing after
	//	dash        `Syntax error: Bad function name`, status 2 — the only
	//	            column that refuses to *parse* it
	//
	// So five of the six read the definition and four of those refuse the
	// *name* where it runs. This parser has no definition-time name check and
	// [Dialect.FunctionKeywordNameIsAnyWord] records why one is not being
	// built for the sake of a diagnostic those four already print: the flag
	// says whether the word is a name, and the five that decline it keep
	// declining it at their own wording, where they declined it before.
	//
	// The quoting is the whole of it, which one control says and no character
	// class could: `'a;b'()` and `'a|b'()` are names here, and bare those
	// characters would have ended the word before the parenthesis. The
	// neighboring control is `'q'()` — an ordinary name in quotes — where
	// the panel splits *two against four* rather than one against five,
	// because ksh93 removes the quotes and defines `q`. That is what says the
	// quoted rows measure the quoting rather than a wider set of characters,
	// and it is the same #1566 gap the keyword form has.
	//
	// The one exception the keyword flag keeps, this one keeps for the same
	// reason: a name whose *unquoted* literal text holds `*`, `?` or `[` is
	// still refused, because that shell matches such a word against the
	// filesystem — `a*b() { :; }` defines nothing and is `no matches found:
	// a*b` — and a function literally called `a*b` would be a plausible wrong
	// answer where a refusal is a visible one. Quoted, they are ordinary
	// text: `'a*b'() { :; }` defines it.
	//
	// An assignment is still an assignment. `a=()` is an empty array and not
	// a definition of a function called `a=`, and the reading is lexical, so
	// the `=` has to be *bare* to make one: `'a=b'()` and `a\=b()` both
	// define `a=b` there, and `a[$i]=()` empties an element. That is the same
	// test [Dialect.FunctionNameExpands] already makes and this shares it.
	FunctionNameIsAnyWord bool

	// FunctionNameIsFilenameGenerated makes a definition's name a word the
	// shell matches against the **filesystem** when the definition runs,
	// rather than a word this parser refuses for holding a pattern
	// character.
	//
	// It is the one group [Dialect.FunctionNameIsAnyWord] and
	// [Dialect.FunctionKeywordNameIsAnyWord] both excepted, and both excepted
	// it the same way: a bare `*`, `?` or `[` in the name took the definition
	// reading away while reading, on the reasoning that a function literally
	// called `a*b` is a plausible wrong answer where a refusal is a visible
	// one. That reasoning holds against *defining* the name and not against
	// reading it — the shell being modeled reads the line and generates the
	// name from it — so what it bought was a parse failure where that shell's
	// own `-n` accepts, which costs the rest of the file rather than the
	// definition (#4437).
	//
	// Measured on zsh 5.9.2, 2026-09-25, from a script file under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C`, in a directory holding `ax` and `bx`:
	//
	//	?x() { :; }            defines `ax` **and** `bx` — one per match
	//	function ?x { :; }     the same two, so the spellings agree
	//	nomatch?zz() { :; }    `no matches found: nomatch?zz`, status 1,
	//	                       and the line after it does not run
	//	function a*b { :; }    the same sentence, `a*b` matching nothing here
	//	'a*b'() { :; }         defines `a*b`, the characters being text
	//	a\*b() { :; }          the same, and `function a\*b` too
	//
	// So the name is a *pattern*: it is generated, it may produce several
	// names or none, and none is the ordinary unmatched-pattern refusal
	// rather than a complaint about a definition. Two rows say it happens
	// when the definition **runs**: inside a branch nothing takes it says
	// nothing at all, and under `setopt nonomatch` the word is taken
	// literally and `nomatch?zz` is defined.
	//
	// The quoting decides it and is read per span, as it is for the flags
	// above: a `*` behind a quote or a backslash is ordinary text and names
	// the function it spells.
	//
	// Nothing new is needed to carry it. A name that is not text until the
	// shell runs already rides on [FuncDecl.NameWord], already expands at the
	// definition, and already defines one function per field it produces —
	// which is what a generated name needs, down to the count.
	FunctionNameIsFilenameGenerated bool

	// FunctionKeywordNameIsAnyBareWord makes the word after the `function`
	// keyword a name whenever it was written **bare**, whatever its
	// characters — and leaves a word carrying quoting or an expansion
	// refused, by the dialect's own route and in its own words.
	//
	// The keyword form alone, and narrower than both flags around it. It is
	// not [Dialect.FunctionKeywordNameIsAnyWord], which takes a quoted word
	// too and then has to except the three characters that shell matches
	// against the filesystem; and it is not
	// [Dialect.FunctionNameIsAnyBareWord], which answers the `name()`
	// spelling by the same rule *and* makes the refused word define nothing
	// in silence. The column this is for refuses a quoted word out loud and
	// carries on.
	//
	// Measured 2026-09-16 on bash 5.3.20 and bash 3.2.57, `eval "function $n
	// { echo r; }"` a name at a time from a script file. Every one of these
	// defines, at status 0:
	//
	//	a=2   f=    [     a~b   a?b   a*b   a{b   a}b   x[y
	//
	// and `a!b`, `a#b`, `a-b`, `a.b`, `a@b`, `a]b`, `a^b`, `a%b`, `a,b`,
	// `a:b`, `a/b` and `a+b` define there as they already did here. The
	// refusals are the words that were not written bare — `x$y`, `x${y}`,
	// `x$(y)`, `$x`, `'f'`, `"f"`, `\f`, `a\*b`, `a"b"c` — each “ `…': not
	// a valid identifier “ at 1 with the script carrying on, which is what
	// the source-text reading already writes. `a;b`, `a&b`, `a|b`, `a<b`,
	// `a>b`, `a(b`, `a)b` and `a b` are 2 in both shells and are not about
	// names at all: the word ended at the operator.
	//
	// `a*b` is the row that says the filesystem is never consulted — it
	// defines, and calling `a*b` runs it — which is where the shell this is
	// for parts from the one [Dialect.FunctionKeywordNameIsAnyWord] is for.
	//
	// The `name()` spelling is not this flag's and does not need one here:
	// that route already takes the same characters, since the parentheses
	// are the announcement and no name test stands in front of them.
	FunctionKeywordNameIsAnyBareWord bool

	// FunctionNameIsAnyBareWord is the third answer to the same question, and
	// the one that turns on **how the word was written** rather than on what
	// it says: a name written bare is a name whatever its characters, and a
	// name written with any quoting or any expansion in it is read, defines
	// nothing, and is not complained about.
	//
	// BusyBox ash alone, and it is the seventh column rather than a variant
	// of one of the six. Measured 2026-09-13 in the digest-pinned alpine
	// image, BusyBox v1.37.0, each line its own script file:
	//
	//	'f'() { echo p; }; f          `f: not found`, 127 — and `f` is a
	//	                              name nobody could object to, so this
	//	                              is not the characters
	//	a.b() { echo hi; }; a.b       `hi` — bare punctuation defines, which
	//	                              is [Dialect.FunctionNamePunctuation]
	//	a*b() { echo d; }; a*b        `d`, and `a?b`, `a[b`, `a{b`, `a}b`
	//	                              and `a~b` the same: a bare word is not
	//	                              matched against the filesystem here,
	//	                              which is where this parts company with
	//	                              [Dialect.FunctionNameIsAnyWord]
	//	\f() { echo p; }; f           `f: not found` — one backslash over an
	//	                              ordinary letter is enough
	//	a"b"() { echo p; }; ab        `ab: not found` — so it is the word and
	//	                              not the span the quotes are on
	//	”() { echo x; }; echo $?     `0`, nothing said
	//	w=foo; _p_${w}() { … }        `_p_foo: not found`
	//
	// And the same eight answers come back for the `function` keyword's
	// spelling, which is why this is one flag where the two above are two:
	// the panels differ for those and coincide here. `function 'f' { … }`,
	// `function a\*b { … }` and `function $(echo n) { … }` all define
	// nothing, and `function a.c { … }` and `function a*b { … }` both
	// define.
	//
	// **Nothing is defined, rather than something being defined under
	// another name.** Three probes say so and no one of them would have on
	// its own: `command -v` and `type` both answer 127 for the word's text
	// *and* for its source text, so it is not hiding under `'g'`; and
	// `g() { echo old; }; 'g'() { echo new; }; g` prints `old`, so an
	// existing definition is not replaced either. The definition's own
	// status is 0 and its body never runs.
	//
	// The body is still **parsed**: `'h'() { if; }` is `syntax error:
	// unexpected ";"` at status 2, so this is a definition the grammar reads
	// whole and not a line it skips. That is what makes this the same shape
	// [Dialect.FunctionNameCheckedWhenTheDefinitionRuns] records — the word
	// reaches [FuncDecl.RefusedName] as source text and the answer is the
	// interpreter's, which for this shell is
	// interp.FuncNameDefinesNothing: no wording, nothing bound, status 0.
	//
	// An assignment is still an assignment, and lexically, exactly as the
	// two flags above have it: `a=()` is `syntax error: unexpected "("`
	// here, and `'a=b'() { echo x; }` is a definition of nothing at status
	// 0 — the `=` has to be bare to make one.
	FunctionNameIsAnyBareWord bool

	// FunctionMultipleNames lets the `function` keyword take more than one
	// name for one body: `function clipcopy clippaste { … }` defines both,
	// and `$0` inside the body is the name that was called, which is what
	// makes the construct more than two definitions written once. zsh alone
	// in the panel — measured 2026-09-10, `function f1 f2 f3 { echo "$0"; }`:
	//
	//	zsh 5.9.2   all three defined, each `$0` its own name, status 0
	//	bash 5.3    `syntax error near unexpected token `f2'`, status 2
	//	bash 3.2    the same sentence, status 2
	//	bash-as-sh  the same sentence, status 2
	//	dash        no keyword at all: `function: not found`, then the
	//	            brace group runs and the closing `}` is unexpected
	//	ksh93       parses it and defines **only the first** — `f2` and
	//	            `f3` are `not found`, and `functions f1` says the whole
	//	            header, names and all, back
	//
	// So five of the six part company with zsh and the sixth reads the same
	// text as a different program. ksh93's reading is not modeled: it defines
	// a function whose extra names went nowhere, which is a lenience rather
	// than a construct, and the ksh dialect keeps refusing the line here.
	//
	// **The keyword spelling's list is [Dialect.FunctionKeywordNameList]**,
	// and they are two fields because one of them moves on its own: the
	// dialect that has both carries an option taking the parenthesized list
	// away and leaving the keyword's. This flag is the parenthesized one.
	//
	// **Names are taken greedily**, exactly as [Dialect.ForMultipleNames]
	// takes a loop's: every word after the first is another name until the
	// body begins at `{` or at the `()` of the hybrid form, and a reserved
	// word is a name like any other. Measured: `function a while { … }`
	// defines `a` *and* `while` in zsh, so calling `while` afterwards runs
	// the body rather than opening a loop. A stop word is not taken —
	// `function a } { … }` is a parse error on the `}` — and neither is a
	// compound command: `function a b if true; then …` is a parse error at
	// `then` there, because `if` and `true` were read as two more names and
	// the `;` ended a definition with no body at all.
	//
	// That last reading is [Dialect.FunctionKeywordBodyIsOptional], which
	// says what a name list with no body after it declares. What the list
	// replaces is a worse answer rather than a better one: without it,
	// `function a b` read `b` as the body and defined `a` alone, silently,
	// at status 0.
	//
	// Each name is read by the rule the first one is read by, so
	// [Dialect.FunctionKeywordNameIsAnyWord] and
	// [Dialect.FunctionNameExpands] apply to every name in the list:
	// `function a "b c" d { … }` defines three, the middle one holding a
	// space. What the names then share is one body — measured, a redirection
	// on the definition is shared too, `function a b { echo "$0"; } > out`
	// sending both calls to the file.
	//
	// **The parenthesis spelling takes a name list too**, and this flag is
	// read there as well — `clipcopy clippaste() { … }` defines both, and so
	// does `echo hi () { … }`, which makes *any* word list followed by `()` a
	// definition rather than only a word the grammar already liked. Measured
	// 2026-09-10, all three at status 0 in zsh 5.9.2 and a syntax error at
	// the `(` in the other five. Two shapes bound it, measured with them:
	//
	//	x=1 a b () { … }    `parse error near `()“ — an assignment ends it
	//	a b ()              `parse error near `()“ — the body is not optional
	//	                    in this spelling, unlike the keyword one above
	//	a b >out () { … }   defines both, and the redirection is the body's
	//
	// So the names are read where a command's *arguments* are read, and the
	// two things that are not names — an assignment before them, and a
	// missing body after them — are refusals rather than readings (#1685).
	//
	// **The redirection is the body's**, which is where a definition's
	// written one goes everywhere else. Measured 2026-09-12 on zsh 5.9.2,
	// each in a scratch directory:
	//
	//	a b >o1 () { echo "[$0]"; }; a; b; cat o1     `[b]`, nothing on the
	//	                                              terminal — one body,
	//	                                              two names, one file
	//	>o1 a b () { echo "[$0]"; }; a; cat o1        `[a]` — a leading one
	//	                                              is taken too
	//	a b >o1 >o2 () { echo "[$0]"; }; a            both files written
	//	x=1 a b >o1 () { … }                          still `parse error
	//	                                              near `()'` — the
	//	                                              assignment still ends
	//	                                              the reading
	//
	// It reaches the parser by a second route, because the `(` then follows
	// the redirection's target rather than a name and there is no word in
	// hand to announce the reading — see
	// [Parser.parseFuncPosixNamesAtParen]. And it reaches the *formatter*,
	// which copies a declaration's header from the source: the redirection
	// lies inside that span and the body writes it again, so the header
	// leaves it out. Printed twice, a formatted file redirects twice (#1838).
	FunctionMultipleNames bool

	// FunctionKeywordNameList lets the `function` keyword's name be followed
	// by more words, each of which is another name for the same body:
	// `function a b { … }` defines both and answers each call with its own
	// `$0`.
	//
	// It is the keyword half of [Dialect.FunctionMultipleNames], and the two
	// are separate fields because the dialect that has both moves them apart.
	// Measured on `/opt/homebrew/bin/zsh` — `zsh 5.9.2
	// (aarch64-apple-darwin25.4.0)` — `-f` over a script file under `set -n`,
	// 2026-09-27, with `multifuncdef` moved on the line after an `emulate`:
	//
	//	                                 on       off
	//	a b () { echo "[$0]"; }          parses   refused
	//	echo hi () { printf x; }         parses   refused
	//	a b >out () { echo "[$0]"; }     parses   refused
	//	function a b { echo "[$0]"; }    parses   parses
	//	function a a { echo "[$0]"; }    parses   parses
	//	a () { echo "[$0]"; }            parses   parses
	//
	// The last two rows are the controls — one name is not this question in
	// either spelling — and the two `function` rows are the split: the
	// keyword's list survives the option that takes the other one away, in
	// all three emulations alike.
	//
	// A dialect that wrote both from one flag refuses `function a b { … }`
	// under `emulate sh` and `emulate ksh`, where the reference defines two
	// functions (#4817).
	FunctionKeywordNameList bool

	// FunctionKeywordReferenceList lets the `function` keyword's name be
	// followed by more words, which are **taken and discarded**: only the
	// first word is a function, and the rest name nothing.
	//
	// ksh93 alone, where they are a list of name references the body may
	// bind. Measured 2026-09-11 and 2026-09-12 from a script file on
	// ksh93u+, because the blame lands on a later line and `-c` has none:
	//
	//	function a b { print hi; } ⏎ a ⏎ b
	//	    `hi`, then `b: not found` at 127 — `a` is defined and `b` is not
	//	function a b c d { print hi; } ⏎ a      `hi`
	//	function a "b" { print hi; } ⏎ a        `hi` — the quotes come off
	//	function a 1b { print hi; }             invalid reference list
	//	function a b=c { print hi; }            invalid reference list
	//	function a $foo { print hi; }           invalid reference list
	//	function a b; { print hi; }             `;' unexpected
	//	function a b > out { print hi; }        `>' unexpected
	//
	// So the words are names and nothing else is: an expansion, an
	// assignment and a word that is not an identifier are all the same
	// refusal, and an operator is refused as the operator it is. Quoting is
	// removed before the test, which is what parts this from
	// [Dialect.FunctionNameIsSourceText].
	//
	// It is **not** [Dialect.FunctionMultipleNames], and the pair of them is
	// what says so: zsh defines every name in its list and answers each call
	// with its own `$0`, where here `b` is not found. No dialect sets both.
	//
	// The list stops at the end of the line, and that is where the rule is
	// visible from the outside. This shell wants a brace group after the
	// keyword ([Dialect.FunctionKeywordBodyMustBeBraceGroup]), so
	// `function a echo B` ⏎ `a` blames the `a` on **line 2** — `echo` and `B`
	// were eaten as header words and the body never started — where
	// `function a echo` ⏎ `{ print hi; }` is status 0.
	//
	// Discarded by the *grammar* and not quite by the shell: `typeset -f`
	// writes the declaration back with its list, `function a b { print hi;
	// }`. That is a listing question rather than a parsing one (#1494), and
	// nothing about `b` is reachable from a script.
	FunctionKeywordReferenceList bool

	// FunctionKeywordBodyIsOptional lets a `function` keyword's name list be
	// followed by a separator, and lets it end with no body at all. Each name
	// is then defined with an **empty** body, which is what the shell reports
	// for it — measured 2026-09-10 on zsh 5.9.2, `eval "function a b"` leaves
	// `typeset +f` listing `a` and `b`, `functions a` printing `a () { }`,
	// and a call to either printing nothing at status 0.
	//
	// It is not an autoload stub, which #1686 recorded it as and which the
	// same run disproves: `fpath=(dir); autoload af1; functions af1` prints
	// `# undefined` and `builtin autoload -X`, and a call to it reads the
	// file, where `fpath=(dir); eval "function af1"; af1` prints nothing.
	// The transcript that suggested otherwise was `function af1; af1`, and
	// the `af1` after the `;` is the *body* rather than a call — which is
	// the other half of this flag.
	//
	// **The separator is [Dialect.FunctionKeywordSeparatorBeforeBody]**, and
	// it was half of this flag until #4817. A `;` between the names and the
	// body is taken here — `function a; echo B` defines `a` with body
	// `echo B`, so `echo B` never runs where it stands — and without reading
	// it, a bodyless declaration would swallow the separator and run the next
	// command instead of binding it. Newlines already stand there in every
	// dialect, and the `;` joins them. The body is absent exactly when no
	// command follows: `function a b` at the end of the input, before a `}`,
	// a `fi` or a `done`, before `&&` and before a `|`.
	//
	// The two parted because the dialect that has them carries an option
	// moving one and not the other: with `multifuncdef`'s neighbor
	// `shortloops` off, `function a` is refused and `function a; { echo B; }`
	// still parses, so the separator outlives the optional body. The
	// measurement is under [Dialect.FunctionKeywordSeparatorBeforeBody].
	//
	// How far a body that is not a brace group reaches is a flag of its own:
	// [Dialect.FunctionKeywordBodyIsAnAndOrList], below.
	FunctionKeywordBodyIsOptional bool

	// FunctionKeywordSeparatorBeforeBody lets a `;` stand between a
	// `function` keyword's name list and its body.
	//
	// It was the separator half of [Dialect.FunctionKeywordBodyIsOptional]
	// and is its own field because the dialect that has both moves them
	// apart. Measured on `/opt/homebrew/bin/zsh` — `zsh 5.9.2
	// (aarch64-apple-darwin25.4.0)` — `-f` over a script file under `set -n`,
	// 2026-09-27, with `shortloops` moved on the line after an `emulate`:
	//
	//	                                    on       off
	//	function a; { echo B; }             parses   parses
	//	function a                          parses   refused
	//	function a; echo B                  parses   refused
	//	function a; ( echo B )              parses   refused
	//	function a; if true; then :; fi     parses   refused
	//	function a { echo B; }              parses   parses
	//
	// The first row is this flag on its own: the separator is still read with
	// the option off, where the bodyless declaration under it is not. The
	// rows after it are the body's question and land on
	// [Dialect.FunctionKeywordBodyMustBeBraceGroup] — every body but a brace
	// group goes, compound or not — which is why "the body must be
	// compound" is the wrong sentence for this shell's option.
	//
	// The other dialect with the keyword refuses the separator outright:
	// ksh93 answers “ `;' unexpected “ for `function a b; { print hi; }`,
	// which is why this is not simply always on.
	FunctionKeywordSeparatorBeforeBody bool

	// FunctionKeywordBodyIsAnAndOrList makes a `function` keyword's body
	// reach to the end of the and-or list where the body is not a brace
	// group. The brace group is the one shape that ends the declaration at
	// its `}`; everything else takes the `&&` and `||` after it.
	//
	// zsh alone, and measured 2026-09-12 on zsh 5.9.2 by the **order** the
	// two commands come out in, which is the only thing that parts the two
	// readings — both print `X` and `Y` at status 0:
	//
	//	function a; echo X && echo Y  ⏎ a     X then Y — one body
	//	function a { echo X; } && echo Y ⏎ a  Y then X — `&&` is the
	//	                                      declaration's continuation
	//
	// A pipeline is inside the body too — `function a; echo X | cat && echo
	// Y` prints X then Y — and so is every other compound, which is what
	// says the brace group is special rather than "compound" being: an `if`
	// takes the `&&` after it, `function a; if true; then echo X; fi &&
	// echo Y` printing X then Y from the call.
	//
	// The hybrid form goes with the keyword and not with the parentheses:
	// `function a() echo X && echo Y` prints X then Y, where the bare
	// `a() echo X && echo Y` prints Y then X. So this is asked where the
	// keyword was written, and [Parser.parseFuncParensAndBody] never asks it.
	//
	// A `&` ends the list as it ends any and-or, and it then backgrounds the
	// whole declaration: `function a; echo X &` leaves `a` undefined in the
	// shell that ran it, the definition having happened in the subshell.
	//
	// [syntax.FuncDecl.Body] is a Command and an and-or list is an Expr, so
	// a body of more than one pipeline is wrapped in a [Group]. That is the
	// same program written back — `function a { echo X && echo Y; }` reads to
	// the same tree — which is what [SameProgram] asks of a printer. A body
	// of exactly one pipeline of one command is left bare, because it already
	// was one before this flag and wrapping it would change what every
	// existing definition prints back as.
	FunctionKeywordBodyIsAnAndOrList bool

	// TimeKeyword makes `time` a reserved word at the start of a pipeline,
	// timing the whole pipeline — `time true | wc -l` measures both elements
	// — with the report going to the shell's own standard error. Absent from
	// dash, where `time` is an ordinary name resolved from PATH.
	//
	// Only at the front: `echo hi | time wc -c` keeps `time` an ordinary
	// word, which is what bash and dash do there. It sits on either side of
	// `!`, and a bare `time` with no pipeline parses too.
	TimeKeyword bool

	// TimePosixFlag lets that keyword read `-p`, which switches the report
	// to the POSIX line format. bash and ksh93 read it; zsh does not — there
	// `-p` is the first word of the timed pipeline, a command that is not
	// found — so it is not core, and where it is off the word is left to the
	// pipeline exactly as zsh leaves it.
	TimePosixFlag bool
	// TimeIgnoresADoubleDash lets the `time` keyword read a `--` after its
	// optional `-p`, consume it, and report in the POSIX format.
	//
	// Measured 2026-09-23 with `TIMEFORMAT` set to something the POSIX layout
	// cannot produce, so the two reports are told apart rather than assumed:
	//
	//	time -- echo a       bash `a` and the POSIX report; zsh `--: command not found`
	//	time -p -- echo a    bash consumes both
	//	time -p -- -- echo a bash consumes one `--`; the second is the command
	//	time -- -p echo a    bash consumes the `--`; `-p` is the command
	//
	// So at most one of each, in that order, and **either one selects the
	// POSIX report** — `time -- echo a` is the `-p` layout in bash even though
	// no `-p` was written.
	//
	// Not ksh93's, which consumes the `--` as well and answers in a *third*
	// layout of its own — neither its plain report nor its `-p` one — so the
	// flag as written here would give that dialect a report it does not make.
	// It is recorded rather than claimed. zsh reads the `--` as the command.
	TimeIgnoresADoubleDash bool
	// TimeFoldsARepeatedKeyword reads a second `time` in front of a pipeline
	// as the same request rather than as a timed timing.
	//
	// Measured 2026-09-23: `time time echo a` reports **once** in bash and
	// twice in ksh93 and zsh, and `time time time echo a` reports once there
	// too. The flags accumulate rather than the clauses nesting, which is the
	// half a second measurement pins: `time time -p echo a` is the POSIX
	// layout, so the `-p` of the *inner* keyword reaches the one report.
	TimeFoldsARepeatedKeyword bool

	// InStandsAsACommandName takes the reservation off `in` everywhere but
	// the headers that read it, so a bare `in` is a command name rather than
	// a word the grammar refuses.
	//
	// `in` is one of POSIX's reserved words, and bash, dash and ksh93 all
	// refuse it wherever a command may begin. zsh is the one that does not,
	// which is a difference a script can see: the same text is a parse error
	// there only if something else in the line is. Measured 2026-09-22, each
	// line its own `-c`, with BusyBox ash left out for want of a container on
	// this machine — it holds the POSIX reading below until somebody runs
	// these three lines in one:
	//
	//	              in                 echo | in           in() { :; }
	//	bash 5.3.20   `in' unexpected    `in' unexpected     `in' unexpected
	//	dash 0.5.12   "in" unexpected    "in" unexpected     "in" unexpected
	//	ksh93u+       `in' unexpected    `in' unexpected     `in' unexpected
	//	zsh 5.9.2     command not found  command not found   defines a function
	//
	// The word is still read by the `for`, `select` and `case` headers in
	// every dialect, which ask for it by name rather than through the
	// command position, and it is an ordinary argument everywhere in all
	// five — `echo in` prints `in`, and `in=5` assigns.
	//
	// Off is the POSIX reading, so a preset opts *in* to zsh's answer. It
	// was off for nobody before #4134: `in` stood as a command name in every
	// dialect, so `case x in esac|in) foo;; esac` — where the `esac` after
	// the header closes the `case` and leaves `| in)` behind — named the
	// `)` where bash names the `in`.
	InStandsAsACommandName bool

	// SubstitutionBodyRefusalEndsTheRead says that a `$( … )` body the
	// grammar refuses at a token settles the whole read: no closing
	// parenthesis is looked for, and what follows the substitution is never
	// reached.
	//
	// It goes beside [SubstitutionBodyRead] and is **not** the same question,
	// which this said the opposite of until 2026-09-27: "only a dialect that
	// reads the body with its line can answer it yes". zsh reads a body when
	// it runs and answers this yes — `echo b; v=$(&&); echo a` writes no `b`
	// there, and `echo b; v=$(if); echo a` writes one, so the same shell both
	// settles a read on a refused body and defers a body it could close.
	// Measured 2026-09-27 on zsh 5.9.2, `-f` over a script file under
	// `set -n` and run; see dialect/zsh for the whole table.
	//
	// So a dialect that reads the body when it runs still stops looking for
	// the closer here, and the commands written before the substitution on
	// the same line never run. The two answers were tangled because every
	// dialect that had said yes also read the body with its line.
	//
	// **A body the read merely ran out of is not this**, and the carve-outs in
	// [Lexer.bodyRefusalSettlesTheRead] are where that is drawn. The one that
	// is still wider than the panel is the refusal at the **closer itself**,
	// which is deferred here and is deferred in the reference only for
	// `if`, `while` and `until` — and only while that shell's short-loop
	// option is on. Measured 2026-09-27 over nineteen bodies: `for`, `case`,
	// `{` and `echo |` are refused with the line there and taken here.
	//
	// Measured 2026-09-22, `echo before; : $(case x in esac|in) foo;; esac)`
	// as one `-c` string. The `esac` after the header closes the `case`, so
	// the `in` behind the `|` stands where no command may begin, and the
	// parenthesis after it is the first one a count would find:
	//
	//	bash 5.3.20   `in' unexpected — and no `before'
	//	dash 0.5.12   "in" unexpected — and no `before'
	//	zsh 5.9.2     `;;' — the count's reading, from the leftovers
	//	ksh93u+       `before', and nothing else at all
	//
	// The two that answer the body's own token are the two that read the
	// body with the line; the other two are the counting reading, which is
	// theirs to keep.
	//
	// **BusyBox ash is deliberately not set here.** It reads both spellings
	// with the line, so it is the column this would most likely also be true
	// of — and there is no BusyBox on a macOS machine, so it was not
	// measured, and a value inherited for looking like dash is the shape
	// docs/spec/ash.md exists to refuse. It keeps the counting reading until
	// somebody runs the two lines above in the container.
	SubstitutionBodyRefusalEndsTheRead bool

	// SubstitutionBodyRefusesAnUnfinishedCondition settles a `$( … )` read
	// whose body stopped at the **closing parenthesis** with an `if` or
	// `elif` still short of its `then`.
	//
	// Every other refusal on the closer leaves the construct closed, which is
	// what [Lexer.bodyRefusalSettlesTheRead] carves out: the closer is the
	// token that read was looking for, so finding it there is the body
	// ending where counting says. This is the one shape that parts from it,
	// and it parts because an option decides.
	//
	// Measured on zsh 5.9.2 (aarch64-apple-darwin25.4.0) at
	// `/opt/homebrew/bin/zsh`, `-f` over a script file under `set -n`,
	// 2026-09-27, the body written as a program of its own and then inside
	// `v=$( … )`, with that shell's short-body option on and off:
	//
	//	body                    alone    $( … )   $( … ) with it off
	//	if                      refused  parses   refused
	//	if true                 refused  parses   refused
	//	if true &&              refused  parses   refused
	//	if if true              refused  parses   refused
	//	if true; then :; elif   refused  parses   refused
	//	if true; then           refused  refused  refused
	//	if true; then :; else   refused  refused  refused
	//	if true; then if        refused  refused  refused
	//	while true; do if       refused  refused  refused
	//
	// **The `then` is the line and the nesting does not move it.** The fifth
	// row and the sixth are the same construct one keyword apart, and the
	// eighth is an `if` inside a `then` — its own condition unfinished, and
	// settled all the same, because what decides is the **outermost**
	// construct. A rule about "an unfinished construct" takes all three the
	// same way and is wrong about two of them.
	//
	// **What it does not reach is the rest of the closer's population.**
	// `$(for)`, `$(case)`, `$({)`, `$(select)`, `$(repeat)` and `$(echo |)`
	// are refused with the line in that shell under every mode and are taken
	// here, which is a defect of the carve-out rather than of this flag — see
	// #4859, where the grid and what closing it costs are written down.
	SubstitutionBodyRefusesAnUnfinishedCondition bool

	// TimesIsReserved makes `times` a reserved word rather than a builtin,
	// so a word after it is a syntax error rather than an argument it
	// ignores. ksh93 alone, and the only place in the panel where *which*
	// builtin a shell has changes what parses.
	TimesIsReserved bool

	// CaseContinue enables `;;&`, which keeps testing later patterns. bash
	// only: ksh93 and zsh both reject it, so it is not core.
	CaseContinue bool

	// CaseContinuePipe enables `;|`, which is zsh's spelling of `;;&` — the
	// same terminator, measured to the same output: an arm runs and the
	// *later patterns keep being tested*, which a five-arm program with a
	// `;&` in it confirms letter for letter against bash's `;;&`.
	//
	// A second flag beside CaseContinue rather than a second value of it,
	// for the reason PipeBothStreams already records about `|&`: the two
	// spellings are mutually exclusive, so no single flag could be given a
	// value. zsh takes `;|` and refuses `;;&` with “parse error near `&'“;
	// bash 4-and-later takes `;;&` and refuses `;|`; dash, bash 3.2 and
	// ksh93 have neither.
	//
	// The token is not confined to a `case` arm, because zsh's is not:
	// measured, `echo a ;| echo b` is “parse error near `;|'“ there and
	// “near `|'“ in the other four, so the two bytes are one operator
	// wherever they stand. Where the flag is off the operator table falls
	// back to `;` and then `|`, which is what those four lex — so the
	// refusal lands on the `|` where theirs does, and the diagnostic follows
	// from the lexing rather than being written twice.
	CaseContinuePipe bool

	// DollarSingleQuote enables `$'...'`, where backslash escapes are
	// interpreted. Absent from dash.
	DollarSingleQuote bool

	// HeredocDelimiterValue is what a `$'…'` here-document **delimiter**
	// stands for, for a parser that has to end the document at the decoded
	// text rather than at the escapes as written.
	//
	// `cat <<$'E\tOF'` ends at a line reading `E<TAB>OF` in every reference
	// shell that has the construct, and the delimiter was compared as its raw
	// bytes here, so the document ran to the end of the file (#5062).
	//
	// **The caller's decoder, for the reason
	// [Layout.AnsiCQuotedWordIsItsValue] takes one**: the escape set is a
	// column of semantics axes — the braced-hex policy, `\C` and `\M`, the
	// code-point encoder, what an unknown escape means — and nothing under
	// this package may hold one. interp.Runner.AnsiCValue is that decoder and
	// is exported for it. The argument is the text **between the quotes**, as
	// the lexer kept it, which is what the span holds.
	//
	// Nil leaves the delimiter spelled as it was written, which is this
	// package's own answer and the right one for a formatter: it is also what
	// dash wants, where `$'…'` is not the construct at all and the delimiter
	// really is those bytes.
	//
	// **The whole table is read, not a subset.** Measured 2026-09-29 over
	// eleven escape forms, each as a delimiter and as a word, in zsh 5.9.2,
	// bash 5.3.20 and ksh93u+: every shell ends the document at exactly what
	// that same shell makes of the escapes in a word, including the forms the
	// three disagree about — `$'EO\x{46}'` is `EOF` to bash and ksh and
	// `EO\0{46}` to zsh, `$'EO\cAF'` is `EO^AF` to bash and ksh and literal
	// `EOcAF` to zsh, and each one's document ends at its own answer. So there
	// is no smaller decoder that would do.
	HeredocDelimiterValue func(string) string

	// DoubledQuoteInSingleQuotesIsALiteralQuote makes a doubled `'` inside a
	// single-quoted string stand for one literal quote, with the string
	// carrying on: `””` is one quote character and `'a”b'` is `a'b`.
	// Without it those are an empty word and the two empty runs either side
	// of `ab`, which is what every shell in the panel reads by default.
	//
	// **No preset turns this on.** It is the one grammar flag in this struct
	// that only a *run-time option* reaches — one dialect spells the option
	// `RC_QUOTES` — so the value a dialect ships is off and a script asks
	// for the reading a line at a time. See
	// [interp.Runner.SetDoubledQuoteInSingleQuotes], which replaces the
	// runner's dialect the way the option that moves [ExtendedPattern] does.
	//
	// The reading is one byte of lookahead and nothing else: inside the run,
	// a `'` whose next byte is also `'` consumes both and leaves one quote,
	// and any other `'` closes the run as usual. So the count of quotes
	// decides — measured on zsh 5.9.2 (`/opt/homebrew/bin/zsh`, `-f`) from a
	// script file, 2026-09-26, with `print -r`:
	//
	//	written      on        off
	//	”           (empty)   (empty)
	//	””         '         (empty)
	//	”””       ”        (empty)
	//	'a”b'       a'b       ab
	//	'x””y'     x”y      xy
	//	a””b       a'b       ab
	//	”'          unmatched unmatched
	//	x”'y        unmatched unmatched
	//
	// The last two rows are the control that says this is not "a quote is
	// escapable anywhere": an odd number of quotes runs off the end of the
	// input in both states, and the failure is the same one.
	//
	// **Only the plain run.** A `”` inside `$'…'`, inside double quotes and
	// inside a here-document body is two ordinary characters in both states.
	// Measured the same day: `$'a”b	c'` is `ab	c` either way, which is
	// `$'a'` closing at the first quote and `'b	c'` being an ordinary run —
	// where a reading that reached inside `$'…'` would have given `a'b` and a
	// tab. The row that says the option really was on in that run is
	// `$'p””q'`, which is `p'q` on and `pq` off.
	DoubledQuoteInSingleQuotesIsALiteralQuote bool

	// DollarDoubleQuote enables `$"..."`, the locale-translatable string.
	// With no message catalog — the only condition the panel can measure —
	// bash and ksh93 strip the `$` and read a plain double-quoted string,
	// same escapes and same expansions. It is not core because dash and zsh
	// are on the other side: there the `$` stays a literal character in
	// front of an ordinary double-quoted string — no error, an extra byte in
	// the word, which is the `&>` failure mode again and the reason this is
	// a flag rather than always on.
	DollarDoubleQuote bool

	// Herestring enables `<<<`. Absent from dash.
	Herestring bool

	// ClobberOverrideMarker generalizes the clobber-override marker. Core
	// takes `|` after `>` alone, which is `>|`; this makes the marker `|`
	// *or* `!` and lets it follow any of the write operators, so it enables
	// eleven spellings: `>!`, `>>|`, `>>!`, `&>|`, `&>!`, `&>>|`, `&>>!`,
	// and — with [Dialect.ReversedAmpersandRedirect], since a marker cannot
	// attach to an operator the dialect does not read — `>&|`, `>&!`, `>>&|`
	// and `>>&!`. The `&>` four need AmpersandRedirect for the same reason.
	//
	// Measured 2026-09-07 across the panel and extended 2026-09-29 with the
	// four `>&` spellings, and it still moves as one thing: zsh 5.9.2
	// accepts all eleven, and dash, bash 5.3, bash 5.3 as sh, bash 3.2 and
	// ksh93 accept none of them — they have `>|` and nothing else. That is
	// why it is one flag rather than one per spelling; there is no column
	// that takes some and refuses others.
	//
	// **`>|` is core and so cannot be evidence for this flag.** ash was set
	// true here on the strength of `set -C; echo x >| f` overwriting, and
	// that row reads the same whichever way this flag is set — a correct
	// instrument pointed at an input that cannot exercise it. Measured in
	// `alpine:3.20` on 2026-09-29, BusyBox 1.36.1 accepts none of the seven:
	// `>>|`, `&>|`, `&>>|` and `&>>!` are syntax errors and `>!`, `>>!` and
	// `&>!` write a file named `!out`. Fixed in #5119; ash answers false and
	// `dialect/ash/clobbermarker_test.go` grades the rows on **which file
	// was written**, because three of them are silent.
	//
	// The two fallbacks are different and both are what the shells do, which
	// is the reason to be careful here. A `|` marker falls back to a pipe
	// with nothing on its left, so `echo hi >>| f` is a *refusal* in the
	// five — a syntax error at the `|`, each in its own words. A `!` marker
	// falls back to a word, and that one is silent: `echo hi >! f` in all
	// five writes a file whose name is the single character `!`, holding
	// `hi f`, and reports 0. This is the AmpersandRedirect hazard exactly —
	// one spelling, two meanings, no diagnostic — so accepting the union
	// here would quietly pick zsh's reading for text that legitimately has
	// the other one.
	ClobberOverrideMarker bool

	// ReversedAmpersandRedirect enables `>>&`, and with
	// [Dialect.ClobberOverrideMarker] the four marked spellings `>&|`,
	// `>&!`, `>>&|` and `>>&!` — the both-streams operators written with the
	// ampersand *after* the `>` rather than before it.
	//
	// `>&` itself is not here: it is POSIX duplication and every dialect
	// lexes it, and whether a word that is not a descriptor number turns it
	// into a write to a file is a semantic question the interpreter answers
	// (GreatAmpTarget). What this flag adds is the *append* spelling, which
	// has no duplication reading at all, and the markers.
	//
	// The five are their `&>` twins exactly. Measured 2026-09-29 on zsh
	// 5.9.2, thirty rows — both streams into one file, a numeric target, a
	// `-` target, `set -C` over an existing file, `set -C` over a missing
	// one, and an append onto seeded contents — with `>>&` against `&>>`,
	// `>&|` against `&>|`, `>&!` against `&>!`, `>>&|` against `&>>|` and
	// `>>&!` against `&>>!`. Every row identical, and the probe says DIFF on
	// the same rows when the pairs are mismatched, so the agreement is
	// evidence rather than a probe that cannot tell anything apart.
	//
	// **The target is always a name here, even when it looks like a
	// descriptor.** `>>&2` and `>&|2` write a file called `2`, and `>>&-`
	// writes one called `-`, where `>&2` duplicates and `>&-` closes. That
	// is what makes the append and the markers a family of their own rather
	// than decorations on `>&`.
	//
	// zsh 5.9.2 alone, measured the same day: bash 5.3.20, bash 3.2.57,
	// dash and ksh93 all refuse `>>&`, `>&|`, `>>&|` and `>>&!` outright,
	// and BusyBox ash 1.36.1 refuses them too. The fallback where this is
	// off is the AmpersandRedirect hazard in both directions — `>&!f` is a
	// `>&` onto a file named `!f`, silently, in bash and in ash, and `>&|f`
	// is a syntax error at the `|`. Accepting the union would quietly pick
	// zsh's reading for text that has the other one.
	ReversedAmpersandRedirect bool

	// RenameOnSuccessRedirect reads `>;`, one dialect's write that lands
	// only if the command succeeded. The output goes to a temporary file in
	// the target's own directory and is renamed over the target when the
	// command ends at status 0; at any other status the target is left
	// exactly as it was, and a target that did not exist is not created.
	//
	// Measured 2026-09-14. ksh93u+ alone has it: `echo new >; f` replaces
	// `f`, and `{ printf X; false; } >; f` leaves the old contents and
	// status 1. bash 5.3.15, zsh 5.9.2 and dash all refuse the text with a
	// syntax error at the `;`, which is the fallback this flag being off
	// leaves in place — `>` then `;`, and a `>` with no target.
	//
	// The `;` is part of the operator and must be tight: `echo x > ; f` is a
	// syntax error in ksh93 too. There is no `>>;` and no `<;`; both are
	// syntax errors there, so this is one operator rather than a marker that
	// generalizes.
	//
	// It is also where `<->` comes from, which is what #918 set out to
	// explain. `echo <->; echo done` in ksh93 reports that it cannot open
	// `-` and then does *not* run `done` — because the text is `echo` with
	// `<-` and `>;`, whose target is the word `echo` and whose argument is
	// `done`. `echo <->x` puts an `x` where the `;` was, so there is no `>;`
	// at all, and `done` runs. The diagnostic naming `-` rather than `->`
	// was the clue: the `<` had already taken its operand.
	RenameOnSuccessRedirect bool

	// SeekRedirect reads `<#` and `>#`, one dialect's **file-position**
	// redirections. They move where a descriptor next reads or writes rather
	// than deciding what it is aimed at, so `exec 3<#((0))` rewinds
	// descriptor 3 and nothing is opened, closed or duplicated.
	//
	// The operand is a word. The one this grammar reads is an arithmetic
	// command — `((expr))`, whose expression may name parameters and assign
	// to them — so the parser takes a TokArithCmd where every other
	// redirection takes a target. ksh93 also reads a *pattern* there and
	// seeks to the line matching it; that half is not claimed here, and the
	// operand is refused rather than misread (#3034).
	//
	// Measured 2026-09-16 on ksh93u+ 2012-08-01, script files under
	// `env -i PATH=/usr/bin:/bin LC_ALL=C`, stdin on /dev/null:
	//
	//	printf abcdefghij > f; exec 3< f
	//	read -n4 v <&3       [abcd]
	//	exec 3<#((0)); read -n2 v <&3    [ab]
	//	exec 3<#((6)); read -n2 v <&3    [gh]
	//
	// bash 5.3.20, zsh 5.9 and dash all refuse the text, each in its own
	// words, and none has the operator at all — so with the flag off the two
	// bytes fall back to `<` or `>` followed by a `#`, which begins a
	// comment and leaves the redirection with no target. That is the syntax
	// error those three report and the one this shell reported before the
	// operator existed.
	SeekRedirect bool

	// HeredocEndsAtClosingParen lets a here-document's body end at the
	// closing parenthesis of the construct it sits inside, so that the
	// delimiter is a delimiter even with the `)` written onto its line:
	//
	//	v=$(cat <<EOF
	//	a
	//	EOF)
	//
	// Where it is set — bash and ksh93 — the parentheses are found first and
	// the body is read from what is between them, so `EOF)` is the last line
	// the body could have had and the document ends there. Where it is not —
	// dash and zsh, and so the core — a body is read from the whole input,
	// takes the `)` with it, and the construct is left unclosed. Neither of
	// those two needs new wording for that: the complaint is the one each
	// already makes for a plainly unterminated `(`, word for word, which is
	// what the control `v=$(echo hi` shows.
	//
	// It is not a question about `$( )`. Any parentheses holding a program
	// are the same shape, and real zsh refuses `cat <(cat <<EOF` … `EOF)`
	// with the same complaint pointing at the `<(`. Backquotes are not: a
	// body cannot contain the mark that closes them, so all six shells take
	// “ v=`cat <<E ... E` “ and there is nothing to ask.
	//
	// The one place it is *not* additive is where the body's delimiter also
	// appears further down the file. The document then ends there rather
	// than at end of input — but the `)` was inside the body either way, so
	// the construct is unclosed all the same and the answer does not change.
	HeredocEndsAtClosingParen bool

	// HeredocBodyMustBeInsideTheSubstitution refuses a here-document opened
	// inside a `$( )` or `${ ; }` whose body is not inside it too — which is
	// what a one-line substitution leaves, the text ending before the body
	// could begin:
	//
	//	echo one
	//	echo $(cat <<EOF)
	//	body
	//	EOF
	//	echo after
	//
	// Measured 2026-09-18 from a script file under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C` with standard input on the null device,
	// over that file:
	//
	//	bash 5.3.20     `one`, a warning that a here-document was
	//	                unterminated, `body`, `after` — the body is read from
	//	                the lines after the enclosing command
	//	zsh 5.9.2       `one`, an empty substitution, `body` and `EOF` run as
	//	                commands, `after`
	//	dash 0.5.12     the same, in its own words
	//	BusyBox ash     the same
	//	ksh93u+         refused while reading: `syntax error at line 2:
	//	                `<<EOF' here-document not contained within command
	//	                substitution`, status 3, `after` never reached
	//
	// So it is one column against four, and this flag is that column alone:
	// with it off the four-column reading stands, which is what the core and
	// the quiet presets keep. bash's reading is neither, and it is not an
	// answer this flag can hold — it needs the body to reach the *outer*
	// lexer's pending queue from inside a span that has already closed. It
	// is [HeredocBodyFromAfterTheCommand], which is a capability beside this
	// one rather than a third value of it (#3711).
	//
	// The line named is the one the operator is on rather than the one the
	// substitution opened on, measured with `echo $(echo a` / `cat <<EOF)`,
	// which is line 2. The quoted token is `<<` and the delimiter with its
	// quoting off: `<<-EOF`, `<<"EOF"` and `<<\EOF` are each `<<EOF`, a
	// descriptor in front of the operator is not written, and `<<'E O F'` is
	// `<<E O F`, blanks and all. A herestring is not a here-document and is
	// not refused.
	//
	// **It is the two substitution spellings that hold a program and not
	// every parenthesis.** Measured the same day: the backquoted spelling is
	// accepted there and answers the four-column reading, and so is `cat
	// <(cat <<EOF)` — while `${ cat <<EOF; }`, that shell's shared-state
	// form, is refused with the same sentence.
	HeredocBodyMustBeInsideTheSubstitution bool

	// HeredocBodyBraceIsReadWithTheLine refuses an unterminated `${` in an
	// unquoted here-document body when the command is read, rather than when
	// the body is expanded — so the command never runs and the refusal is the
	// script's, at the fatal 2 and located past the delimiter. dash and
	// BusyBox ash. Measured 2026-10-03 over a script file `echo before` /
	// `cat <<EOF; echo same` / `W${` / `EOF` / `echo after`: dash 0.5.12
	// writes `before` and `5: Syntax error: Missing '}'` and ends at 2, and
	// BusyBox v1.37.0 in the pinned image `before` and `line 5: syntax error:
	// missing '}'` at 2, with neither `W`, `same` nor `after`. bash, ksh93
	// and zsh run the line and report the body when they expand it.
	//
	// The line named is where the input ends, not the body's: the `${` reads
	// on past the delimiter looking for its `}`. Over files of three, four,
	// five and seven lines both name the last line a newline closed, and the
	// same text through `-c` with no newline at its end names one fewer.
	HeredocBodyBraceIsReadWithTheLine bool
	// HeredocDelimiterSubstitutions is what a delimiter may be written as.
	// See [HeredocDelimiterSubstitution], which carries the panel (#4691).
	HeredocDelimiterSubstitutions HeredocDelimiterSubstitution
	// HeredocMax bounds the here-documents one command may open, and zero means
	// no bound.
	//
	// One shell in the panel has a bound. Measured 2026-09-23 on bash 5.3.20 and
	// identically on the 5.3.15 in the image its own suite is graded in: a `cat`
	// with **sixteen** `<<EOF` redirections runs, a seventeenth is `maximum
	// here-document count exceeded` at the command's line, the input ends and
	// the shell exits 2. dash, ksh93 and zsh take seventeen — and a hundred —
	// without a word, which is why this is a dialect's number and not the
	// substrate's.
	//
	// **Per command and not per input**, which is the measurement that says what
	// the counter is: two commands of sixteen each in one file both run. So the
	// bound is on the redirections still waiting for a body, which is the list
	// the lexer drains at every newline.
	//
	// It is the bound CVE-2014-7186 was about — an unbounded redirection stack —
	// and bash's own `exportfunc1.sub` is the regression test for it, which is
	// where this was found (#4143).
	HeredocMax int

	// HeredocBodyFromAfterTheCommand reads a here-document a substitution's
	// text opened and could not feed **from the lines after the enclosing
	// command**, and hands the body back to the substitution.
	//
	// The reading [HeredocBodyMustBeInsideTheSubstitution] names as bash's
	// and declines to hold, because it is not an answer a flag can carry on
	// its own: the body has to reach the *outer* lexer's pending queue from
	// inside a span that has already closed, and then travel back to the
	// parse that runs the body. See [Span.CarriedHeredocs] for the seam.
	//
	// Measured 2026-09-20 from a script file under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C`, standard input on the null device, over
	//
	//	echo one
	//	echo $(cat <<EOF)
	//	body
	//	EOF
	//	echo after
	//
	// and the `cat <(cat <<EOF)` and “ echo `cat <<EOF` “ spellings of the
	// same file:
	//
	//	column            $( )              <( )              backquoted
	//	bash 5.3.20       body, warned      body, warned      quiet reading
	//	bash 3.2.57       quiet reading     quiet reading     quiet reading
	//	ksh93u+           refused           body, silent      quiet reading
	//	zsh 5.9.2         quiet reading     quiet reading     quiet reading
	//	dash 0.5.12       quiet reading     no such spelling  quiet reading
	//	BusyBox ash       quiet reading     quiet reading     quiet reading
	//
	// where the *quiet reading* is an empty substitution with the body's own
	// lines then run as commands, which is what the zero value keeps.
	//
	// **Three things that a bool would have got wrong**, each of them a row
	// above rather than a symmetry:
	//
	//   - It is a **version line inside one lineage** and not a name's. bash
	//     3.2 is on the quiet side of every spelling, so a value keyed on
	//     "bash does this" would put it in that column too — the shape
	//     [SubstitutionBodyRead] already has.
	//   - It is **per spelling**, and the spellings split the columns
	//     differently: ksh93 reads `<( )` this way while refusing `$( )`
	//     outright, which is why this has a value for the process
	//     substitution alone.
	//   - The **backquoted spelling is outside it in every column**. bash 5.3
	//     writes a different warning there — the ordinary
	//     [RemarkHeredocAtEOF] one — and then takes the quiet reading, so a
	//     capability reached from the backquote scanner as readily as from
	//     the parenthesis one would be wrong in every column that has it.
	//
	// The warning is a separate question and is [RemarkHeredocCarriedOut]:
	// bash writes one naming how many documents were carried, and ksh93
	// writes nothing at all. See #3711.
	HeredocBodyFromAfterTheCommand HeredocBodyOutside

	// HeredocLastLineIsADelimiterPrefix reads the last line of the text
	// inside `$( )` as a delimiter *prefix*, where a here-document in that
	// text reached the end of it without ever seeing the delimiter on a line
	// of its own: the delimiter is consumed and the rest of that line is
	// parsed as more of the substitution.
	//
	// The looser half of HeredocEndsAtClosingParen above, and one shell has
	// it. Measured 2026-09-12 and re-measured 2026-09-18 from a script file
	// under `env -i PATH=/usr/bin:/bin LC_ALL=C`, bash 5.3.20 against bash
	// 3.2.57, ksh93u+, dash and zsh 5.9.2, over
	//
	//	v=$(cat <<E
	//	w
	//	E <tail>
	//	echo "[$v]"
	//
	//	tail          bash 5.3                          bash 3.2 and the rest
	//	)             [w]                               [w⏎E ] or a refusal
	//	x)            `x: command not found`, [w]        the same
	//	x y)          the same with an argument          the same
	//	x) (no blank) `x: command not found`, [w]        the same
	//	E)            `E: command not found`, [w]        the same
	//	; echo hi)    syntax error at `;`, echoing       the same
	//	              “ ` ; echo hi)' “
	//
	// **The match is a bare prefix and needs no blank**, which is what the
	// fourth and fifth rows say: `Ex)` consumes the `E` and runs `x`.
	//
	// **It is a recovery at the end of the text and not a prefix match on
	// every body line**, which is the control that makes it safe. A body line
	// beginning with the delimiter, with the document then closed properly —
	//
	//	v=$(cat <<E
	//	EXTRA
	//	E
	//	)
	//
	// — answers `[EXTRA]`: `EXTRA` begins with `E` and is not taken as the
	// delimiter. Move the closer up onto the `E` line and the answer is
	// `[EXTRA]` again, with the `E ` line consumed. So a well-formed document
	// cannot be terminated early by this, which was the regression it looked
	// like it might carry into a very common construct.
	//
	// **And it is `$( )` and nothing else.** The discriminating shape puts
	// the prefix line last in a plain file — `cat <<E` / `w` / `E x` with
	// nothing after it — and bash gives cat the body `w⏎E x`, so the rule is
	// not a general here-document rule. The backquoted spelling of the same
	// substitution answers `[w⏎E ]`, body, exactly as bash 3.2 has it.
	//
	// The warning that goes with it is not a cost: RemarkHeredocAtEOF fires
	// in exactly these cases already and both shells write
	// `warning: here-document at line 1 delimited by end-of-file (wanted
	// `E')` character for character. The well-formed control is what says the
	// remark must *not* fire for a document closed on its own line.
	//
	// Not ksh93, which refuses this shape with “ syntax error at line 1:
	// `(' unmatched “ — it runs the document to end of input and then cannot
	// find the closer, which is a question about how the substitution's
	// extent is found rather than about this rule (#1021).
	HeredocLastLineIsADelimiterPrefix bool

	// HeredocDelimiterAcrossAContinuation says whether a body line built out
	// of two or more physical lines may itself be the delimiter, and how far.
	// See [ContinuedHeredocDelimiter], which carries the measurement.
	//
	// The *joining* is not this flag's to decide and happens in every
	// dialect: an unquoted here-document's body line that ends in an odd
	// number of backslashes continues onto the line under it, and the
	// delimiter is looked for on the result. Without that, a body holding a
	// continued line ends at the first line that merely looks like the
	// delimiter and everything under it is run as commands — which, where the
	// real delimiter never arrives, reads on to the end of the input and
	// under a terminal is a hang rather than a diagnostic (#2430).
	//
	// A quoted delimiter — `<<'EOF'` or `<<\EOF` — makes the body literal
	// throughout, continuation included, and never reaches this.
	HeredocDelimiterAcrossAContinuation ContinuedHeredocDelimiter

	// HeredocPrefixLineKeepsItsContinuation keeps the backslash and the
	// newline of an unquoted here-document body line that is nothing but a
	// non-empty start of the delimiter and the backslash, rather than joining
	// it to the line under it. ksh93u+ alone.
	//
	// Measured 2026-10-03 on ksh93u+ 2012-08-01, delimiter ABC, each body
	// printed by `cat`:
	//
	//	AB\ ⏎ X        AB\ ⏎ X    kept: AB starts the delimiter
	//	A\ ⏎ BC        A\ ⏎ BC    kept, so the joined ABC is never formed
	//	ABC\ ⏎ X       ABC\ ⏎ X   kept: the whole delimiter counts
	//	→AB\ ⏎ X       AB\ ⏎ X    under <<-, after the tabs are stripped
	//	B\ ⏎ X         BX         joined: B does not start it
	//	xA\ ⏎ X        xAX        joined: nor does xA
	//	\ ⏎ X          X          joined: an empty start is no start
	//
	// It reads as the delimiter matcher failing to back out of a partial
	// match, and the shapes around it say so — `AB\` over `X`, a backslash
	// pair that ends no line anywhere else, joins there, and `A$v\` over `X`
	// joins with `$v` left unexpanded. Those two are not modeled: this is
	// the one shape whose answer is a rule rather than an accident, and it is
	// the one the corpus records (heredoc/a-body-line-opening-with-the-
	// delimiters-own-letters, heredoc/a-joined-line-that-spells-the-delimiter).
	HeredocPrefixLineKeepsItsContinuation bool

	// HeredocDelimiterKeepsAQuotedExpansion leaves a double-quoted part of a
	// here-document delimiter that holds an expansion as it was written,
	// quotes and all, rather than taking the quotes away.
	//
	// ksh93u+. Measured 2026-10-03 from script files, each delimiter written
	// again on a line of its own under the body:
	//
	//	"$d"      ends at a line reading "$d"
	//	"$d"x     ends at "$d"x
	//	a"$d"     ends at a"$d"
	//	"${d}"    ends at "${d}"
	//	"`x`"     ends at "`x`"
	//	"EOF"     ends at EOF       — no expansion, the quotes go
	//	"$" "a$"  end at $ and a$   — a dollar that starts nothing
	//	\$d '$d'  end at $d
	//
	// bash 5.3.20, zsh 5.9.2 and dash take the quotes away in every row
	// (corpus row heredoc/quoting-the-delimiter-is-what-makes-the-body-literal).
	HeredocDelimiterKeepsAQuotedExpansion bool

	// HeredocLeadingContinuationStripsTheNextLine strips the tabs of a
	// `<<-` body line that a continuation standing before any text brought
	// in, as it strips the line's own: the joined text is what loses its
	// tabs. bash 5.3.20, and dash where no tab stood before the backslash
	// (see HeredocStrippedLoneBackslashIsKept for where one did). Measured 2026-10-03 from script files, `cat <<-EOF`
	// over `→\` ⏎ `→EOF` ⏎ `X` ⏎ `→EOF`: bash ends the document at the second
	// line and runs `X`; over `→\` ⏎ `→foo` it writes `foo`, and over `→\`
	// ⏎ `→\` ⏎ `→→bar` it writes `bar`. A continuation after text joins as
	// everywhere: `→a\` ⏎ `→b` is `a→b` (corpus row heredoc/stripping-and-
	// joining-are-ordered-and-the-panel-parts-over-which-comes-first).
	HeredocLeadingContinuationStripsTheNextLine bool

	// HeredocStrippedLoneBackslashIsKept keeps the backslash and the newline
	// of a `<<-` body line that is tabs and one backslash, and takes the line
	// under it as written — not stripped and never the delimiter. dash. The
	// same measurements: `→\` ⏎ `→EOF` ⏎ `X` ⏎ `→EOF` writes `\`, `→EOF` and
	// `X` and ends at the last line; `→\` ⏎ `→\` ⏎ `→→bar` writes `\` and
	// `→→→bar`, the second line's own continuation joining as usual. A line
	// that is the backslash alone, with no tab before it, joins as the
	// leading continuation it is.
	HeredocStrippedLoneBackslashIsKept bool

	// StrippedHeredocDelimiter is what `<<-` does with a delimiter written
	// with leading tabs, which only a quoted delimiter can be. See
	// [HeredocDelimiterTabs], which carries the measurement.
	StrippedHeredocDelimiter HeredocDelimiterTabs

	// ArithCommand enables `(( expr ))` as a command. Consumed by the lexer,
	// which scans the expression as raw text: what is inside is an arithmetic
	// expression rather than a command list, so the token stream would lose
	// it. Where this is off, `(( 1+1 ))` is two nested subshells running
	// `1+1` as a command name, which is what dash does — not an error, a
	// different program.
	ArithCommand bool

	// ArithCommandScanIgnoresQuoting makes the scan that looks for an
	// arithmetic command's closing `))` blind to quoting, so a `)` written
	// inside quotes closes the expression's nesting like any other — and,
	// arriving where nothing closes it twice, gives the arithmetic reading up
	// and leaves two groupings behind.
	//
	// `((` is ambiguous and ArithCommand's own doc has the rule that settles
	// it: the reading holds where the expression's nesting is closed by two
	// *adjacent* `)`. This is the narrower question of whether the scan
	// looking for them sees a `)` inside a quoted run at all, and the panel
	// splits on it where the rule itself is unanimous. Measured 2026-09-15,
	// each probe in a script file of its own:
	//
	//	probe                 bash 5.3, 3.2, as sh  zsh 5.9.2   ksh93u+
	//	((echo "a)b"))        arithmetic error, 1   prints a)b  prints a)b
	//	((echo 'a)b'))        arithmetic error, 1   prints a)b  prints a)b
	//	((echo "(" ))         arithmetic error, 1   prints (    `"' unmatched, 3
	//	((echo a\) ))         arithmetic error, 1   arith error arith error
	//	((echo $(echo a) ))   arithmetic error, 1   arith error arith error
	//
	// dash and BusyBox ash have no `((` at all and print the text in every
	// row. The last two rows are what keeps the flag narrow: a backslash and a
	// command substitution are stepped over by every column, so this is about
	// *quoting* and not about skipping in general.
	//
	// It is not shared with the `$((` fallback, which asks the same-shaped
	// question at a different construct: bash tracks quoting there too and
	// zsh and ksh93 do not, but the two were measured separately and nothing
	// here assumes they move together. See
	// ArithSubstFallsBackToCommandSubst and `substitutions.md`.
	//
	// ksh93's third row is its own third answer and is a fact about that
	// shell's *fallback* rather than about this scan: having given the
	// reading up at the `(` inside the quotes it meets a quote it cannot
	// close, where the flag alone leaves two groupings that print `(`. It is
	// measured and left (#3069).
	ArithCommandScanIgnoresQuoting bool

	// ArithBracketsMustBalance makes the scan for the closing `))` of an
	// arithmetic command or a `$((` substitution count square brackets too,
	// and gives up the arithmetic reading when they do not balance there — so
	// the `((` is two subshells after all, and the `$((` a command
	// substitution of a subshell: `print $(( a[1 ))` is `bad pattern: a[1`
	// as well. Measured 2026-10-02 on zsh 5.9.2 under `-f`:
	// `(( a[1 ))` is `bad pattern: a[1`, the glob refusal of a command word,
	// `(( 1 ] ))` is `command not found: 1`, `(( [ ))` runs `[` and says `']'
	// expected`, and `(( a[1]] ))` beside a file named `a1]` is `command not
	// found: a1]`; a balanced `(( a[[1]] ))` is arithmetic. bash 5.3.20
	// reads every one of those as arithmetic and refuses the expression, and
	// ksh93u+ refuses `(( a[1 ))` as `syntax error: `)' unexpected` (#5378).
	ArithBracketsMustBalance bool

	// ArithUnbalancedBracketIsRefused is the third reading of the same
	// question: where the closing `))` of an arithmetic command or a `$((`
	// substitution arrives with a `[` the source opened still open, the
	// `)` is a syntax error. ksh93 alone. Measured 2026-10-03 on ksh93u+
	// 2012-08-01: `(( a[1 ))`, `echo $(( a[1 ))`, `echo "$(( a[1 ))"` and
	// `echo $(( 1 + [ ))` are each `syntax error: `)' unexpected` at status
	// 3, and so is `echo $(( a[1 )) ]))`, where a `]` does come later. Only
	// the *source's* brackets count: `k="1]"; $(( a[$k ))` is refused too,
	// the `]` a value brings being no closer the reader could have seen.
	// bash reads every one of them as arithmetic, and zsh gives the
	// arithmetic reading up (ArithBracketsMustBalance).
	ArithUnbalancedBracketIsRefused bool

	// QuotedNewlineIsUnmatched refuses a newline written inside quotes — `'…'`,
	// `"…"`, `$'…'` and a backquoted substitution — as the quote that never
	// closed, unless a backslash escapes it, and then it is the newline with
	// the backslash gone. One shell's run-time option, `cshjunkiequotes`.
	// Measured 2026-10-02 on zsh 5.9.2 from a script file: with it set,
	// `print 'one⏎two'` is `unmatched '`, `"one⏎two"` is `unmatched "`, a
	// newline in `$'…'` is `unmatched '` and in backquotes “ unmatched ` “;
	// `print 'three\⏎four'` prints the two lines, and so does `"five\⏎six"`,
	// where without the option the second joins them.
	QuotedNewlineIsUnmatched bool

	// ReplacementSlashIgnoresQuotes finds the `/` that ends a replacement's
	// pattern without regard to quotes, a backslash being the only thing that
	// protects one. Each half is then read with whatever quote it was cut
	// in the middle of, which the pattern and word readers already take as
	// that shell does: an unclosed quote in the pattern is that character,
	// and one in the replacement runs to its end. One shell's reading, under
	// every emulation. Measured 2026-10-03 on zsh 5.9.2 under `-f`:
	//
	//	x=a/b;    ${x//'/'/X}  ${x//"/"/X}  ${x//$'/'/X}   a/b, no match
	//	x="a'b";  ${x//'/'/X}                              a/Xb
	//	x='a"b';  ${x//"/"/X}                              a/Xb
	//	x=a/b;    ${x//\//X}                               aXb
	//	x=ab;     ${x//b/'/'}                              a/
	//
	// bash 5.3 and ksh93u+ answer aXb for the first row. The test zsh's own
	// suite marks as failing (users/28784) is this reading (#5151).
	ReplacementSlashIgnoresQuotes bool

	// ArithSubstScanIgnoresQuoting is the same question at the `$((`
	// fallback: whether the scan that decides between arithmetic and a
	// command substitution holding a subshell sees a `)` written inside
	// quotes.
	//
	// Measured 2026-09-17 and again 2026-09-18, each probe in a script file
	// of its own under `env -i PATH=/usr/bin:/bin LC_ALL=C`:
	//
	//	probe                   bash 5.3, 3.2, as sh  zsh 5.9.2        ksh93u+
	//	echo $(( '0)' + 1 ))    arithmetic error, 1   `0)` not found   the same
	//	echo $(( "0)" + 1 ))    arithmetic error, 1   `0)` not found   the same
	//
	// So the three bash columns track quoting in the deciding scan and the
	// other two do not — the same split ArithCommandScanIgnoresQuoting
	// records for `((`, at the other construct.
	//
	// **Two fields and not one**, for the reason #3069 gives: the two scans
	// were measured separately and a single flag would assert that they can
	// never part. They agree today; that is a measurement rather than a
	// guarantee.
	//
	// Meaningless where ArithSubstFallsBackToCommandSubst is off, which is
	// dash and BusyBox ash — neither has the fallback reading at all (#3530).
	ArithSubstScanIgnoresQuoting bool

	// PatternCharacterUndoesTheContinuationStop takes away the stop
	// [Dialect.ContinuationStopsADollarAt] puts on a `$` where an unquoted
	// `*`, `?`, `[`, `{` or `~` stands earlier in the same word.
	//
	// One column has a stop at all and this is that column's exception to it.
	// Measured 2026-09-16 and again 2026-09-18 on ksh93u+ 2012-08-01, `x=5`,
	// each probe a script file of its own; the word is `<prefix>$\⏎x`, written
	// as the operand of a `printf` whose format wraps it in angle brackets:
	//
	//	prefix   ksh93u+     prefix   ksh93u+
	//	a        `<a$x>`     [        `<[5>`
	//	!        `<!$x>`     {        `<{5>`
	//	}        `<}$x>`     *        `<*5>`
	//	]        `<]$x>`     ?        `<?5>`
	//	'*'      `<*$x>`     ~        `<~5>`
	//	"*"      `<*$x>`     a[b]     `<a[b]5>`
	//	\*       `<*$x>`
	//
	// So the left column keeps the stop and the right loses it, and the three
	// quoted spellings are what say the character has to be unquoted. The
	// list is neither the glob metacharacters nor the expansion characters:
	// `~` is in it and the closing `}` and `]` are not.
	//
	// Whether the rule is about globbing or about that shell taking a second
	// pass over a word it has marked as a pattern is not decidable from
	// outside, and the field says what was seen rather than which of those it
	// is. The other four columns have no stop for it to take away, so there
	// is no panel split here — this is one shell against our reading of it
	// (#3523).
	PatternCharacterUndoesTheContinuationStop bool

	// FunctionKeyword enables `function name { ... }`. Absent from dash,
	// present in bash, ksh93 and zsh.
	FunctionKeyword bool

	// FunctionKeywordParens enables the hybrid `function name() { ... }`.
	// bash and zsh accept it; ksh93 — where the keyword originated — rejects
	// it, so it is not core.
	//
	// It is a second flag rather than part of FunctionKeyword because the
	// two forms are accepted by different sets of shells, and a flag that
	// answers for both cannot be given a value for ksh93. The distinction
	// was documented on FunctionKeyword before anything enforced it, which
	// is how ksh93 came to accept a form it rejects.
	FunctionKeywordParens bool

	// SourceDropsNulBytes reads a program as though every NUL byte in its
	// text were not there. bash, dash and BusyBox ash do; zsh keeps the byte
	// in the word it stands in, and ksh93 refuses it as `zero byte`
	// unexpected. Measured 2026-10-03 over a script file holding `echo
	// t\0wo` and `echo "q\0r"`: `two` and `qr` in bash 5.3.20, dash 0.5.12
	// and BusyBox v1.37.0 in the pinned image, where zsh 5.9.2 writes the
	// byte back and ksh93u+ stops at line 2. The same holds for a file run
	// because the kernel would not start it: `printf 'echo ran\0more\n' >
	// b.img; chmod +x b.img; ./b.img` writes `ranmore` in BusyBox ash, the
	// one column that runs such a file at all.
	SourceDropsNulBytes bool

	// FunctionKeywordParensAreAPair reads the `function` keyword as a prefix
	// to one of two things, and nothing else: the `()` pair and then any body
	// the parenthesized form takes, or no parentheses and a compound body.
	// BusyBox ash.
	//
	// Measured 2026-10-03 in the pinned image under `-c`, `function a` and
	// then:
	//
	//	{ echo B; }, a newline then { … }, if/for/while/case …   defined
	//	[[ 1 = 2 ]], which this shell otherwise runs as a builtin defined
	//	() { … }, () echo B, a newline after () then echo B     defined
	//	echo B, a newline then echo B          unexpected word
	//	( echo B ), a newline then ( echo B )   unexpected word (expecting ")")
	//	(( 1 )), a newline then (( 1 ))         unexpected "(" (expecting ")")
	//	zz then a newline and { … }             unexpected word, at zz
	//
	// So a `(` after the name is always the first of the pair — a newline
	// in front of it changes nothing, and a subshell body is out of reach
	// without the `()` — and a bare simple command is a body only behind the
	// pair. Three readings that the other dialects answer separately, and
	// one shell that has all three; a flag per reading would be three
	// answers nobody else gives.
	FunctionKeywordParensAreAPair bool

	// FunctionKeywordRefusesAnAssignment refuses a `function` keyword whose
	// next word is spelled as an assignment, at whatever stands after that
	// word: BusyBox ash. Measured 2026-10-03 in the pinned image under `-c`:
	// `function a=b { … }` and `function a= { … }` are `unexpected "{"`, `a=b`
	// then a newline and `{` the same, `function a=b;` is `unexpected ";"`
	// and `function a=b` alone `unexpected end of file`, where `function
	// =ab`, `function a-b=c`, `function "a=b"` and `function a\=b` are names
	// like any other word and define a function.
	FunctionKeywordRefusesAnAssignment bool

	// ParamSubstitution enables `${x/pat/rep}` and the spellings that put a
	// `#` or a `%` after the `/`. Absent from dash.
	//
	// It used to say "and its anchored forms", which read the two as one
	// feature and they are not: BusyBox ash accepts every spelling and reads
	// no anchor in any of them — `${w/#a/Q}` on `x#ay%bz` is `xQy%bz` there,
	// the `#a` found where it really stands. Acceptance is what this flag
	// answers and it is unanimous among the shells that have the construct;
	// what the `#` *means* is interp.Semantics.ReplacementAnchors, which is
	// where the divergence lives (#3272).
	//
	// The same division one slash further along: a `#` or a `%` after the
	// **global** `//` is accepted by every column too and is an anchor in
	// zsh alone — `${v//#a/X}` on `abcabc` is `Xbcabc` there and `abcabc`
	// in bash, bash 3.2, bash-as-`sh` and ksh93. The grammar reads the
	// anchor in both spellings and
	// interp.Semantics.GlobalReplacementAnchors decides whether it counts
	// (#3307).
	ParamSubstitution bool

	// BadSubstitutionAtParseTime refuses a `${...}` with an unrecognized
	// operator while reading the script, rather than when the expansion is
	// reached. ksh93 alone diagnoses it at parse time; bash, dash and zsh
	// treat a bad substitution as a runtime error, so one inside a branch
	// that is never taken is never diagnosed at all. The zero value is the
	// majority: defer to runtime.
	//
	// This is not a corner case in the wild — Terraform templates carry
	// `${name ~}` interpolations bash never evaluates, and refusing them at
	// parse time refuses the whole file.
	BadSubstitutionAtParseTime bool

	// BadSubstitutionAfterAFixedNameIsDeferred keeps one shape out of
	// BadSubstitutionAtParseTime: certain characters right after a special
	// parameter, a positional parameter, or a name a subscript has closed are
	// refused when the expansion is reached, not while reading.
	//
	// Measured 2026-10-03 on ksh93u+ 2012-08-01, each body written as
	// `if false; then echo "<body>"; fi; echo reached; echo "<body>"`:
	//
	//	reached, then `"<body>": bad substitution` at 1:
	//	  ${?x} ${1x} ${0x} ${*x} ${$x} ${#@x} ${#$w} ${-x}
	//	  ${?(x)} ${1(x)} ${@(x)} ${$(f)} ${a[1]x} ${a[1]_}
	//	a syntax error while reading, naming the character, at 3:
	//	  ${?1} ${-1} ${?_} ${?.} ${? } ${?"x"} ${1$} ${1$x} ${$$}
	//	  ${?[1]} ${1[2]} ${@[1]} ${a[1]$} ${a[1](x)}
	//	  ${a b} ${ab!} ${#ab c} ${!x y} ${.sh.x y}
	//	  ${${a}}, which names `!`
	//
	// So a letter defers after either kind of fixed name, a parenthesis only
	// after a special or positional one, and an underscore only after a
	// subscript; everything else, and anything after a name that could have
	// gone on, is refused while reading. The shells that defer every bad
	// substitution never ask.
	BadSubstitutionAfterAFixedNameIsDeferred bool

	// ParamSubstring enables `${x:off:len}`. Absent from dash.
	ParamSubstring bool

	// ParamSubstringOffsetTakesALeadingColon puts a `:` written immediately
	// after the substring's own into the **offset expression**, so the range
	// has no separator there and `${x::2}` is an expression reading `:2`.
	//
	// The other reading — the zero value, and bash's — is that the offset is
	// simply empty and the range is `0:2`. Measured 2026-09-15 under `env -i
	// PATH=/usr/bin:/bin` with `v=oldoldold`:
	//
	//	                bash 5.3.15   ksh93u+
	//	${v::2}         `ol`          `:2: arithmetic syntax error`
	//	${v::}          empty         `:: arithmetic syntax error`
	//	${v::-D}        empty         `:-D: arithmetic syntax error`
	//	${v::1:2}       `o`           `:1:2: arithmetic syntax error`
	//
	// zsh reads `${v::…}` as a modifier list and dash has no substring at
	// all, so the panel is one column against one and two that are asked
	// something else.
	//
	// A grammar flag and not a wording, because it decides where the range
	// is *cut*: the same characters are one expression under it and two
	// under the other reading, and what the shell then says about them
	// follows from that. `${v::=A}` is the shape that made it worth having —
	// it is the always-assign operator in zsh, an offset of nothing and a
	// length of `=A` in bash's reading, and the single expression `:=A` here
	// (#2818).
	//
	// It cannot collide with the colon-prefixed conditionals: `${v:-D}` has
	// one colon and is read as the default-value operator before any of this.
	ParamSubstringOffsetTakesALeadingColon bool

	// ParamCaseChange enables `${x^^}` and `${x,,}`. **bash alone**: ksh93
	// reports a syntax error and zsh a bad substitution, so a construct one
	// panel shell supports is not a common denominator and this is off for
	// the core.
	ParamCaseChange bool

	// ParamTransformations enables `${x@Q}` and the rest of the letter
	// family — Q E P A a K k L U u — which transform the value rather than
	// test or edit it. **bash alone**: dash and zsh call the construct a bad
	// substitution when the expansion is reached, and so does ksh93, whose
	// refusal of every *other* unrecognized operator while reading makes the
	// `@` family its one deferred bad substitution. Off for the core.
	//
	// The letter set is fixed. `${x@}`, `${x@QQ}` and `${x@Z}` are bad
	// substitutions in the shell that has the form, so where the flag is on
	// they stay exactly what they are where it is off.
	ParamTransformations bool

	// AliasesExpandUnlessTold is whether this shell expands aliases with
	// nobody having asked it to. bash is the holdout and needs `shopt -s
	// expand_aliases`; every other shell in the panel expands out of the box,
	// and turns it off with an option of its own — zsh's `unsetopt aliases`.
	//
	// It is the **whole** gate on every text the shell reads that is not its
	// own program: `eval`'s string, a command substitution's, a sourced
	// file's, a trap body's. Measured 2026-09-12 under `-c`, with the alias
	// defined on a line of its own so that the probe is not measuring the
	// rule that an alias never expands on the line that defines it:
	//
	//	shell        eval  $( )  .  trap
	//	bash          no    no   no  no
	//	bash +shopt  yes   yes   —   —
	//	bash as sh   yes   yes  yes   —
	//	dash         yes   yes  yes  yes
	//	ksh93        yes   yes  yes  yes
	//	zsh          yes   yes  yes  yes
	//
	// A one-liner is the trap here, and it caught the earlier measurement of
	// this and the one behind ash's route set (#2338): `sh -c 'alias t=echo;
	// t X'` answers `t: not found` in *every* shell, because the alias has
	// not run when the line holding its use is parsed. Two lines, or nothing
	// is being measured.
	//
	// **Not a route set.** That is what ExpandAliasesInProgramText below is,
	// and separating them is #2109: one field holding both made bash's "off
	// until asked" indistinguishable from zsh's "not under `-c`", and the
	// front end derived the nested texts' answer from the same value — so a
	// zsh `-c` string, which really does not expand its own text, turned the
	// alias table off for every `eval` and `$( )` inside it as well.
	AliasesExpandUnlessTold bool

	// ExpandAliasesInProgramText is the set of *non-interactive* routes on
	// which the shell's **own program text** expands an alias, when
	// AliasesExpandUnlessTold has said there is anything to expand. All the
	// shells expand interactively, which is the front end's to know rather
	// than this — it is what decides there is a person at the keyboard.
	//
	// Measured 2026-09-12, the same two lines by all three routes, with the
	// option turned on where the shell has it off by default:
	//
	//	shell        -c    script file   standard input
	//	bash        yes    yes           yes
	//	bash as sh  yes    yes           yes
	//	dash        yes    yes           yes
	//	ksh93       yes    yes           yes
	//	ash         yes    yes           yes
	//	zsh         *no*   yes           yes
	//
	// The ash row is measured 2026-09-13, BusyBox v1.37.0 in the pinned
	// alpine image, and it moved: see below.
	//
	// **One cell, and it is zsh's.** The row is not really about aliases:
	// zsh reads a `-c` string *whole* before running any of it — see
	// Diagnostics.CommandStringParsedWhole — so the `alias` on line 1 has not
	// run when line 2 is parsed, and nothing on the string can expand. The
	// route set stands in for that here because this front end parses a
	// command string a line at a time and would otherwise expand where zsh
	// does not.
	//
	// ash held that value too until #2338, and it never should have: the
	// probe behind it was a one-liner, which dash — expanding on every route
	// — answers identically. Asked with two lines, BusyBox expands under
	// `-c`, so the cell is dash's and zsh's is the only one in the column.
	// That is worth saying out loud, because the reading above depends on
	// it: the route set stands in for a reading strategy, and exactly one
	// shell in the panel has that strategy.
	//
	// Whether a word *is* expanded, and into what, is not a dialect question:
	// every shell that expands agrees on the whole algorithm, so that is the
	// core's behavior and lives in alias.go.
	ExpandAliasesInProgramText ProgramRoutes

	// AliasesExpandReservedWords lets an alias whose *name* is one of the
	// words this grammar reserves stand in for it, so `alias for=echo` on
	// one line turns `for x in 1` on the next into a command rather than
	// into a loop. Where it is off the name is still stored and still
	// listed — only the substitution is declined, and the word goes on
	// meaning what the grammar says it means.
	//
	// The standard puts the name out of bounds and two shells take it
	// anyway. Measured 2026-09-13 from a script file, the alias defined on a
	// line of its own and used on the next, with the option turned on where
	// the shell has it off:
	//
	//	shell         for  if  while  case  {  !  select  function  time
	//	bash 5.3      yes yes  yes    yes  yes yes  yes     yes      yes
	//	bash 3.2      yes yes  yes    yes  yes yes  yes     yes      yes
	//	zsh 5.9       yes yes  yes    yes  yes yes  yes     yes      yes
	//	bash as sh     no  no   no     no   no  no   no      no       no
	//	dash           no  no   no     no   no  no  yes     yes      yes
	//	ksh93          no  no   no     no   —   no   no      no       no
	//	ash            no  no   no     no   no  no  yes      no      yes
	//
	// Two rows are what make this one field and not a table of names. The
	// protected set is **the words that dialect reserves** and nothing else:
	// dash has no `select`, no `function` keyword and no `time` keyword, so
	// an alias takes all three; BusyBox ash has `function` and neither of
	// the others, and protects exactly `function`. ksh93 has all three and
	// protects all three. So the question is per dialect and the set is
	// read off the grammar — see [Parser.reservedInDialect] — rather than
	// written down a second time here. (ksh93's cell for `{` is a dash
	// because it refuses the alias *name*, which is a different axis.)
	//
	// Two of the words are read one level out from a command — a pipeline's
	// leading `!` and the `time` in front of it — and that is where this
	// reached nothing at first: parsePipeline answered both before a
	// command existed to consult a table for, so `alias '!'='echo took'`
	// with `! true` behind it printed nothing here and `took true` in all
	// three shells that expand (#2638). The same field decides them, at the
	// same three-against-four split, from [Parser.expandPipelineHead]. The
	// `time` cell above is measured in that position and the `!` cell in
	// both it and the one after a value ending in a blank.
	//
	// Both shells with a POSIX mode move it, in both directions, and
	// interp.Runner.SetPosixMode is what moves it: `set -o posix` protects
	// the words in bash 5.3 and in the 3.2 macOS ships, and `set +o posix`
	// hands them back — measured with `shopt -s expand_aliases` re-issued
	// after leaving, since leaving the mode turns that switch off on its own.
	// Invoking either bash or zsh as `sh` protects them for the whole run.
	//
	// Off in the core, which refuses what the panel disagrees about.
	AliasesExpandReservedWords bool

	// AliasAtAFunctionName is what happens when the command word an alias
	// would replace is a **function name being defined** — the word in front
	// of the `()` of `name() { … }`.
	//
	// Three of the five columns expand it like any other command word and
	// two do not, so the panel splits three ways rather than two. See the
	// type for the measurement.
	AliasAtAFunctionName AliasAtAFunctionName

	// AliasedReservedWordStandsBehindAnAssignmentPrefix keeps a reserved
	// word an alias supplied reserved where the command word stands behind
	// an assignment prefix, which is the one position a *written* one loses
	// the reading in.
	//
	// The two spellings part company only there, which is what makes this a
	// question about the expansion rather than about the construct. Measured
	// 2026-09-18 on ksh93u+ 2012-08-01 from script files under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C` with stdin `/dev/null`:
	//
	//	written out          alias g="…" then `v=x g`
	//	v=x { :; }      `}'  `{'
	//	v=x while …     `do' `while'
	//	v=x if …        `then' `if'
	//	v=x case …      `)'  `case'
	//	v=x ( : )       `('  `('
	//
	// Written out, `{` behind the prefix is an ordinary word and the list
	// runs on until the `}` that closes nothing — which is why the token
	// quoted there is the far end of the group. Supplied by an alias it is
	// the reserved word, a compound command has nowhere to stand behind an
	// assignment, and the complaint is at the word itself. `(` is an
	// operator in both spellings and so agrees in both, which is the control
	// row: this is about *words* the lexer would otherwise hand on as names.
	//
	// bash never reaches the question — it expands no alias in a
	// non-interactive shell without `shopt -s expand_aliases` — and dash
	// quotes the far end for both spellings. zsh quotes the near word for
	// both, because it keeps the reading for a written-out reserved word
	// too; that is a separate answer this flag does not carry, and is
	// measured in #3560.
	//
	// The prefix is an **assignment** one. A redirection alone does not take
	// the reading away from a written reserved word in the shell this is
	// for — `>/dev/null { echo hi; }` is “ `}' unexpected “ there and
	// `>/dev/null g` with the same alias runs the group — so that row is
	// measured and left as it is, being a compound command to parse rather
	// than a token to name (#3560).
	AliasedReservedWordStandsBehindAnAssignmentPrefix bool

	// AliasBodyCountsLines counts the newlines inside a substituted alias
	// body as lines of the input, so that every later line shifts by one per
	// newline and a command written on the body's second line is reported
	// there.
	//
	// It is the one place the two substitution models are visible from
	// outside. dash, ksh93, zsh and BusyBox ash splice the body's *text*, so
	// its newlines are input lines; bash splices tokens and the whole body
	// sits on the line the alias word was written on. Measured 2026-09-05
	// with `$LINENO` after a two-line body physically on line 5 — bash 5,
	// the other four 6 — and again with a three-line body, which shifts by
	// two; and with a command that fails inside the body, reported on the
	// body's own line by the four and on the alias word's line by bash.
	//
	// The bash column is only reachable with the aliases on. Re-measured
	// 2026-09-16, a script that never asks for them expands nothing in bash
	// 5.3 or bash 3.2 and the word is `command not found`; the same file
	// under that binary called `sh` expands it and answers 5. So this is one
	// of the rows where argv[0] decides whether there is an answer at all. The shift is per
	// *expansion*: using the alias twice shifts twice, and defining it and
	// never using it shifts nothing.
	//
	// True in the core, which is the majority of the panel and of the
	// dialects that expand at all. Unreachable where ExpandAliases is empty,
	// since nothing is ever spliced.
	AliasBodyCountsLines bool

	// AliasTrailingBlankReachesPastAnOpenConstruct keeps the value's
	// trailing blank working when the blank is *inside* a construct the
	// body opened, so the word after that construct closes is offered to
	// the table in turn.
	//
	// A value ending in a blank makes the next word eligible — the rule
	// behind `alias sudo='sudo '` — and it is unanimous while the blank is
	// a blank. Where the body also opens a quote, the blank is inside the
	// quote and the panel parts:
	//
	//	alias c='CEE'
	//	alias q='echo "x '
	//	q b" c
	//
	//	bash 5.3, bash as sh, bash 3.2   x  b CEE
	//	dash, ksh93, zsh, BusyBox ash    x  b c
	//
	// Measured 2026-09-13 from a script file, since zsh expands no alias
	// under `-c`. bash offers the next *word* of the resulting line, which
	// is the one after the quote closes; the other four offer the text
	// immediately after the value, which is inside the quote and is no word
	// at all, so nothing is offered.
	//
	// The half that does not split is measured with it: `q b"` alone, with
	// `b` an alias, is `x  b` in all seven — the word the quote swallows is
	// never a candidate anywhere. So this is about the word *past* the
	// construct and not about the one inside it.
	//
	// Off in the core, which refuses what the panel disagrees about, and it
	// is reachable only through [Parser.carryOpenWord] — there is no open
	// construct for a blank to be inside of otherwise (#2685).
	AliasTrailingBlankReachesPastAnOpenConstruct bool

	// AliasBodyBackslashJoinsTheNextLine lets a backslash an alias body ends
	// with reach the *newline* that follows the alias word, where it is an
	// ordinary line continuation and joins the next line to the word.
	//
	// The backslash reaching the input at all is not this axis and is not
	// split: with anything else after the alias word the panel is six to one
	// that the backslash escapes the character it meets. Measured 2026-09-15
	// from a script file, because zsh expands no alias under `-c`, with
	// `alias q='printf "[%s]" a'` and one conversion reused so an empty
	// field is visible as a field:
	//
	//	written  dash       bash 5.3   bash as sh  bash 3.2   ksh93      zsh      ash
	//	q b      [a b]      [a b]      [a b]       [a ][b]    [a b]      [a b]    [a b]
	//	q        [aecho]    [aecho]    [aecho]     [a ]two    [aecho]    [a ]two  [aecho]
	//	         [two]      [two]      [two]                  [two]
	//
	// The first row is #2710's defect and needs no flag — the body's last
	// token ends at the body's edge with a backslash still in it, and the
	// blank in the input is what it escapes. The second is the split this
	// answers: five columns join `echo` to the word and print two fields,
	// and zsh and bash 3.2 do not.
	//
	// Off in the core, which is the reading that joins nothing: the line
	// after the alias word is left as a line of its own, which is what the
	// text says without the continuation. bash 3.2's `[a ]` — a word with a
	// blank in it — is a difference of its own that this does not carry; it
	// is the extra blank that whole family shows and no dialect preset is
	// that build.
	AliasBodyBackslashJoinsTheNextLine bool

	// ParamExpansionFlags enables the parenthesized flag group that may open
	// an expansion: `${(U)x}`, `${(s.:.)x}`, `${(%):-%x}`. One shell in the
	// panel parses it; to the rest the whole expansion is a bad substitution,
	// which BadSubstitutionAtParseTime already splits into a parse-time
	// refusal for one dialect and a deferred runtime error for the others.
	//
	// The flag also relaxes the name: `${(%):-%x}` — found in the wild as
	// the idiom for "the path of the file being sourced" — has no parameter
	// at all, only flags and an operator, so an empty name is legal exactly
	// when a flag group was read.
	ParamExpansionFlags bool

	// ParamTildeFlag enables a `~` written between the `${` and the
	// parameter: `${~name}`, which makes the *result* of the substitution
	// eligible for tilde expansion and filename generation whatever the
	// `GLOB_SUBST` option says. zsh alone has it; to the other four a
	// leading `~` is not a name and the whole expansion is unreadable, which
	// BadSubstitutionAtParseTime already splits into a parse-time refusal
	// for ksh93 (“ `~' unexpected “) and a deferred runtime error for the
	// rest.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// NestedParamExpansion is one: it decides where the word is cut. Without
	// it there is no parameter at the front of `${~name}` at all, so the
	// expansion is unreadable rather than differently read, and there is
	// nothing for a value to switch between.
	//
	// The node carries the *count* of tildes rather than a bool, because
	// parity is the whole of the meaning and it is not a toggle of the
	// option: measured under `GLOB_SUBST` both ways, one tilde is yes and
	// two are no from either starting point, and only a spec with no tilde
	// consults the option. Counting in the parser keeps that arithmetic in
	// one place and lets the printer write the span back as written.
	//
	// The flag also relaxes the name, the way ParamExpansionFlags does:
	// `${~}` is the empty string in the shell that has the construct.
	//
	// It cannot collide with ParamCaseChange, and not only because no
	// dialect has both: bash's case-toggle `~` follows the name — `${x~}` —
	// and this one precedes it.
	ParamTildeFlag bool

	// ParamSplitFlag enables an `=` written between the `${` and the
	// parameter: `${=name}`, which splits the *result* of the substitution
	// into words on `IFS` whatever the `SH_WORD_SPLIT` option says. zsh
	// alone has it; to the other four a leading `=` is not a name and the
	// whole expansion is unreadable, which BadSubstitutionAtParseTime
	// already splits into a parse-time refusal for ksh93 (“ `=' unexpected
	// “) and a deferred runtime error for the rest.
	//
	// The sibling of ParamTildeFlag in every structural respect, and a
	// grammar flag for the same reason: without it there is no parameter at
	// the front of `${=name}` at all, so the expansion is unreadable rather
	// than differently read.
	//
	// The node carries the *count*, because parity is the meaning here too:
	// measured under `SH_WORD_SPLIT` both ways, one `=` splits and two do
	// not, from either starting point.
	//
	// It cannot collide with ParamAssign, the `${x=word}` that assigns a
	// default: that `=` follows a name and this one precedes it, so the
	// two are told apart by position before either is read. `${#=word}` is
	// the assignment on `$#` in zsh, and it stays one here, because the
	// run is read in front of the `#` and not behind it.
	//
	// The flag also relaxes the name, the way ParamExpansionFlags and
	// ParamTildeFlag do: `${=}` is the empty string in the shell that has
	// the construct.
	ParamSplitFlag bool

	// ParamRcExpandFlag enables a `^` written between the `${` and the
	// parameter: `${^name}`, which distributes the word the expansion stands
	// in over the elements it came to — `a=(1 2); x${^a}y` is the two words
	// `x1y` and `x2y`, where `x${a}y` is `x1` and `2y` — whatever the
	// `RC_EXPAND_PARAM` option says. zsh alone has it; to the other four a
	// leading `^` is not a name and the whole expansion is unreadable, which
	// BadSubstitutionAtParseTime already splits into a parse-time refusal
	// for ksh93 (“ `^' unexpected “) and a deferred runtime error for the
	// rest.
	//
	// The third occupant of ParamTildeFlag's slot and a grammar flag for the
	// same reason: without it there is no parameter at the front of
	// `${^name}` at all, so the expansion is unreadable rather than
	// differently read.
	//
	// The node carries the *count*, because parity is the meaning here too:
	// measured under `RC_EXPAND_PARAM` both ways, one `^` distributes and
	// two do not, from either starting point.
	//
	// It cannot collide with anything else this grammar spells with a `^`.
	// bash's case-conversion `^` follows the name — `${x^}` — and this one
	// precedes it, and the pattern-negation `^` of `EXTENDED_GLOB` is not
	// inside a `${`.
	ParamRcExpandFlag bool

	// ParamSetTestFlag enables a `+` written between the `${` and the
	// parameter: `${+name}`, which substitutes `1` when the parameter is set
	// and `0` when it is not, and never fails. zsh alone has it; to the
	// other four a leading `+` is not a name and the whole expansion is
	// unreadable, which BadSubstitutionAtParseTime already splits into a
	// parse-time refusal for ksh93 (“ `+' unexpected “) and a deferred
	// runtime error for the rest.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// ParamTildeFlag is one: without it there is no parameter at the front
	// of `${+name}` at all, so the expansion is unreadable rather than
	// differently read, and there is nothing for a value to switch between.
	//
	// A bool rather than a count, which is the one place this differs from
	// ParamTildeFlag and is measured rather than assumed: `${++x}` is a bad
	// substitution, so there is no second `+` for a parity to be about. It
	// asks a question rather than setting a mode, and asking it twice is not
	// a spelling the grammar has.
	//
	// Unlike ParamTildeFlag it does *not* relax the name: `${+}` is a bad
	// substitution where `${~}` is the empty string, and so are `${(U)+}`
	// and `${~+}`. The name it takes is a name or a positional — `${+@}`,
	// `${+?}`, `${+#}`, `${+$}`, `${+!}`, `${+-}` and `${+*}` are all bad
	// substitutions, while `${+0}` and `${+10}` read.
	ParamSetTestFlag bool

	// ParamElementSelection enables the three operators that choose which
	// *elements* of a value survive: `${a:#pattern}` drops the ones a
	// pattern matches, `${a:|other}` the ones another array holds, and
	// `${a:*other}` keeps only those. One shell in the panel has them.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// BareSubscript is one: the panel does not disagree about what these
	// characters *mean*, it cuts the word in different places. `${a:#two}`
	// is an exclusion in zsh, an offset whose arithmetic begins `#two` in
	// bash — which is a refusal, `operand expected` — and in ksh93 it is
	// `${a#two}`, prefix removal, the colon simply ignored. Three readings,
	// no shared syntax for a value to switch between, so the flag decides
	// which grammar is being read and nothing downstream has to ask.
	//
	// It is only about the colon. `${a:1}` is still an offset with the flag
	// on, because the disambiguation is the single character after it, and
	// `${a:-x}` is still a default for the same reason.
	//
	// The exclusion is spelled four ways, the four trims' spellings with a
	// colon in front, and they are one operator. Measured on zsh 5.9.2,
	// 2026-10-03, with `v=hello`: `${v:#hel*}`, `${v:##hel*}`, `${v:%hello}`
	// and `${v:%%hello}` are all empty, and `${v:##x*}`, `${v:%lo}` and
	// `${v:%%l*}` all leave `hello`. A third character is the pattern's:
	// `${v:###hel*}`, `${v:%%%hello}`, `${v:%#hello}` and `${v:#%hello}` all
	// leave `hello`, because their patterns begin with a `#` or `%` the value
	// does not.
	ParamElementSelection bool

	// ParamArrayZip is `${a:^b}` and `${a:^^b}`: interleave a parameter with
	// the array the operand names, one element from each in turn. `:^` stops
	// when the shorter runs out, `:^^` cycles it.
	//
	// zsh alone. Without the flag the colon opens a substring and `^b` is
	// handed to arithmetic, which refuses it — that refusal is what a real
	// startup file produced, from `FG=( ${codes:^fg} )` in Oh My Zsh's
	// spectrum library (#2112).
	ParamArrayZip bool

	// ParamWholeElementReplace enables `${a:/pattern/replacement}`, the
	// fourth operator of that family: the elements the pattern matches
	// **whole** become the replacement and the rest are left alone, and an
	// empty replacement drops them the way ParamExclude does.
	//
	// A flag of its own rather than a fourth character in the set above,
	// because it is a different *shape* — two operands split on an unquoted
	// slash where those three take one — and a dialect may have the family
	// without it. Set together in the one shell that has either, which is
	// where the sameness ends.
	//
	// The disambiguation is the same one character, and it can be read
	// before the substring without taking anything from it: no arithmetic
	// expression begins with a division, so `${x:/p/r}` is unreadable as an
	// offset in the grammars without the flag — `bad math expression:
	// operand expected at \`/p/r'` — where `${x:3/2}` keeps its offset of
	// `3/2` under the flag. Measured on zsh 5.9.2 and bash 5.3.
	//
	// It is not ParamReplace with a colon in front. That one substitutes a
	// matching *span* inside the value; this one tests the whole of it.
	// `${x:/foo/Z}` on `foobar` is `foobar` where `${x/foo/Z}` is `Zbar`,
	// so a grammar that folded the two would replace where the shell leaves
	// the value alone.
	ParamWholeElementReplace bool

	// ParamColonBeforeTrimIsIgnored reads a colon written before a trim as
	// nothing at all: `${v:#p}` is `${v#p}`, and so are `${v:##p}`,
	// `${v:%p}` and `${v:%%p}`. One shell in the panel does this.
	//
	// The third of the three readings ParamElementSelection's note names,
	// and the one that had no answer behind it: those six characters are an
	// exclusion in zsh, an arithmetic offset beginning `#p` in bash — a
	// refusal — and the plain trim in ksh93. So this is the flag that says
	// which grammar is being read, exactly as that one is, and for the same
	// reason it is a grammar flag rather than a semantics axis: the shells
	// do not disagree about what the characters mean, they cut the word in
	// different places.
	//
	// The colon is *ignored* rather than recorded, so the node is the trim
	// it would have been without it and everything downstream — the
	// per-element mapping under `[@]`, the pattern coming out of a word —
	// works because it is the same node. ParamExpr.Colon stays false: a
	// trim has no unset-or-empty test for it to extend, and setting it
	// would put a flag on the node that nothing reads and that a later
	// reader would have to guess the meaning of.
	//
	// Only the four trims. Measured on ksh93u+ 2012-08-01: `${v:/l/L}`,
	// `${v:^^}` and a colon with a space after it stay arithmetic errors
	// there, `${v:2}` is still an offset, and `${v:#}` is `${v#}` — an
	// empty pattern that trims nothing rather than a special case.
	//
	// No shell in the panel sets this and ParamElementSelection together,
	// and the parser takes them in that order so the pair has a defined
	// reading rather than depending on which case came first in the file.
	ParamColonBeforeTrimIsIgnored bool

	// NestedQuoteResetsOperandEscapes says the escaping context of a `${ }`
	// operand stops at a `"` written inside it, so the brace that would close
	// the expansion is no longer escapable there. One shell in the panel says
	// so and five say the whole body is the context, nested quotes included.
	//
	// Measured 2026-09-10, `u` unset:
	//
	//	printf '[%s]' "${u-"A\}B"}"
	//
	//	dash                      [A}B]
	//	bash 5.3.15               [A}B]
	//	that build as `sh`        [A}B]
	//	bash 3.2.57               [A}B]
	//	ksh93u+                   [A}B]
	//	BusyBox ash               [A}B]
	//	zsh 5.9.2                 [A\}B]
	//
	// Note that bash 3.2 joins the majority *here*, which is the reverse of
	// its position on the bare-operand row `operandEscapes` records — the two
	// rows are independent and neither predicts the other.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// ParamColonBeforeTrimIsIgnored is one: it decides what text the operand
	// *is*, at the stage that reads it, and by the time a value could switch
	// on it the backslash is either in the word or gone. The lexer is also
	// the only thing that knows a `"` was written inside an operand at all.
	//
	// False is the majority and the core's answer, which is a change of
	// answer rather than a new construct: this engine gave zsh's reading in
	// every dialect, arrived at without anyone choosing it — a `"` inside an
	// operand calls the plain double-quote scanner, and that scanner's escape
	// set has never had the brace in it (#2001).
	//
	// The single-quoted spelling is not this question and is unanimous apart
	// from bash 3.2: `"${u-A'\}'B}"` is `[A'}'B]` in six of the seven
	// columns, single quotes opening no run at all inside a quoted operand.
	NestedQuoteResetsOperandEscapes bool

	// NestedQuoteInAQuotedOperandEscapesAnything says that inside a `"` run
	// written in a `${ }` operand that *itself* stands in double quotes, a
	// backslash escapes whatever follows it, the way one outside quotes does
	// — rather than only the four characters double quotes make special.
	//
	// A different row from NestedQuoteResetsOperandEscapes above, which asks
	// only whether the closing brace stays escapable in that run. This one
	// asks what the rest of the alphabet does there, and the panel splits
	// along a different line.
	//
	// Measured 2026-09-22, `u` unset:
	//
	//	printf '[%s]' "${u-"A\pB"}"     and     "${u-"A\'B"}"
	//
	//	bash 5.3.20               [ApB]    [A'B]
	//	that build as `sh`        [ApB]    [A'B]
	//	bash 3.2.57               [ApB]    [A'B]
	//	ksh93u+ 93u+ 2012         [ApB]    [A'B]
	//	dash 0.5.12               [A\pB]   [A\'B]
	//	zsh 5.9.2                 [A\pB]   [A\'B]
	//	BusyBox ash 1.37.0        [A\pB]   [A\'B]
	//
	// The enclosing quotes are what decides it: written without them the
	// operand is an ordinary word, and there the same nested run keeps the
	// double-quote set in every column — `${u-"A\pB"}` unquoted is `A\pB`
	// for all seven. So this is a property of the *quoted* operand's reading
	// and not of the nested quote alone.
	//
	// A grammar flag for the reason the row above is one: it decides which
	// bytes are in the word at the stage that reads it, and by the time a
	// value could switch on it the backslash is either kept or gone.
	//
	// False is the core's answer and the wider half of the panel. bash and
	// ksh set it, which is what bash's own `rhs-exp` questions measure.
	NestedQuoteInAQuotedOperandEscapesAnything bool

	// ParamLengthTakesAnOperator lets `${#name}` carry an operator as well:
	// `${#v#a}` is the length of what the trim leaves. One shell in the
	// panel accepts it; the other four call the whole expansion a bad
	// substitution.
	//
	// A grammar flag, because the disagreement is over whether the text is a
	// construct at all rather than over what it means — `${#v#a}` is not a
	// number bash computes differently, it is a refusal there. Modeling it
	// as a semantics answer would mean building a node four grammars cannot
	// read and then declining to evaluate it, which puts the refusal a stage
	// later than the shells put it.
	//
	// Only with an operator. `${#v}`, `${#a[@]}`, `${#@}` and `${#*}` are
	// unanimous and are not this flag's business.
	//
	// The refusal is *deferred* in every dialect, including the one that
	// otherwise refuses an unknown operator while reading. Measured
	// 2026-09-06: `if false; then echo ${#v#a}; fi` runs clean in bash 5.3,
	// bash 3.2, bash as `sh`, dash and ksh93 alike, so none of them decides
	// this before the expansion is reached — which is why the node is marked
	// rather than failed here. The issue this came from expected dash to
	// refuse while reading; it does not.
	ParamLengthTakesAnOperator bool

	// ParamAssignAlways enables `${name::=word}`, the assignment that runs
	// every time: the word is stored and substituted whatever the parameter
	// held, where `${name:=word}` stores it only when the parameter is unset
	// or empty and `${name=word}` only when it is unset.
	//
	// One shell in the panel has it. The other four read the same characters
	// as `${name:off:len}` with an empty offset and a length of `=word`, and
	// fail in arithmetic there — `operand expected at \`=word'` — or, in
	// dash, which has no substring at all, call it a bad substitution.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// ParamLengthTakesAnOperator is one: the disagreement is over whether
	// the text is a third assignment operator or a substring, which is a
	// question about what was written and not about what it means. A shell
	// without the flag must keep reading it as a substring, because that is
	// what its own arithmetic error is evidence of.
	//
	// The flag is what tells the two readings apart *before* either is
	// evaluated, and reading it the substring way is not a quiet
	// mis-answer — it is the arithmetic error that stood between a real
	// plugin manager and its own colored output for eighteen lines of one
	// startup (#1369).
	ParamAssignAlways bool

	// NestedParamExpansion enables an expansion to stand where a parameter
	// name would: `${${v}}` applies one expansion to the result of another,
	// and `${${v}#a}` applies the outer operator to what the inner came to.
	// One shell in the panel has it; bash and ksh93 refuse the same
	// characters, in their own words and at their own moment.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// BareSubscript is one: it decides where the word is cut. Without it
	// there is no parameter name at the front of `${${v}#a}` at all, so the
	// expansion is unreadable rather than differently read — there is
	// nothing for a value to switch between.
	//
	// The inner expansion is the *whole* of the name position. Measured:
	// `${x${v}}` and `${${v}x}` are both a bad substitution in the shell that
	// has the construct, so text either side of it is not a shape at all and
	// an implementation that appended it would be inventing one.
	NestedParamExpansion bool

	// NamelessParamExpansion enables an expansion with no parameter name at
	// all: `${}` is the empty string, `${:-abc}` is `abc` because the name
	// that is not there is never set, and `${%x}` trims a suffix off the
	// nothing in front of it. One shell in the panel has it; the other five
	// call every one of those texts a bad substitution.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// NestedParamExpansion is one: without it there is no parameter at the
	// front of `${:-abc}`, so the expansion is unreadable rather than
	// differently read, and there is nothing for a value to switch between.
	// Measured 2026-09-08 on the six-column panel:
	//
	//	${:-abc}   abc   bash, bash-as-sh, bash 3.2 and dash: bad
	//	                 substitution; ksh93: `:' unexpected while reading
	//	${}        “    the same five refusals
	//	${%x}      “    and again
	//
	// What the flag does *not* relax is a character that is a parameter or a
	// prefix in its own right, because those are taken before the name scan
	// ever runs and the nameless reading never competes: `${-x}` is a bad
	// substitution in all six — `-` is the name `$-` and `x` is a stray word
	// after it — `${?x}` is the same with `$?`, and `${#}` is `$#` in all
	// six rather than a length over nothing. Widening the guard leaves every
	// one of those exactly where it was, which is the measured answer.
	//
	// The flag also decides `${#:-w}`, which is the one place the two
	// readings of `#` are separated by nothing else: with the nameless form
	// the `#` is a length over `${:-w}` and zsh answers `1`, and without it
	// the `#` is the parameter `$#` and the other five answer `2` — see
	// hashIsTheParameter.
	//
	// `~/.zi/bin/zi.zsh` writes `${:-…}` inside the nested expansion that
	// builds the argv of every non-zsh plugin, which is why the empty name
	// is a daily-driver blocker rather than a corner (#1529).
	NamelessParamExpansion bool

	// ParamLengthOverASpecialNameIsFinal keeps the length reading of `${#-…}`
	// and `${#?…}` when text is left over behind the name, instead of
	// re-reading the `#` as the parameter `$#`.
	//
	// zsh alone. All six shells read a *bare* `${#-}` and `${#?}` as a length
	// over the special name — which is measured rather than assumed, and it
	// takes a probe where the two readings differ, since `$-` happens to be
	// two characters in bash and ksh93 and `$#` was two in the run that filed
	// this. With `set -- p q r`, so `$#` is 3:
	//
	//	probe        bash 5.3   dash   ksh93   zsh 5.9.2
	//	${#-}        2          0      2       4          the length of `$-`
	//	${#?}        1          1      1       1          the length of `$?`
	//	${#-w}       3          3      3       bad substitution
	//	${#?w}       3          3      3       bad substitution
	//	${#-:-x}     3          3      3       4
	//	${#?:-x}     3          3      3       1
	//
	// So the divergence is not which reading is *reached* — every shell reads
	// the `-` as a name — but what happens when that reading cannot use the
	// whole expansion. Five of them fall back and take the `#` as the
	// parameter, so `-w` and `?:-x` become an operator on `$#` and answer 3;
	// zsh keeps the name and either applies a real operator to it (`${#-:-x}`
	// is the length of `${-:-x}`, which is `$-`, so 4) or refuses the stray
	// word.
	//
	// **It is `-` and `?` and no other special name**, which is what makes it
	// a rule about operators rather than about specials: `${#$w}` and
	// `${#!w}` are bad substitutions in all six, because `$` and `!` are not
	// operators and there is no second reading to fall back to. That is the
	// same test the `%`, `/` and `#` rows in hashIsTheParameter already make
	// — an operator with an operand takes the `#` — and these two were the
	// entries missing from it because they can begin a name as well (#1242).
	ParamLengthOverASpecialNameIsFinal bool

	// ParamLengthBareOperatorIsTheParameter keeps the `#` at the front of an
	// expansion as the parameter `$#` when an operator stands behind it with
	// **no operand at all**, so `${#+}` and `${#=}` are `$#` with an
	// operator that has nothing to substitute rather than a length over a
	// character that names nothing.
	//
	// zsh alone, and it is the bare form that splits the panel rather than
	// the operator: with an operand every column agrees, which is what makes
	// this a rule about the empty word. Measured 2026-09-22 with
	// `set -- p q`, so `$#` is 2:
	//
	//	probe      bash 5.3   bash-as-sh   bash 3.2   dash   ksh93   zsh 5.9.2
	//	${#+}      bad subst  bad subst    bad subst  bad    bad     (empty)
	//	${#=}      bad subst  bad subst    bad subst  bad    bad     2
	//	${#+w}     w          w            w          w      w       w
	//	${#=w}     2          2            2          2      2       2
	//
	// So five of the six read a bare `+` or `=` as a *name* behind a length
	// prefix — and neither character can begin a name, so the expansion has
	// none and the whole of it is refused. zsh keeps the parameter reading
	// it takes for the operand rows and substitutes the empty word.
	//
	// The same shape [hashIsTheParameter] already requires of `%`, `/`, `#`,
	// `-` and `?`, which have wanted an operand since they were measured;
	// `=` and `+` were the two rows of that table written without one, and
	// `${#+}` inside a `$THIS_SH -c` is where the difference shows — bash
	// abandons the command the expansion was a word of, so what follows it
	// on that line never runs (#4166).
	//
	// BusyBox ash is not in the measurement above: it needs a container and
	// none was running. It takes the default, which is the reading dash —
	// the shell it is closest to — was measured to hold.
	ParamLengthBareOperatorIsTheParameter bool

	// ParamLengthRefusesTheBangName makes a length over the special name `!`
	// unreadable, so `${#!}` is a bad substitution rather than the length of
	// `$!`.
	//
	// zsh alone, and it is a rule about that one name behind a `${#` rather
	// than about `$!` or about lengths. Measured 2026-09-12 from a script
	// file under `env -i` with a scratch HOME and ZDOTDIR, `set -- p q r`,
	// across the seven-column panel:
	//
	//	probe     bash 5.3   bash 3.2   bash-as-sh   dash   ksh93   ash   zsh 5.9.2
	//	${!}      (empty)    (empty)    (empty)      ""     ""      ""    0
	//	${#!}     0          0          0            0      0       0     bad substitution
	//	${#$}     5          5          5            5      5       5     5
	//	${#?}     1          1          1            1      1       1     1
	//
	// The first row is what makes the second a fact about the shape rather
	// than about the value: `$!` reads perfectly well in zsh — with no
	// background job it is `0` there where the other six leave it empty — so
	// the refusal is not "there is nothing to measure". The third and fourth
	// rows are the boundary: `$` and `?` behind the same `${#` are lengths in
	// all seven, so it is not a rule about special names either.
	//
	// Deferred rather than fatal at parse time, which is measured too: with
	// `${#!}` inside an `if false` branch, zsh runs the script to the end and
	// exits 0, and `zsh -n` accepts the file.
	//
	// Distinct from ParamIndirection, which is about a `!` at the *front* of
	// an expansion. Here the `!` stands behind a length prefix, where no
	// dialect reads it as indirection: the length case is taken first and the
	// `!` is scanned as an ordinary name (#2415).
	ParamLengthRefusesTheBangName bool

	// ParamContinuationNeedsAName keeps a line continuation inside `${ }` —
	// so the expansion is refused — where no name has begun: directly behind
	// the `${`, or its `#` or `!` prefix, or behind a positional or special
	// parameter.
	//
	// A backslash-newline anywhere else in the expansion is removed before it
	// is read, and that is unanimous; this is the one shape the panel splits
	// on. Measured 2026-09-16 from script files under `env -i`, stdin on
	// /dev/null, fresh directory:
	//
	//	probe                   bash 5.3  bash 3.2  zsh 5.9.2  dash  ksh93u+
	//	xy=5 ${x\⏎y}            5         5         5          5     5
	//	${x\⏎}  ${x:\⏎-d}        value     value     value      value value
	//	${#x\⏎}  ${@:\⏎-d}       value     value     value      value value
	//	${\⏎x}  ${#\⏎x}          value     value     value      value refused
	//	${1\⏎}  ${12\⏎}  ${@\⏎}  value     value     value      value refused
	//	${?\⏎}  ${#\⏎}  ${$\⏎}   value     value     value      value refused
	//
	// "refused" is “syntax error at line 1: `\' unexpected“, status 3, which
	// is what the kept pair already reads as there. The last two rows are
	// the boundary: the continuation is kept until a name has begun, and a
	// special or positional parameter never begins one — `${1:-a\⏎b}` and
	// `${@:\⏎-d}` read, because an operator stands before the pair.
	//
	// ksh93 alone, and not in an unquoted here-document body, where its
	// continuations are gone before the expansion is scanned: `${\⏎x}` is
	// the value there.
	ParamContinuationNeedsAName bool

	// ContinuationStopsADollarAt names the forms a `$` does **not** reach
	// across a line continuation written directly behind it, outside quotes,
	// and ContinuationStopsADollarAtInDoubleQuotes asks the same inside `"`.
	// The empty set is the core answer: the pair goes and the `$` introduces
	// whatever stands behind it, which is what every shape in bash 5.3, bash
	// 3.2 and dash does.
	//
	// This is the construct's *delimiter* and not its inside, so it is not
	// ParamContinuationNeedsAName's question nor the arithmetic one: the pair
	// stands between the `$` and the character that says which construct this
	// is. Measured 2026-09-16 from script files, `env -i PATH=/usr/bin:/bin
	// LC_ALL=C`, stdin on /dev/null, fresh directory, with `x=5` and
	// `set -- a b`:
	//
	//	probe            bash 5.3/3.2  dash       zsh 5.9.2  ksh93u+
	//	$\⏎x             5             5          5          $x
	//	$\⏎{x}           5             5          5          5
	//	$\⏎(echo hi)     hi            hi         hi         `(' unexpected
	//	$\⏎[1+2]         3             —          3          —
	//	$\⏎'a\tb'        a<tab>b       —          a<tab>b    a<tab>b
	//	"$\⏎x"           5             5          5          $x
	//	"$\⏎{x}"         5             5          {x}        ${x}
	//	"$\⏎(echo hi)"   hi            hi         $(echo hi) $(echo hi)
	//	"$\⏎[1+2]"       3             —          $[1+2]     —
	//	"$\⏎1"           a             a          a          $1
	//
	// A `—` is a shell that has no such form at all, so nothing there is a
	// measurement of this question. BusyBox ash was not measured. `$\⏎` with
	// a blank or the end of the word behind it is a literal `$` in bash 5.3,
	// bash 3.2, dash, zsh 5.9.2 and ksh93u+ alike, which is the row that says
	// the `$` reaches a *form* rather than the pair being removed and
	// forgotten.
	//
	// So zsh stops at everything but a bare parameter inside double quotes
	// and at nothing outside them, and ksh93 stops at a bare parameter and a
	// parenthesis outside quotes and at everything inside them. Neither is a
	// subset of the other, which is why this is a named set per quoting
	// rather than a count (#3457).
	ContinuationStopsADollarAt               DollarForms
	ContinuationStopsADollarAtInDoubleQuotes DollarForms

	// DollarGoesWhenAContinuationStopsItAtABrace drops the `$` where the set
	// above stopped it at a `${`, instead of leaving it as text.
	//
	// zsh's alone, and only where it stops at one: `"$\⏎{x}"` is `{x}` there
	// and `${x}` in ksh93, which is the same stop with the two opposite
	// answers about the character that was already read. It is asked at the
	// brace and nowhere else, because zsh keeps the `$` at every other form
	// it stops at — `"$\⏎(echo hi)"` is `$(echo hi)` and `"$\⏎[1+2]"` is
	// `$[1+2]` there.
	DollarGoesWhenAContinuationStopsItAtABrace bool

	// ContinuationPartsTheArithmeticOpener reads a line continuation written
	// between the `$(` and the second `(` as *parting* them, so `$(\⏎( 1 + 2
	// ))` is a command substitution whose first command is a subshell rather
	// than an arithmetic expansion.
	//
	// ContinuationPartsTheArithmeticCloser is the same question at the other
	// end: whether a pair standing between the two `)` still closes an
	// arithmetic expansion.
	//
	// Two fields, because the panel splits differently at the two ends and a
	// single answer could not be given a value for the shell that joins one
	// and parts the other. Measured 2026-09-16 from script files, `env -i
	// PATH=/usr/bin:/bin LC_ALL=C`, stdin on /dev/null, fresh directory;
	// BusyBox ash has no arithmetic expansion of this shape and was not
	// measured:
	//
	//	probe                bash 5.3, 3.2  dash  zsh 5.9.2  ksh93u+
	//	echo "[$(\⏎( 1 + 2 ))]"  3          3     3          runs `1`
	//	echo "[$(( 1 + 2 )\⏎)]"  3          3     runs `1`   runs `1`
	//
	// "runs `1`" is the command-substitution reading: the body is the
	// subshell `( 1 + 2 )`, whose first word is a command nobody has, so the
	// diagnostic names `1` and the expansion is empty.
	//
	// A blank on either side of the pair takes the question away and is the
	// control: `$( \⏎( 1 + 2 ))` and `$(( 1 + 2 ) \⏎)` are a command
	// substitution in every column, because the parentheses are no longer
	// adjacent whatever becomes of the continuation.
	//
	// The arithmetic *command*'s closer is not this question and needs no
	// field: `(( 1 + 2 )\⏎)` is two groupings running `1` in bash 5.3, zsh
	// 5.9.2, ksh93u+ and dash alike (#3454).
	ContinuationPartsTheArithmeticOpener bool
	ContinuationPartsTheArithmeticCloser bool

	// ContinuationEndsTheArithmeticCommandOpener makes `((` followed
	// *directly* by a line continuation open nothing at all: the two
	// parentheses and the pair are consumed, no command is produced, and the
	// next token is read as though the construct had never been written.
	//
	// ksh93u+ alone, measured 2026-09-16 from script files under `env -i`,
	// stdin on /dev/null. The visible answer is a syntax error, but the error
	// is at the *leftovers* rather than at the continuation, which is what
	// says the two characters were consumed and discarded:
	//
	//	((\⏎x = 5 )); echo "[$x]"      `)' unexpected at line 2, status 3
	//	if ((\⏎1 )); then …            `)' unexpected
	//	for ((\⏎i=0; i<1; i++)); do …  `i' unexpected
	//	((\⏎)); echo st=$?             `)' unexpected
	//	echo pre⏎((\⏎echo hi⏎echo b    pre, hi and b, status 0
	//	false⏎((\⏎echo "rc=$?"         rc=1 — not even the status moved
	//
	// The last two rows are the whole of it: the construct runs nothing and
	// leaves nothing behind, and every refusal above is the text that
	// followed being read on its own. bash 5.3, bash 3.2 and zsh read all six
	// as an ordinary arithmetic command; dash has no `((` and reads two
	// groupings.
	//
	// A blank in front of the backslash takes the rule away — `(( \⏎x = 5 ))`
	// is 5 there — and so does anything at all: `((x\⏎ = 5 ))` is 5 too. It
	// is the pair standing *directly* behind the second parenthesis (#3455).
	ContinuationEndsTheArithmeticCommandOpener bool

	// BareBraceNestsInExpansion makes an unquoted `{` inside `${…}` open a
	// nesting level, so the expansion ends at the brace that *balances* it
	// rather than at the first `}`.
	//
	// A grammar flag rather than a semantics axis, for the reason
	// NestedParamExpansion is one: it decides where the word is cut. With it
	// `${u:-{a,q}.z}` is one expansion whose operand runs to `.z`; without
	// it the expansion stops at the first `}` and `.z}` is two more
	// characters of the enclosing word. Those are different words, not one
	// word two values could disagree about.
	//
	// The panel splits two against three. Measured 2026-09-10 with
	// `unset u; printf "[%s]" ${u:-{a,q}.z}`:
	//
	//	zsh 5.9.2   [a.z][q.z]   the operand ran to `.z` and the group was
	//	ksh93       [a.z][q.z]   then expanded — two fields
	//	bash 5.3    [{a,q.z}]    the operand stopped at the first `}` and
	//	bash 3.2    [{a,q.z}]    `.z}` arrived as literal text — one field
	//	dash        [{a,q.z}]
	//
	// Brace *expansion* is not what separates them: `${u:-a{b}c}` has no
	// comma in it and splits the panel exactly the same way, `a{b}c` in the
	// two that balance and `a{bc}` in the three that do not. So the flag is
	// about the scan and not about what the group would have produced —
	// which is why dash, which has no brace expansion at all, still has an
	// answer here.
	//
	// Off in the core, which is what the common denominator means: the wider
	// reading takes text the other five columns read as belonging to the
	// word, and a core that swallowed it would be reading a word nobody
	// wrote.
	//
	// Only unquoted. In double quotes a **word** operand stops at the first
	// `}` in every column, which is what #1586 settled; a *pattern* operand
	// does not, in one column, and that is the flag below rather than this
	// one. This note used to say all seven stopped there and it was too
	// wide by the six rows [BareBraceNestsInAQuotedPatternOperand] carries.
	//
	// The `${ cmd;}` command form keeps its own rule for a third reason
	// again: its body is a program, so a `{ … }` block written in one has to
	// balance the way the program's braces do.
	BareBraceNestsInExpansion bool

	// BareBraceNestsInAQuotedPatternOperand is the same nesting inside
	// **double quotes**, and only in an operand that is a pattern — the
	// operand of `#`, `##`, `%`, `%%` or `/`, which is what
	// [Lexer.braceOperandIsAPattern] answers.
	//
	// Separate from the field above rather than a widening of it, because
	// the two are not the same rule seen through a quote: unquoted, the
	// column that nests does so for *every* operand, and inside quotes only
	// for a pattern. A single flag would have to be read with the operand
	// kind attached, which is the shape [QuoteProtectsTheClosingBrace]
	// already has for a different question about the same two operands.
	//
	// Measured 2026-09-27 from `-c` under `env -i PATH=/usr/bin:/bin` on
	// `/bin/ksh` `Version AJM 93u+ 2012-08-01`, `/opt/homebrew/bin/bash`
	// 5.3.20, `/bin/bash` 3.2.57, `/opt/homebrew/bin/zsh` 5.9.2 and
	// `/bin/dash` — `go version -m` says *not a Go executable* for each:
	//
	//	probe                        ksh93u+  the other four
	//	s=x{y}z; "[${s#x{y}}]"       [z]      [}z}]
	//	s=x{y}z; "[${s##x{y}}]"      [z]      [}z}]
	//	s=a{b}c; "[${s%{b}c}]"       [a]      [a{b}cc}]
	//	s=a{b}c; "[${s%%{b}c}]"      [a]      [a{b}cc}]
	//	s=x{y}z; "[${s/{y}/-}]"      [x-z]    [x}z/-}]  — dash has no `/`
	//	u=;      "[${u:-x{y}z}]"     [x{yz}]  [x{yz}]   — a *word* operand
	//	u=;      "[${u-x{y}z}]"      [z}]     [z}]
	//	u=;      "[${u:+x{y}z}]"     [z}]     [z}]
	//
	// The last three rows are what make this the operand kind rather than
	// the shell: the same column that balances in the five above them stops
	// at the first `}` in all three, exactly as the other four do.
	//
	// **The `[}z}]` shape is the tell** and it is worth reading once: the
	// expansion stopped at the brace's `}`, the `z` behind it came out as
	// text, and the word's own `}` was then written as another character.
	//
	// Two controls say it is the brace and nothing broader. A bracket
	// expression in the same position is balanced in every column already —
	// `s=aXb; "[${s#a[X]}]"` is `[b]` for all five — so a pattern operand is
	// not simply unscanned; and `s=aXb; "[${s#{a,q}X}]"` is `[aXb]` in the
	// column that balances against `[aXbX}]` in the rest, although brace
	// *expansion* reaches inside `${…}` in no column at all. So this is the
	// scan and not what a group would have produced, which is the same thing
	// the field above says about its own panel.
	BareBraceNestsInAQuotedPatternOperand bool

	// QuoteProtectsTheClosingBrace says which of a `${ … }` operand's kinds a
	// single quote written inside **double quotes** protects the closing brace
	// in, so that the expansion ends at the brace *after* the quoted text
	// rather than at the one inside it.
	//
	// A grammar flag rather than a semantics axis for the reason
	// BareBraceNestsInExpansion is one: it decides where the word is cut.
	// `"[${v-'}'}]"` is one expansion whose operand is `'}'` where the quote
	// protects, and the expansion `${v-'}` followed by two more characters of
	// the enclosing word where it does not. Those are different words, not one
	// word two values could disagree about.
	//
	// Measured 2026-09-12 with `v=Vx}y; echo "[${v<op>'a}b'}]"`, where the
	// leaked `b'}` is the scan having stopped at the quoted brace:
	//
	//	                   `-` `:-` `=` `+` `?` `:1:`   `#` `##` `%` `%%` `/`
	//	bash 5.3.15        [Vx}y]                       [Vx}y]
	//	bash 3.2.57        [Vx}y]                       [Vx}y]
	//	bash 5.3.15 as sh  [Vx}yb'}]                    [Vx}y]
	//	ksh93u+            [Vx}yb'}]                    [Vx}y]
	//	dash               [Vx}yb'}]                    [Vx}y]
	//	BusyBox ash        [Vx}yb'}]                    [Vx}y]
	//	zsh 5.9.2          [Vx}yb'}]                    [Vx}yb'}]
	//
	// So the panel splits three ways rather than two, and the line it splits
	// on is the **kind of the operand** rather than the shell: a word operand
	// inherits the enclosing double quoting, where a single quote stands for
	// itself and cannot quote anything, and a pattern operand does not, so its
	// quotes are quotes. Only bash reads a word operand's quote as quoting,
	// and only zsh declines to read a pattern's that way.
	//
	// Three boundaries, all measured and none of them this:
	//
	//   - **Unquoted, every column protects**, in both kinds — the divergence
	//     is inside double quotes only. A here-document body answers with the
	//     double-quoted columns, which is the reading its spans already carry.
	//   - **A double quote protects in every column**, in both kinds:
	//     `v=SET; echo "[${v-"a}b"}]"` is `[SET]` in all seven. So this is
	//     about the single quote, and the `"` case of the scan is unanimous.
	//   - **The `${ cmd;}` command form keeps its own rule.** Its body is a
	//     program, so its quotes are that program's for the same reason its
	//     braces are.
	//
	// A dialect without `${x/pat/rep}` has no `/` operator to introduce a
	// pattern with, so `/` is read as a pattern only where ParamSubstitution
	// says the form exists — which is what keeps dash, the one panel member
	// without it, refusing `"${v/'$('/z}"` where ksh93 and ash accept it.
	//
	// The `sh` row is modeled, and by the *mode* rather than by the name:
	// QuoteProtectsTheClosingBraceInPosixMode below is where a dialect says
	// what POSIX mode makes of this axis, and driver enters that mode for
	// every dialect invoked as `sh`.
	//
	// It was recorded and not modeled until #2604, on the grounds that "POSIX
	// mode is a Semantics question here, and `set -o posix` cannot reach a
	// grammar flag through it (#2399)". The second half of that is false and
	// was already false when it was written: SetPosixMode replaces the
	// Runner's Dialect to move AliasesExpandReservedWords, the front end
	// notices the replacement and hands it to Parser.SetDialect, and the
	// parser re-reads what it has not tokenized yet. A grammar flag is
	// reachable at run time, and this is the second one through that door.
	//
	// **And it is decided when the word expands rather than when it is read**,
	// which says a front end handing the parser a different reading at startup
	// would agree with bash only on the inputs it was built for. Measured
	// 2026-09-13 on one function body, parsed once, before the mode was on:
	//
	//	$ bash -c "v=Vx}y; f(){ printf '[%s]' \"\${v-'a}b'}\"; echo; }; f;
	//	          set -o posix; f"
	//	[Vx}y]
	//	[Vx}yb'}]
	//
	// The same already-parsed word answers both ways as the mode moves under
	// it. Under the name `sh` the move is from BraceQuoteProtectsEveryOperand
	// to BraceQuoteProtectsAPatternOnly — the core's own reading — and only
	// for the *word* operand: `"${v#'a}'}"` is `[Vx}y]` under either name. zsh
	// under the same name does not move at all, which is the shape
	// Semantics.BadOptionToSpecialBuiltinFatalInPosixMode has, so whatever
	// mechanism reaches this the answer has to come from the dialect, which
	// is what the field below is.
	//
	// So the mode reaches this axis twice, and the two halves are different
	// things. The **parse-time** half is the dialect swap itself: text the
	// parser has not read yet is read under the new value, exactly as an
	// alias-expanded reserved word is. The **expansion-time** half is
	// ParamExpr.RawTail, which keeps the source from a `${` to the end of the
	// word it stands in so that an already-cut word can be divided again when
	// it expands. Measured 2026-09-14, and it is the measurement that bounds
	// the whole design: the re-read never moves a *word* or a *statement*
	// boundary —
	//
	//	$ bash -c 'v=V; f(){ printf "[%s]" "${v-'"'"'a}"; echo MIDDLE;
	//	          :"'"'"'}"; echo END; }; f; set -o posix; f'
	//	[V]END
	//	[V; echo MIDDLE; :'}]END
	//
	// where `MIDDLE` never prints in either mode and the `printf` takes one
	// argument in both. The tree's shape stays a fact about the parse; what
	// the run re-decides is one already-cut word's internal division.
	QuoteProtectsTheClosingBrace BraceQuotePolicy

	// QuoteProtectsTheClosingBraceInPosixMode is where POSIX mode puts the
	// reading above, and the zero value is *unmoved*.
	//
	// bash moves and zsh does not. Measured 2026-09-14 over `v=Vx}y; printf
	// '[%s]' "${v-'a}b'}"` with `s=a}b; printf '[%s]' "${s#'a}'}"` beside it
	// as the pattern-operand control: bash 5.3.15 answers `[Vx}y]` and moves
	// to `[Vx}yb'}]` under `set -o posix`, `--posix`, `POSIXLY_CORRECT=1` and
	// the name `sh` alike, while the pattern operand is unmoved under every
	// one of them; zsh 5.9.2 answers `[Vx}yb'}]` and `-o posixbuiltins` and
	// the name both leave it there. bash 3.2.57 does not move either, which
	// is recorded and not modeled for the reason every 3.2 divergence is.
	//
	// A type of its own rather than a second BraceQuotePolicy field, because
	// BraceQuotePolicy's zero value is BraceQuoteProtectsAPatternOnly — a
	// real reading — and driver calls SetPosixMode(true) for *every* dialect
	// named `sh`. A dialect that never took a position on the mode would
	// acquire one, and zsh-as-`sh` is measured above not to have it. That is
	// the hazard #2659 names one axis over, and the zero value is what
	// answers it here: unanswered stays unanswered, as it does for
	// ForNameRunForm and for the three listing axes (#2604).
	QuoteProtectsTheClosingBraceInPosixMode BraceQuotePosixMove

	// CompoundAssignmentErrorGivesUpTheLine makes a syntax error inside
	// `a=( … )` end the line it was written on rather than the file, so the
	// shell reports it, throws that line away unrun, and reads on.
	//
	// The parentheses of a compound assignment are part of a *word*, and what
	// stands between them is a list read on its own. That is what separates
	// this from every other syntax error: the file around it parsed, and only
	// the list did not.
	//
	// Measured 2026-09-12 from a script file, `echo one` above and `echo two`
	// below, with `a=(p & q)` between them:
	//
	//	bash 5.3.15           one · the complaint · two, status 0
	//	bash 3.2.57           one · the complaint · two, status 0
	//	bash 5.3.15 as `sh`   one · the complaint, status 1
	//	zsh 5.9.2             one · parse error near `&', status 1
	//	ksh93u+               one · `&' unexpected, status 3
	//	dash                  one · "(" unexpected, status 2 — it has no arrays
	//
	// So two columns carry on and four stop, and `bash -n` reports it in
	// every one of them: this is not about *when* the error is found but
	// about how much it ends. `$?` on the line after is 1 in the two that
	// carry on, and a script whose last line is the bad one exits 1 — the
	// status a failed command leaves, not the status a refused file leaves.
	//
	// It is not a property of nested parsing in general. `echo $(if)` is
	// fatal at status 2 in bash, so a substitution's contents are the file's
	// and an assignment's elements are not.
	//
	// The input running out between the parentheses is this too, and only
	// the status is left of it: there is no next line to read on to, and
	// `a=( x` is 1 where `echo $(` — the same message, outside an array
	// literal — is 2 (#2404).
	//
	// A grammar flag rather than a semantics axis because the parser is what
	// recovers: it has to read past the construct for there to be a next line
	// at all. See Parser.giveUpOnTheArray and File.Refused.
	CompoundAssignmentErrorGivesUpTheLine bool

	// ParamIndirection enables `${!x}` to *parse*. bash and ksh93 accept it;
	// dash and zsh reject it outright.
	//
	// What it then means is not this file's question, and separating the two
	// is what finally made the construct expressible. It is the panel's
	// clearest three-way divergence — bash indirects, ksh93 yields the name,
	// dash and zsh error — and a single flag could not carry three answers.
	// Split into "does it parse" here and "what does it mean" in the
	// semantics vector, each half is binary. That is the grammar/semantics
	// split doing the work it was introduced for, on the case that motivated
	// it.
	ParamIndirection bool

	// ParamIndirectionPrefixListing enables `${!name@}` and `${!name*}`, the
	// spelling that yields the **names** beginning with `name` rather than a
	// value. It rides on [Dialect.ParamIndirection], which is what makes the
	// `!` an indirection in the first place.
	//
	// A flag of its own because the three columns that read `${!x}` do not
	// agree about it. bash and ksh93 have both; the third reads `${!x}` and
	// refuses this — measured on `/opt/homebrew/bin/zsh`, `zsh 5.9.2
	// (aarch64-apple-darwin25.4.0)`, from a script file whose first line is
	// `emulate ksh`:
	//
	//	ZQ_a=1; ZQ_b=2; echo ${!ZQ_@}    bad substitution
	//	echo "[${!ZQNOSUCH_@}]"          bad substitution
	//	x=y; y=V; echo "${!x}"           y
	//	typeset -A m; m[k]=v
	//	echo "${!m[@]}"                  k
	//
	// The last two rows are the control: the same mode reads the indirection
	// and the subscript listing in the same script that refuses the prefix
	// form, so this is the *spelling* and not the sigil. `emulate zsh` and
	// `emulate sh` refuse all four, which is [Dialect.ParamIndirection]
	// being off there rather than anything this flag says.
	//
	// The corpus records the other two columns: `${!ZQ_@}` is `ZQ_a ZQ_b` in
	// bash 5.3, bash 3.2, bash-as-`sh` and ksh93u+, and a bad substitution in
	// dash, BusyBox ash and zsh (#4957).
	ParamIndirectionPrefixListing bool

	// IndirectionRefusedAtExpansion carries the refusal of a `${!name}` this
	// dialect cannot read **past the parse** instead of ending it: the
	// expansion is read as unreadable and complains where it would have been
	// expanded, so a `${!x}` in a branch nothing takes costs nothing.
	//
	// The same deferral [Dialect.FlagGroupRefusedAtExpansion] makes, and the
	// same one every other unreadable expansion in this grammar already
	// makes — see the `Bad` sites in paramexp.go. `${!name}` was the outlier,
	// ending the parse where its neighbors are carried.
	//
	// Measured with `set -n`, which is what tells a parse refusal from an
	// expansion-time one: a construct the *parser* refuses is refused
	// wherever it stands, and one the expander refuses is refused only where
	// the shell reads the words. On `/opt/homebrew/bin/zsh` 5.9.2, from a
	// script file, 2026-09-28:
	//
	//	                                  emulate zsh   emulate sh
	//	echo "${!x}"                      1, bad subst  1, bad subst
	//	{ echo "${!x}"; }                 0, silent     0, silent
	//	if true; then echo "${!x}"; fi    0, silent     0, silent
	//	echo "${9nope}"                   1, bad subst  1, bad subst
	//	{ echo "${9nope}"; }              0, silent     0, silent
	//
	// The `${9nope}` rows are the control and they are the whole argument:
	// an expansion this grammar has always deferred answers every position
	// exactly as `${!x}` does, so the two are the same kind of refusal and
	// `${!x}` was being reported in the wrong place.
	//
	// **dash and BusyBox ash defer it as well**, and both are on now (#4974).
	// `/bin/dash` 0.5.12 and BusyBox 1.38.0 each answer
	// `set -n; echo "${!x}"` at 0 and silent, in a group and inside an `if`
	// too, while agreeing with this shell exactly at run time.
	//
	// **ash's wording is not evidence about the place**, and this comment
	// said it was. It answers `syntax error: bad substitution`, which reads
	// like a parse failure — and it answers `${9nope}` with the *same
	// sentence*, an expansion every column in the panel defers. Under
	// `set -n` both are silent at 0 there, so the sentence is that shell's
	// wording for an unreadable expansion rather than a statement about
	// when it was refused. The control is what falsifies the reading: an
	// expectation drawn from a recorded message rather than from a run, and
	// one run settles it.
	IndirectionRefusedAtExpansion bool

	// ParamBangNameContinues lists the characters that carry a *name* on when
	// they stand immediately after `${!`, so the expansion is an indirection
	// rather than the parameter `!` with an operator behind it.
	//
	// A letter, an `_`, and — where [Dialect.DottedName] is on — a `.` carry
	// it on in every column that has the construct, so they are not listed
	// here. What is listed is the rest, and the rest is where the panel
	// splits three ways over the same seventeen characters.
	//
	// The empty string is the core's answer and it is a **positive** one
	// rather than an absence: with no indirection to compete, `${!` is the
	// parameter `!` and whatever stands behind it is an operator, which is
	// what `${!#}`, `${!%}` and `${!:-w}` are in four of the five dialects.
	//
	// Measured 2026-09-20 over a script file, `env -i PATH=/usr/bin:/bin
	// LC_ALL=C` with a scratch HOME, stdin on /dev/null, one character at a
	// time across the seven-column panel:
	//
	//	set -- a b c
	//	sleep 0 &
	//	wait
	//	printf 'bang=[%s]\n' "$!"
	//	printf 'out=[%s]\n'  "${!C}"
	//
	// **`$!` is deliberately set, and that is the whole of why the first
	// measurement on #3966 read the construct backwards.** With no background
	// job `$!` is empty in six of the seven columns, and an empty `$!` with a
	// trim behind it prints exactly what a failed indirection prints — so
	// `${!#}` was recorded as "the name-of operator answering nothing" when
	// it is `$!` with a `#` trim of an empty pattern. With a pid in it the two
	// readings are never the same string.
	//
	// `param` below means the `!` was the parameter — the answer was the pid.
	// `indirect` means the character carried a name on, and the cell says
	// what through.
	//
	//	after ${!   bash 5.3      bash-as-sh   bash 3.2     dash      ksh93          ash       zsh 5.9.2
	//	: - + =     param         param        param        param     param          param     param
	//	%           param         param        param        param     param          param     param
	//	/           param         param        refused      refused   param          param     param
	//	#           indirect $#   param        indirect $#  param     param          param     param
	//	?           indirect $?   param        indirect $?  param     param          param     param
	//	@ *         indirect $@   indirect $@  indirect $@  refused   syntax error   refused   refused
	//	[           subscript     subscript    refused      refused   syntax error   refused   param
	//	^ , ~       param         param        refused      refused   syntax error   refused   refused
	//	$ !         refused       refused      refused      refused   syntax error   refused   refused
	//	.           refused       refused      refused      refused   name           refused   refused
	//	0-9         indirect $1   indirect $1  indirect $1  refused   syntax error   refused   refused
	//	letter _    indirect      indirect     indirect     refused   name-of        refused   refused
	//
	// So the dialect answers, with the reason each one is what it is:
	//
	//   - bash `0123456789#?@*[`. The specials it indirects *through* — the
	//     `${!#}` row is `$3` where `$!` is a pid, and `${!@}` complains about
	//     `a b c: invalid variable name`, which is the value of `$@` and not a
	//     name — plus a digit, which is an ordinary positional there, and the
	//     `[` that opens `${!x[@]}`.
	//   - ksh93 empty. Letters, `_` and its dotted names are the whole of the
	//     name there: `${!.}` answers `.`, every operator character ends the
	//     name, and a **digit is a parse failure** — `${!1}` is “ `1'
	//     unexpected “ and the script ends, where bash indirects through
	//     `$1`. That is the one column that answers a character here with a
	//     refusal to read the file at all.
	//   - dash, ash and zsh empty, and for a different reason: none of them
	//     has the construct, so nothing competes with the `!` and every one
	//     of `# % / : - + = ?` is that parameter with an operator behind it.
	//     zsh takes `[` there too — `${![@]}` is the pid — which is the one
	//     cell where a column with no indirection still parts from the other
	//     two.
	//
	// A character in none of those sets and in no name is refused under
	// **both** readings — `${!$}`, `${!!}`, and `^ , ~` outside bash — so it
	// does not discriminate and is deliberately left out rather than guessed
	// into one: unlisted makes the `!` the parameter, and the operator scan
	// refuses what is not an operator.
	//
	// Distinct from [Dialect.ParamIndirection], which answers whether the
	// construct exists at all. This one answers where it *begins*, and the
	// two are independent: zsh has no indirection and still has an answer
	// here, because the character after `${!` decides whether there is a
	// second reading to reject.
	ParamBangNameContinues string

	// ArithIncDec enables `++` and `--`. Not POSIX; dash rejects them.
	ArithIncDec bool

	// ArithIncDecNeedsAPlace makes `++` and `--` operators only where they
	// stand against something a value can be stored in. Everywhere else the
	// two characters are two signs, exactly as they are in a dialect with no
	// [Dialect.ArithIncDec] at all.
	//
	// A grammar question and not an evaluation one, because the shells that
	// answer it this way never reach an lvalue complaint: measured 2026-09-22
	// on bash 5.3.20, `$(( ++7 ))` is **7** — two signs read — and `$(( 7++ ))`
	// is `arithmetic syntax error: operand expected (error token is "+ ")`,
	// which is the second sign standing with nothing behind it. The shells
	// that take the operator and then refuse the target say so instead, and
	// name the whole text: ksh93 `assignment requires lvalue` and zsh `bad
	// math expression: lvalue required` for all of `7++`, `++7` and `7--`.
	//
	// So this parts the panel where interp.Diagnostics.ArithIncrementNeedsAPlace
	// words it — that field is what the other half says, and reaching it at all
	// is what this flag turns off (#2420).
	ArithIncDecNeedsAPlace bool

	// ArithComma enables the sequence operator. Not POSIX; dash rejects it.
	ArithComma bool
	// ArithCommaMayEndTheExpression lets a sequence end at its comma, the
	// value being what stands in front of it. Measured 2026-10-02: `echo
	// $(( 3, ))` writes 3 in ksh93u+ 2012-08-01, and is an operand that ran
	// out in zsh 5.9.2 (`bad math expression: operand expected at end of
	// string`) and in bash 5.3.20 (`operand expected (error token is ", ")`).
	ArithCommaMayEndTheExpression bool

	// ArithExponent enables `**`, exponentiation. Not POSIX — the operator
	// is not in ISO C either — and dash rejects it; bash, ksh93 and zsh all
	// have it. What a negative exponent means is not this flag's question:
	// the shells that parse it disagree, and the semantics vector answers.
	ArithExponent bool

	// ArithExponentAssign enables `**=`, exponentiation's compound
	// assignment. One shell in the panel has it.
	//
	// **It is not [ArithExponent]'s to imply, and that is measured rather
	// than assumed.** Every column that parses `**` at all was asked, and
	// three of the four columns that have the operator refuse the
	// assignment spelling — measured 2026-09-26, `n=2; $(( n **= 3 ))`:
	//
	//	bash 5.3      arithmetic syntax error: operand expected
	//	ksh93u+       arithmetic syntax error
	//	BusyBox ash   arithmetic syntax error
	//	zsh 5.9.2     8, and n is 8
	//
	// So the operator and its assignment are two questions, and folding them
	// into one flag would hand `**=` to three shells that refuse it. dash is
	// not a fourth column here: it has neither.
	//
	// The spelling is *not* already taken where the flag is off, which is the
	// difference from [ArithLogicalXor]: `x **= 3` there is `x ** ` with the
	// operand missing, so the refusal names `= 3` rather than reading as
	// something else. What a negative or fractional exponent means is not
	// this flag's question either; see [ArithExponent].
	ArithExponentAssign bool

	// ArithLogicalXor enables `^^`, the logical exclusive-or of an arithmetic
	// expression, and its assignment spelling `^^=`. One shell in the panel
	// has them and no script that runs under bash can contain one.
	//
	// It yields 1 or 0 like every other logical operator there, evaluates
	// both operands — there is nothing to short-circuit, since neither side
	// can decide the answer alone — and sits on the `||` rung of that
	// shell's own ladder, left-associative, while [ArithPrecedence]'s C order
	// gives it a rung of its own between `||` and `&&`. Both are measured;
	// see docs/spec/grammar/arithmetic.md.
	//
	// The spelling is already *taken* where the flag is off: `^` is bitwise
	// xor, so `a ^^ b` there is an xor whose right operand is missing, which
	// is what every column without the operator reports (#2991).
	ArithLogicalXor bool

	// ArithExplicitBase enables the `base#digits` form. Absent from dash.
	ArithExplicitBase bool

	// GroupOpeningAPatternOperandIsRefused stops the parse at a `(` standing
	// first in an expansion's *pattern* operand — `${v#(a)}`, `${v%(b)}`,
	// `${v/(a)/Z}` — rather than reading it as part of the pattern.
	//
	// A refusal rather than a construct, which is the one shape a grammar
	// flag here takes the other way round, and it is what the panel says:
	// four columns accept the text and one refuses it. Measured 2026-09-12
	// with `v=aXb`:
	//
	//	                dash   bash 5.3, 3.2, as sh   ksh93        zsh
	//	${v#(a)}        aXb    aXb                    syntax error Xb
	//	${v%(b)}        aXb    aXb                    syntax error aX
	//	${v/(a)/Z}      —      aXb                    syntax error ZXb
	//	${v#@(a)}       aXb    aXb                    Xb           aXb
	//	${v#\(a\)}      aXb    aXb                    aXb          aXb
	//
	// The fourth row is the control that makes it the *bare* spelling: `@(`
	// is that shell's own group and is read. The fifth is the other one: an
	// escaped parenthesis is an ordinary character and passes.
	//
	// A **word** operand is not this question and is measured to be so:
	// `${u:-(a)}`, `${u-(a)}` and `${u:=(a)}` all come to `(a)` in that
	// shell. So it is the pattern's reader that refuses and not the brace.
	//
	// The refusal is a syntax error at the operand's position, naming `(`,
	// and it takes the script with it — status 3 in the one shell that has
	// it.
	GroupOpeningAPatternOperandIsRefused bool

	// ArithColonIsAToken reads `:` as a math token wherever it stands rather
	// than as text no operator could be. zsh alone.
	//
	// Measured 2026-09-12: `$(( 1 : ))` there is `operand expected at end of
	// string` — the reader took the colon and then wanted a value — and
	// `$(( 1 : 2 ))` is `':' without '?'`, which is a complaint only a reader
	// that got as far as the second value can make. The other shells stop at
	// the byte: bash blames `:` and `: 2` with the sentence it gives any
	// leftover text, and ksh93 calls it an invalid character.
	//
	// A grammar flag rather than a wording, because what differs is how far
	// the reader gets before it complains and not what it says when it does.
	ArithColonIsAToken bool

	// ArithNumeralEndsAtABadDigit stops a numeral at the first character its
	// own base cannot use, rather than reading every character the base-64
	// alphabet knows and refusing the lot. zsh alone.
	//
	// Measured 2026-09-12. The two readings differ in *how many tokens* the
	// text is, which is why it is a grammar flag and not a wording:
	//
	//	           1abc                        0y
	//	bash 5.3   1abc: value too great …     0y: value too great …
	//	zsh 5.9.2  operator expected at `abc'  operator expected at `y '
	//
	// The base is known from the text — a radix prefix names it, a `base#`
	// names it, ten otherwise — so the reader can stop where that shell
	// stops. It is the same rule behind three rows that look unrelated:
	// `$(( 2#12 ))` is `operator expected at `2'` there, `$(( 08#9 ))` at
	// `9`, and `$(( 0b2 ))` at `2`, each being a digit run that ended early
	// with the rest left standing.
	ArithNumeralEndsAtABadDigit bool

	// ArithLeadingZeroNamesOctalDigits makes a leading `0` name the numeral's
	// base for the reader above, so the numeral ends at the first digit
	// **eight** cannot use rather than at the first digit ten cannot.
	//
	// The **lexical** half of the option one dialect spells `octalzeroes`,
	// and it is separate from Semantics.ArithLeadingZeroIsOctal for the same
	// reason [Dialect.CloseBraceEndsAWord] is separate from its reserved
	// half: where a numeral *ends* is a question about how text is lexed, the
	// value it comes to is a question about what the digits mean, and one
	// dialect switches both at run time with one name.
	//
	// Without it the two halves disagree and the numeral is refused whole.
	// Measured 2026-09-28 on `/opt/homebrew/bin/zsh` — zsh 5.9.2
	// (aarch64-apple-darwin25.4.0), `go version -m` says *not a Go
	// executable* for it — `-f` over a script file under
	// `env -i PATH=/usr/bin:/bin TERM=dumb` with a scratch `HOME` and stdin
	// at `/dev/null`, the option set on the line above:
	//
	//	                  zsh 5.9.2                          before
	//	$(( 08 ))         operator expected at `8 '          invalid number: 08
	//	$(( 09 ))         operator expected at `9 '          invalid number: 09
	//	$(( 0778 ))       operator expected at `8 '          invalid number: 0778
	//	$(( 1 + 08 ))     operator expected at `8 '          invalid number: 08
	//	$(( 08 + 1 ))     operator expected at `8 + 1 '      invalid number: 08
	//	v=08 under -i     operator expected at `8'           invalid number: 08
	//
	// **The fifth row is the one that says it is where the numeral ends and
	// not how the refusal is worded.** The quoted text is the *rest of the
	// expression* — `8 + 1`, not `8` — so the reader stopped after the `0`
	// and handed everything from the bad digit on to the parser, which is
	// two tokens where this shell had one.
	//
	// The controls hold still on both sides: `$(( 010 ))` is 8 and
	// `$(( 0777 ))` is 511 with the option on, `$(( 0x18 ))` is 24 because a
	// radix prefix names its own base, and `$(( 08 ))` is **8** with the
	// option off, where a leading zero names nothing and ten can use the
	// digit (#4436).
	ArithLeadingZeroNamesOctalDigits bool

	// ArithDigitSeparator makes an underscore inside a numeral a **digit
	// separator**: it is skipped, and the numeral reads as though it were
	// not there. zsh alone.
	//
	// Measured 2026-09-13 against zsh 5.9.2 and bash 5.3.15. `$(( 1_ ))` was
	// the row that started this and it cannot decide it — 1 is what both a
	// separator and a discarded byte would give — so the question is `1_0`:
	//
	//	            1_    1_0   1_0_0  0x1_f  2#1_0  16#f_f  1_0.5  1_abc
	//	zsh 5.9.2   1     10    100    31     2      255     10.5   operator
	//	                                                             expected
	//	                                                             at `abc'
	//	bash 5.3    value too great for base, every one of them
	//
	// Ten is what a separator gives and nothing else does: the byte is
	// neither a digit — bash reads it as digit 63 of the base-64 alphabet and
	// then refuses a digit base ten has no room for, which is what
	// ArithNumeralEndsAtABadDigit's sibling rows pin — nor a leftover token,
	// `1` followed by an unset name being an `operator expected` rather than
	// a 1.
	//
	// **Removed, and then the ordinary rules apply to what is left.** That is
	// the whole rule and it is worth stating that way round, because every
	// other row follows from it: `setopt octalzeroes; $(( 0_10 ))` is 8, so
	// the leading zero the separator uncovers is an octal prefix; `$(( 1_#5
	// ))` is `invalid base … : 1`, so the base is read from the cleaned text;
	// `$(( 0x_1 ))` and `$(( 2#_10 ))` are 1 and 2, so a separator may stand
	// where the first digit would; `$(( 1__0 ))` is 10, so a run of them is
	// one; and `$(( 1_ ))` is 1, so a trailing one belongs to the numeral it
	// follows rather than being left standing.
	//
	// **It is only a separator inside a numeral.** A numeral begins with a
	// digit, so a leading underscore is a name as it always was: `$(( _ ))`
	// and `$(( _1 ))` are both 0, an unset name being zero.
	//
	// A grammar flag rather than a semantics axis, and asked in the two
	// places that need it: the parser, where it decides how far the numeral
	// reaches, and the conversion, where a value a *variable* was holding is
	// read by the same rule — `x=1_0; $(( x ))` is 10 there, and no parser
	// saw that text.
	//
	// One shape is knowingly short of the shell, and it is a wording rather
	// than a value. zsh cleans the token and then reports what is left of the
	// *cleaned* text, so `$(( 1e_foo ))` blames `efoo`; this reader leaves the
	// separator standing in the leftover and blames `e_foo`. Both stop in the
	// same place and both are an `operator expected`. #2223.
	ArithDigitSeparator bool

	// ArithBinaryLiteral enables `0b101`, the binary radix prefix. zsh alone
	// among the panel: measured 2026-09-12, `$(( 0b101 ))` is 5 there and
	// `0b101: value too great for base` in bash 5.3, bash 3.2 and
	// bash-as-sh, which read it as an octal constant carrying a `b`. ksh93
	// and dash refuse it too.
	//
	// A grammar flag rather than an axis for the reason ArithFloat is one:
	// the question is whether the dialect has the literal at all, not what
	// it means where both have it.
	ArithBinaryLiteral bool

	// ArithHexFloat enables C's hexadecimal float spelling inside `$(( ))`:
	// a hexadecimal literal carrying a point or a `p` exponent is a float.
	// ksh93 alone among the panel — measured 2026-09-16, `$(( 0x1p4 ))` is
	// 16 and `$(( 0x1.8 ))` is 1.5 there, while bash 5.3, bash 3.2,
	// bash-as-sh, zsh 5.9.2, dash 0.5.12 and BusyBox ash 1.37.0 all refuse
	// every shape of it.
	//
	// The `e` of the decimal spelling is not this: `0x1e5` is the integer
	// 485 in every column, `e` being a hexadecimal digit. Only a point or a
	// `p` makes the literal a float.
	//
	// A grammar flag rather than an axis for the reason ArithFloat and
	// ArithBinaryLiteral are: the question is whether the dialect has the
	// literal at all, and the reader and the evaluator must not be able to
	// disagree about it. See interp's hexFloatNumeral for the measured
	// table, the two shapes ksh93 refuses and what an exponent with no
	// digits comes to.
	ArithHexFloat bool

	// DollarBracketArith enables `$[expr]`, the older spelling of `$((expr))`.
	//
	// Measured 2026-09-06: bash 5.3.15, bash 3.2.57, bash invoked as `sh` and
	// zsh 5.9.2 all read it as arithmetic — `echo $[1+1]` is 2, `$[2**10]` is
	// 1024, and it expands inside double quotes and in a here-document body
	// exactly as `$((…))` does. ksh93u+ and dash do not read it at all: the
	// `$` stays literal and the brackets are a pattern, so `echo $[1+1]`
	// prints `$[1+1]` when nothing on the filesystem matches.
	//
	// The additive kind of difference, so a grammar flag: where it is off the
	// text takes the route it takes today and nobody means something else by
	// it. bash has *documented* it as deprecated for years, which is a fact
	// about its manual rather than about its parser — both builds in the
	// panel still take it, which is why they are separate members.
	//
	// The construct is arithmetic and nothing else: the expression inside is
	// the same grammar `$((…))` holds, the diagnostics for a bad expression
	// or a division by zero are word for word the ones `$((…))` gives, and
	// what is produced is an ArithSubst span. Only the spelling differs,
	// which Span.Bracketed carries for anything writing one back.
	DollarBracketArith bool

	// ArithSubstFallsBackToCommandSubst decides what `$((` opens where the
	// parentheses do not close as `))`.
	//
	// `$(( … ))` is arithmetic and `$( ( … ) )` is a command substitution
	// whose first command is a subshell, and the second may be written with
	// the parentheses touching. POSIX tells the author to separate them and
	// says nothing about the shell that meets them together, so this is
	// measured.
	//
	// Measured 2026-09-12: `echo $((echo ab cde) )` prints `ab cde` in bash
	// 5.3.15, in the 3.2.57 macOS ships, in that build invoked as `sh`, in
	// ksh93u+ and in zsh 5.9.2; dash 0.5.12 and BusyBox ash 1.37.0 refuse it
	// for a missing `))`. Every shell in the panel but the two minimal ones,
	// which is the head count that put ProcessSubstitution in the core, so
	// this is on in [Core] and off in [POSIX].
	//
	// Additive rather than a conflict: where it is off the text is arithmetic
	// and nothing else, which is what the two say by refusing it. Where it is
	// on, the rule the five agree on is positional — counting from one after
	// the `$((`, the `)` that brings the count to zero is arithmetic only
	// when another `)` follows it immediately. So `$(( 1 ) + (2 ))` is a
	// command substitution even though `(1) + (2)` is good arithmetic, and
	// `$(( (1+2)) )` runs `1+2` as a command where `$(( (1+2) ))` is 3.
	ArithSubstFallsBackToCommandSubst bool

	// ArithSubstClosesOnlyAtTwoParens ends an arithmetic expansion only at a
	// `))` standing where its own parentheses are balanced. A lone `)` there
	// is text the expression will refuse, and input that runs out first is
	// the unclosed `$((`. Measured 2026-10-04 on dash 0.5.12 and in the
	// pinned BusyBox ash image:
	//
	//	echo $((echo a) )         Missing '))'
	//	echo "[$(( (1+2)) )]"     Missing '))'
	//	echo $(( (1+2)) ))        expecting EOF: " (1+2)) " — the `))` at
	//	                          the end closes it, the lone `)` was text
	//
	// The two that do not fall back to a command substitution; see
	// ArithSubstFallsBackToCommandSubst for the three that do.
	ArithSubstClosesOnlyAtTwoParens bool

	// ArithFloat enables floating point, which ksh93 and zsh have and POSIX
	// does not.
	ArithFloat bool

	// ArithEmptyExponentIsConsumed takes a decimal float's exponent letter,
	// and a sign after it, into the numeral even where no digit follows, and
	// reads the numeral as though the exponent were not there.
	//
	// ksh93 alone. Measured 2026-10-03 on ksh93u+ 2012-08-01: `$(( 1.e ))`,
	// `$(( 1e ))`, `$(( 1.e+ ))` and `$(( 3e+ ))` are 1, 1, 1 and 3, and
	// `$(( 2.5e ))` is 2.5, while `$(( 1.ex ))` is still `arithmetic syntax
	// error` — the `x` is a byte after the numeral and not part of it. The
	// hexadecimal float's `p` already reads this way in the same shell (see
	// hexFloatTail). zsh ends the numeral before the letter, so `1.e` is a
	// number and then a name, and its operator complaint follows.
	ArithEmptyExponentIsConsumed bool

	// ArithBytesRefusedOutright are the bytes this shell's arithmetic reader
	// refuses as part of no token at all, and reports at the byte rather than
	// as a missing operand: `$((@))` is `illegal character: @` in the one
	// shell in the panel that has the sentence.
	//
	// A table of bytes and not a code path, which is the whole reason it is
	// here: the other four dialects have no such sentence, and for them this
	// is empty and every failure keeps the wording it already had. Measured
	// 2026-09-16, `x=$((@))` is `operand expected` in the three bash builds,
	// `arithmetic syntax error` in ksh93 and in BusyBox ash, `expecting
	// primary` in dash, and `illegal character: @` in zsh alone. The bytes are
	// measured, not derived — every other punctuation byte tried is either a
	// math token in that shell or can begin a value.
	//
	// It does not decide on its own. The same byte gets the operand sentence
	// where an operator has just been consumed and a value is wanted, so the
	// parser asks this only at the two positions where the expression could
	// legally have stopped: before anything has been read, and where an
	// operator belonged.
	ArithBytesRefusedOutright string

	// ArithBadByteAfterAGroupWantsAnOperand words a byte that can begin no
	// token, standing where an operator belonged right behind a closing
	// parenthesis, as a missing *operand* rather than as a bad operator.
	// Measured 2026-10-04 on bash 5.3.20: `$(( (1)@ ))`, `$(( -(1)@ ))`,
	// `$(( (1) @ ))` and `$(( (1)[2] ))` are each `operand expected`, where
	// `$(( 1@ ))`, `$(( x@ ))` and `$(( x[1]@ ))` — the same byte behind a
	// number, a name or a subscript — are `invalid arithmetic operator`.
	// bash only; see docs/spec/arithmetic.md.
	ArithBadByteAfterAGroupWantsAnOperand bool

	// ArithLeftoverOperandReadsTheNextToken reads one token past an operand
	// standing where an operator belonged before refusing it, so a byte that
	// can begin no token right after that operand is what gets refused.
	// Measured 2026-10-04 on bash 5.3.20: `$(( 1 x@ ))` and `$(( (1)x[2]@ ))`
	// are `invalid arithmetic operator (error token is "@ ")`, where
	// `$(( 1 x y ))` and `$(( (1)x ))` — nothing unreadable behind the
	// operand — keep `syntax error in expression` naming the operand on.
	// bash only; see docs/spec/arithmetic.md.
	ArithLeftoverOperandReadsTheNextToken bool

	// ArithDoubleQuote is what a `"` standing inside an arithmetic expression
	// is: a byte the reader passes over, a byte it removes from the text
	// before reading it at all, or no part of any token. See
	// [ArithDoubleQuotePolicy].
	//
	// Measured 2026-09-07 and re-measured 2026-09-10, from a script file with
	// `n=5`: `$(( "1" + 1 ))` is 2, `$(( "n" + 1 ))` is 6 and `$(( 1 + "2" ))`
	// is 3 in bash 5.3.15, that build as `sh`, ksh93u+ and zsh 5.9.2; bash
	// 3.2.57, dash and BusyBox ash refuse all three, ash with the bare
	// `arithmetic syntax error` it gives every reader failure. So four
	// columns read what the quote held and three refuse it — additive, which
	// is why it is here and not on the semantics vector, exactly as
	// [ArithCharacterCode] is.
	//
	// It matters more than the spelling suggests: `$(( "$n" + 1 ))` looks
	// defensive and is common, and refusing it fails under three of the five
	// dialects graded here (#1223).
	ArithDoubleQuote ArithDoubleQuotePolicy

	// ArithSubscriptQuoting says a quotation inside an arithmetic subscript
	// holds its brackets: the `]` that ends the subscript is one written
	// outside the quotes, so a key spelled with a bracket in it is
	// reachable.
	//
	// Measured 2026-09-16 from a script file with `typeset -A a; a[']']=5`:
	// `$(( a[']'] ))` is 5 in bash 5.3.20, under that build as `sh` and in
	// ksh93u+ 2012-08-01, and so is `(( a[']'] ))`. bash 3.2.57 has no
	// associative arrays to ask, and dash and BusyBox ash have no arrays at
	// all.
	//
	// The same subscript inside `[[ ]]` is deliberately *not* this flag, and
	// that is measured rather than assumed: ksh93u+ answers `[[ a[']'] -eq 5
	// ]]` with `a[]]: arithmetic syntax error` in the same run that answers
	// the `(( ))` line with 5. A condition's operand reaches its arithmetic
	// already expanded, so what a shell does there is a question about which
	// reading it takes rather than about what its reader sees — see
	// interp.Semantics.ConditionArithmeticReadsTheWrittenSubscript.
	//
	// zsh 5.9.2 is measured *off* rather than left out: it will not store
	// such a key from an assignment at all, and with the element put there
	// by `a=( "]" 5 )` instead, `e="a[']']"; $(( $e ))` is `bad math
	// expression: illegal character: '` — the quote reaching its reader as
	// a byte and not as a quotation. Which is the same shell that reads a
	// subscript's expanded text back as syntax, and for a reader that never
	// sees quoting the two are one behavior (#3302).
	//
	// Additive rather than a semantics axis, for the reason
	// ArithDoubleQuote is: it is what the *reader* does with a byte, and a
	// dialect without subscripts never reaches it.
	ArithSubscriptQuoting bool

	// ArithPrecedence is the order the binary operators bind in. See
	// [ArithPrecedencePolicy]: one shell in the panel does not use C's, and
	// says so in its own manual.
	//
	// It is a grammar question and not a semantics one, which is what puts
	// it here: nothing about what `<<` *means* changes, only which operands
	// it is given, so the disagreement is entirely about the tree the text
	// parses to.
	//
	// Measured 2026-09-15 from a script file, `env -i` with a scratch HOME:
	//
	//	                 zsh 5.9.2   bash 5.3   ksh93   dash
	//	1 << 2 + 1           5           8        8       8
	//	1 + 2 << 1           5           6        6       6
	//	1 << 2 * 2           8          16       16      16
	//	1 < 2 & 1            0           1        1       1
	//	2 ** 1 | 3           8           3        3       -
	//	6 | 1 + 1            8           6        6       6
	//
	// Parenthesized, every column agrees, which is what says this is
	// precedence and not a broken operator.
	ArithPrecedence ArithPrecedencePolicy

	// ArithCharacterConstant enables `'c'` inside an arithmetic expression:
	// the code of the character between the quotes, the way C reads one.
	//
	// One shell in the panel. Measured 2026-09-10 on ksh93u+, where
	// `$(( '1' + 1 ))` is 50 — the code of `1` is 49 — `$(( 'a' ))` is 97 and
	// `$(( '\101' ))` is 65, against bash 5.3.15, bash 3.2.57, bash as `sh`
	// and dash, which call it an arithmetic syntax error, and zsh 5.9.2,
	// which refuses the byte outright as an illegal character. A literal one
	// shell has and five refuse is the additive kind of split (#1223).
	//
	// The closing quote is optional, which is measured rather than assumed:
	// `$(( 'a ))` is 97 and `$(( ” ))` is 39 — the second quote read as the
	// character, with nothing left to close it. That also says why `$(( 'ab' ))`
	// is a syntax error rather than a multi-character constant: the reading
	// stops after `a`, and the `b` is left standing where an operator belongs.
	//
	// Kept apart from the double-quote question above because no shell's
	// answer to one predicts its answer to the other: the four that read
	// through a double quote include three that refuse this.
	ArithCharacterConstant bool

	// ArithCharacterEscapes is the escape vocabulary the *one character* after
	// a character-code operator is written in — the operand of `##c` and of the
	// `'c'` constant alike.
	//
	// A span and not a meaning. What an escape stands for is decoded where the
	// value is taken, against the same table a `$'…'` is read with; all the
	// parser has to know is where the one character ends. The common escapes —
	// `\x41`, `\u00e9`, `\101`, and a backslash before anything else — are
	// spanned the same way everywhere and are not this field's business.
	//
	// `\C` and `\M` are, because the two shells that have them **span them
	// differently**, and the span is what decides whether the operand is
	// complete. The key is the *dialect's escape table* and not the operator
	// that reached it, which is worth saying because each of the two operators
	// lives in exactly one dialect and so cannot tell the two readings apart on
	// its own. The table is measurable without either operator, and that is the
	// evidence: measured 2026-09-26 through `od`, `$'\C-a\M-b'` is `01 e2` in
	// zsh 5.9.2, `6d 61 1b 62` in ksh93u+ 2012-08-01 — `m`, `a`, ESC, `b` — and
	// the eight characters as written in bash 5.3.20. See
	// interp.DollarSingleCaretMetaPolicy, which is the same split seen from the
	// decoding end.
	ArithCharacterEscapes ArithCharacterEscapes

	// ArithConditionalThenLevel is the level the **then** branch of
	// `c ? t : e` is read at, and ArithConditionalElseLevel the level the
	// **else** branch is read at.
	//
	// Two fields rather than one, because the panel's columns do not hold the
	// two branches at the same level and no single value can say so. C's
	// grammar is the shape three of the columns have — the then-expression is
	// a full `expression`, so the sequence operator may stand in it, and the
	// else-expression is a `conditional-expression`, which is not a place — and
	// the other two each part from it at one end. Measured 2026-09-27 on
	// bash 5.3.20 and 3.2.57, ksh93u+ 2012-08-01, zsh 5.9.2 run `-f`, dash
	// 0.5.12 and BusyBox ash 1.37.0:
	//
	//	written                   bash  ksh93  zsh             dash    ash
	//	$(( 1 ? 2 , 3 : 4 ))      3     3      ':' expected    —       3
	//	$(( 1 ? x = 2 : 3 ))      2     2      ':' expected    2       2
	//	$(( 1 ? 2 : x = 3 ))      refused  2   lvalue required refused refused
	//	$(( 0 ? 2 : x = 3 ))      refused  3   lvalue required refused refused
	//	$(( 1 ? 2 : (x = 3) ))    2     2      2               2       2
	//	$(( 1 ? 2 : 3 , 4 ))      4     4      4               —       4
	//
	// So the then branch is the sequence level everywhere but zsh, and the
	// else branch is the conditional level everywhere but ksh93. The last two
	// rows are the controls that make each of those a **level** rather than a
	// ban: a parenthesized store in the else is taken in every column, and a
	// comma written after the whole conditional belongs to the expression
	// around it rather than to the branch, so `4` is the same answer under
	// both readings of the else.
	//
	// The dash cells written `—` are the sequence operator being absent from
	// that dialect altogether rather than an answer about a branch: `$(( 1 ,
	// 2 ))` is refused there too, so nothing measurable distinguishes the two
	// loosest levels in that column. It holds the sequence level vacuously,
	// which is what the zero value means.
	//
	// A refused else branch earns the refusal a `$(( 7 = 4 ))` earns, because
	// the whole conditional is left standing as the target of the store —
	// which is what makes the sentence the dialect's own rather than one
	// written here. zsh's `lvalue required` and bash's `attempted assignment
	// to non-variable` are the same reading (#4775, #4776).
	ArithConditionalThenLevel ArithConditionalBranchLevel

	// ArithConditionalThenLeftoverIsAnOperand blames an operand standing
	// where a conditional's `:` belonged as the operand it is — `operator
	// expected` at it — rather than as a missing colon. Measured 2026-10-02:
	// zsh 5.9.2 says `operator expected at `abc : 3 '` for `$(( 1 ? 2 abc :
	// 3 ))` and for `(3)` in that place, and `':' expected` for `;` or for
	// nothing at all; bash 5.3.20 says `:' expected for conditional
	// expression`, ksh93u+ its plain syntax error and dash `expecting ':'`
	// for every one (#5378).
	ArithConditionalThenLeftoverIsAnOperand bool

	// ArithConditionalElseLevel is the level the **else** branch of
	// `c ? t : e` is read at. See [Dialect.ArithConditionalThenLevel] for the
	// measurement, which covers both.
	ArithConditionalElseLevel ArithConditionalBranchLevel

	// ExtendedPattern enables `@(a|b)`, `?(a)`, `+(a)`, `*(a)` and `!(a)` in
	// a pattern: a group with a quantifier in front of it. ksh93 has them
	// wherever a pattern may stand.
	ExtendedPattern bool

	// ExtendedPatternInCondition enables the same groups inside `[[ ]]` and
	// nowhere else, which is bash's answer: `[[ abc == @(abc|xyz) ]]` matches
	// there while `case abc in @(abc|xyz))` is a syntax error, because bash
	// reads the condition's operand under rules a `case` pattern does not
	// get. ksh93 answers yes to this as well as to the field above; a shell
	// with neither leaves both false.
	ExtendedPatternInCondition bool

	// CountedPatternGroup enables `{n,m}(pattern)`: a repetition count
	// written in braces in front of a pattern group, matching the group
	// between n and m times. It is ksh93's alone.
	//
	// **It is a grammar flag before it is a pattern one**, which is why it
	// lives here beside [Dialect.ExtendedPattern] rather than only in the
	// matcher. The parenthesis is what needs letting into the word: without
	// this the `(` after the `}` ends the word and the parser reports it
	// unexpected, so nothing a matcher could do would ever see the text.
	//
	// The `}` decides it and the brace's contents do not. Measured
	// 2026-09-27 on `/bin/ksh` `Version AJM 93u+ 2012-08-01`, each probe
	// under `env -i PATH=/usr/bin:/bin`:
	//
	//	echo A{2,3}(a)B      A{2,3}(a)B      read, and matched nothing
	//	echo A{,}(a)B        A{,}(a)B        a count with both ends omitted
	//	echo A{z,y}(a)B      A{z,y}(a)B      not a count, and still read
	//	echo A{}(a)B         `(' unexpected  an *empty* brace is not one
	//
	// So a brace with anything at all between its halves takes the `(`, and
	// an empty one does not — which is the whole of what this flag asks. Why
	// the text then matches nothing is the matcher's question and is
	// answered there.
	//
	// The brace must be **written and unquoted**: `f "{2,3}"(a)` is
	// “syntax error … `(' unexpected“ in that shell, the same refusal a
	// shell without this gives, because a quoted brace is not brace syntax
	// and so has no `}` for the parenthesis to stand behind.
	//
	// bash 5.3.20 refuses `echo A{2,3}(a)B` with `extglob` on or off, dash
	// refuses it, and zsh 5.9.2 reads the brace as an ordinary list and the
	// `(a)` as its own bare group — `no matches found: A2(a)B`. One column,
	// which is why this is a flag of its own and not a widening of
	// [Dialect.ExtendedPattern].
	CountedPatternGroup bool

	// CompletionConditions enables the four completion-context tests —
	// `[[ -prefix … ]]`, `[[ -suffix … ]]`, `[[ -after … ]]` and
	// `[[ -between … … ]]` — as conditions whose operator stands in front of
	// its operands.
	//
	// **The arities**, measured on zsh 5.9.2, 2026-09-19. `-prefix` and
	// `-suffix` take a pattern with an optional count in front of it, and
	// `-between` takes a start pattern and an end pattern:
	//
	//	[[ -prefix a ]]        [[ -prefix 1 '*=' ]]    the condition
	//	[[ -prefix a b c ]]                            unknown condition, 2
	//	[[ -after a ]]                                 the condition
	//	[[ -after a b ]]                               unknown condition, 2
	//	[[ -between a b ]]                             the condition
	//	[[ -between a ]]       [[ -between a b c ]]    unknown condition, 2
	//
	// A count outside the range is the run-time refusal a known operator
	// with a bad arity already gets here — see
	// ConditionIsResolvedWhenItRuns, which is the same sentence and the
	// same status.
	//
	// zsh alone, and the other four have no such operator at all — `-prefix`
	// is the completion system's, and dash has no `[[ ]]` — so this is
	// additive grammar for one dialect rather than a divergence.
	//
	// They are in the **grammar** unconditionally there and the restriction
	// is on where they may *run*. Measured 2026-09-11 and again 2026-09-12
	// with `-n` against a script file, zsh 5.9.2: `[[ -prefix : ]]`,
	// `[[ -prefix 'ab' ]]`, `[[ -prefix //(a|b)/ ]]` and `[[ -suffix : ]]`
	// all parse, and running any of them answers `condition can only be used
	// in completion function` at status 1. That split is the one this
	// substrate draws everywhere, and it is what makes the gap a parser's: a
	// completion function is a file, and a file that will not parse never
	// gets as far as the restriction. Two files in an ordinary plugin tree
	// reach it, both completions from `zsh-users/zsh-completions` (#1879).
	//
	// The operand is read the way a pattern operand is rather than the way
	// `-o`'s option name is: `//(127.0.0.1|localhost)/` is one of the two
	// real occurrences, and it is a pattern with a group in it.
	//
	// Deliberately **only these four**. The same shell parses `[[ -nosuch x
	// ]]` as well and refuses it at run time with a different sentence, which
	// is a decision to change the rule that an operator is either implemented
	// or refused while reading — see docs/spec/grammar/conditions.md, and
	// #965, which is that question and not this one.
	//
	// **Loading `zsh/complete` is not what makes them exist.** Measured
	// 2026-09-19 on zsh 5.9.2 with a fresh `-f` shell, whose `zmodload`
	// listing is `zsh/main` alone: `[[ -prefix foo ]]` answers `condition
	// can only be used in completion function` at status 1 *before* the
	// module is loaded and the identical sentence after it. So the module is
	// a no-op for every one of them and the grammar carries them always,
	// which is why this is a dialect flag and not a thing `zmodload` turns
	// on (#3042).
	CompletionConditions bool

	// ConditionIsResolvedWhenItRuns lets `[[ … ]]` accept an operator whose
	// name this shell will look up *when the condition runs*, and leaves the
	// refusal to the interpreter. Two shapes reach it and the shell gives
	// them one sentence: a known operator standing with the wrong number of
	// operands, and a `-word` that is no condition here at all.
	//
	// The grammar question #965 is about, and it is the one place in this
	// parser where a condition is *accepted* and then refused. Measured
	// 2026-09-14 on zsh 5.9.2, which is the only column that does it:
	//
	//	[[ -n ]]            unknown condition: -n     at evaluation, status 2
	//	[[ -n x y ]]        unknown condition: -n     the same
	//	[[ -n x -z "" ]]    unknown condition: -n     the same
	//
	// against bash 5.3 and ksh93, which refuse all three while reading and
	// name the offending token. `echo pre; [[ -n x y ]]` prints `pre` in zsh
	// and prints nothing in the other two, which is what says where the
	// refusal happens rather than only how it is worded.
	//
	// **A name this shell has not got is the same reading**, measured
	// 2026-09-22 on the same binary and the half this flag did not cover
	// until #4261:
	//
	//	[[ -Q x ]]          unknown condition: -Q     at evaluation, status 2
	//	[[ -R x ]]          unknown condition: -R     the same
	//	[[ -bogus x ]]      unknown condition: -bogus the same
	//	[[ -eq x ]]         unknown condition: -eq    the same
	//	[[ 1 == 1 || -Q x ]]  0, and nothing said     — never evaluated
	//
	// The last row is what says this is a lookup rather than a wording: a
	// short-circuit reaches the `]]` with the operator never resolved, so
	// there is no complaint to make. This parser refused all five while
	// reading, which no argument about wording could have fixed.
	//
	// **Two boundaries keep it from swallowing ordinary lines**, and both are
	// measured on the same day. A name of three characters or more is an
	// operator only where something stands behind it — `[[ -bogus ]]`,
	// `[[ -zz ]]`, `[[ -eq ]]` and `[[ -bogus && -n x ]]` are all 0, the
	// bare-word reading, where a two-character `-X` is an operator wherever
	// it stands and `[[ -Q ]]` is the refusal. And a two-operand operator
	// behind the word makes the word that operator's **left operand**:
	// `[[ -Q == bar ]]` is a string comparison answering 1 and
	// `[[ -1 -lt 2 ]]` is arithmetic answering 0, which a rule firing on
	// every `-word` would have refused.
	//
	// A lone `-` is not a name in either half: `[[ - x ]]` is
	// `parse error: condition expected: -` there.
	//
	// **And not where the operand is itself an operator**, which is the row
	// that stops this being "everything after the operand is surplus":
	// `[[ -n -z x ]]` is `parse error near `x'` in zsh, where `[[ -n x y ]]`
	// is the run-time refusal — so an operator-shaped operand starts a
	// reading of its own and the word after it is simply unexpected.
	// `[[ -n -n ]]` is 0 in the same shell, which is that reading finishing.
	ConditionIsResolvedWhenItRuns bool

	// ParameterIsSetTest enables `[[ -v name ]]`, which asks whether a
	// parameter is set rather than anything about its value.
	//
	// A grammar flag rather than core, and bash 3.2 is why: it is the one
	// shell in the panel with `[[ ]]` that does not have the operator, and it
	// does not merely answer differently — it cannot read the line at all,
	// `conditional binary operator expected` followed by `syntax error near
	// `x”. dash has no `[[ ]]` to put it in. So the head count that made `-o`
	// core fails here by exactly one column.
	//
	// The operand is an ordinary word, read the same way `-o`'s is: `[[ -v
	// 'x' ]]` and `n=x; [[ -v $n ]]` both ask about `x`, and a subscript
	// belongs to it — `[[ -v 'a[2]' ]]`. What the name then *means* is the
	// interpreter's, and the three shells that have the operator disagree
	// about two classes of name; see interp.Semantics.
	ParameterIsSetTest bool

	// NameReferenceTest enables `[[ -R name ]]`, which asks whether a name is
	// a **reference** — a question about the binding rather than about what it
	// points at, so a reference answers true and its target answers false.
	//
	// A grammar flag for the same reason [ParameterIsSetTest] is one, and the
	// same column decides it. Measured 2026-09-22 with `v=1` and a reference
	// `r` aimed at it:
	//
	//	bash 5.3.20      `[[ -R r ]]` 0, `[[ -R v ]]` 1
	//	ksh93u+ 2012-08  `[[ -R r ]]` 0, `[[ -R v ]]` 1
	//	bash 3.2.57      `conditional binary operator expected`, then a
	//	                 syntax error naming the operand — it cannot read the
	//	                 line at all, which is what makes this grammar
	//	zsh 5.9.2        `unknown condition: -R`
	//	dash 0.5.12      no `[[ ]]` to put it in
	//
	// The builtin spelling is a separate question and a separate axis, because
	// it is an operand rather than grammar: see
	// interp.Semantics.TestHasTheNameReferenceOperator. The two must answer
	// together in any dialect that has both — the corpus case
	// `cond/a-name-reference-test-asks-about-the-binding` puts `[ -R r ]`
	// beside `[[ -R r ]]` so that they cannot come apart.
	NameReferenceTest bool

	// FunctionNameExpands reads a function definition's name as a *word*
	// rather than as literal text, so an expansion in one names the function
	// the expansion produces: `w=foo; _p_${w}() { … }` defines `_p_foo`.
	//
	// One dialect's, and it is the only one — dash, bash 5.3, bash-as-`sh`,
	// bash 3.2 and ksh93 all refuse both spellings, and their wordings say
	// they are refusing a *name*: “ `_p_${w}': not a valid identifier “ and
	// `_p_${w}: invalid function name`. So this is additive grammar rather
	// than an axis.
	//
	// The flag also decides what happens without it. The keyword form parsed
	// either way and took the token's literal text, which for `_p_${w}` is
	// `_p_w` — a different function, defined at status 0, with the one the
	// script asked for missing. Off, a name holding an expansion is refused
	// rather than flattened — while parsing, or where the definition runs;
	// see [Dialect.FunctionNameCheckedWhenTheDefinitionRuns], which is the
	// stage those two shells answer at.
	FunctionNameExpands bool

	// FunctionNameCheckedWhenTheDefinitionRuns makes a `function` keyword
	// whose name is not a name **parse**, with the word carried on the
	// declaration as source text and the complaint raised when the definition
	// is reached.
	//
	// The same stage question [Dialect.ForNameCheckedWhenTheLoopRuns] asks of
	// a loop variable, and the same two shells answer it that way. Measured
	// 2026-09-10 through `-c`, over `w=foo; function _p_${w} { echo HI; };
	// echo st=$?; echo after`:
	//
	//	bash 5.3.15  “ `_p_${w}': not a valid identifier “, then
	//	             `st=1` and `after` — the script carries on
	//	bash 3.2.57  the same two lines
	//	bash-as-sh   the same sentence and nothing after: fatal at 2
	//	ksh93u+      `_p_${w}: invalid function name`, fatal at 1
	//	zsh 5.9.2    defines `_p_foo` — FunctionNameExpands, above
	//	dash         no keyword at all, and the brace group's `}` is
	//	             where it stops
	//
	// **Both of the four name the offending text, and they name it as it was
	// written.** `_p_${w}` and `_p_$@`, not what the word would come to and
	// not its literal spelling — which is why the word is kept on
	// [FuncDecl.RefusedName] as source text rather than as a name or a
	// [Word]. A refusal that named neither left a script with several such
	// definitions no way to tell which one was disliked, which is half of
	// what #1296 is about.
	//
	// The other half is the stage, and it matters more: ours refused this
	// while *parsing*, so the script stopped where bash reports and carries
	// on. dash is a syntax error there, so the split is real rather than a
	// simplification — and it is the reason this is a grammar flag and the
	// three answers behind it are not. What happens when the definition is
	// reached is interp.Semantics.FunctionNameWhenTheDefinitionRuns.
	//
	// Only the *keyword* form is carried this far. The `name()` spelling is
	// its own question with its own panel — see
	// [Dialect.FunctionNameIsAnyWord] — and the two shells here read a
	// quoted word there where ksh93 refuses an expansion outright:
	// `_p_${w}() { … }` is “syntax error … `}' unexpected“ in ksh93 and
	// the run-time complaint in bash, which is a different split again.
	FunctionNameCheckedWhenTheDefinitionRuns bool

	// ReservedWordStandsBehindAnAssignmentPrefix keeps a written-out reserved
	// word's reading where an assignment prefix stands in front of it, so the
	// complaint lands on the word itself rather than on whatever token closes
	// the construct it opened.
	//
	// One column, and it is every word the grammar reserves rather than a
	// handful. Measured 2026-09-18, script files under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C` with a scratch HOME and no startup files:
	//
	//	written                     zsh 5.9.2   bash 5.3, bash 3.2, ksh93,
	//	                                        dash and BusyBox 1.37.0 ash
	//	v=x { :; }                       names `{`     names `}`
	//	v=x while :; do :; done          names `while` names `do`
	//	v=x if :; then :; fi             names `if`    names `then`
	//	v=x for i in a; do :; done       names `for`   names `do`
	//	v=x case a in a) :;; esac        names `case`  names `)`
	//	v=x function f { :; }            names `function`  names `}`
	//	v=x select i in a; do :; done    names `select`    —
	//	v=x until false; do :; done      names `until` names `do`
	//	v=x repeat 2 do :; done          names `repeat`    —
	//	v=x foreach i (a); :; end        names `foreach`   —
	//	v=x [[ -n a ]]                   names `[[`    a command that is not found
	//	v=x time :                       names `time`  runs /usr/bin/time
	//	v=x !                            names `!`     —
	//	v=x then / do / done / fi / esac / else / elif   names each one
	//	v=x end                          names `end`   —
	//	v=x coproc cat                   names `coproc`    —
	//	v=x in                           command not found: in
	//	v=x ( : )                        names `(` everywhere
	//
	// The last two rows are the controls. `in` is special inside `for` and
	// `case` and an ordinary command name where a command begins, so it is
	// not in the set even in that column; `(` is an operator rather than a
	// word, so no reading was ever taken away from it and every column
	// agrees.
	//
	// Three of the rows are worse than a wording without this: `v=x time :`
	// ran `/usr/bin/time`, `v=x !` was a command that could not be found, and
	// `v=x [[ -n a ]]` reached the pattern matcher (#3560).
	//
	// Not the same question as
	// [Dialect.AliasedReservedWordStandsBehindAnAssignmentPrefix], which is
	// ksh93's and is asked where the word arrived **from an alias**: there a
	// written-out `v=x { :; }` names `}` and only the aliased spelling keeps
	// the reading. This column needs no alias for any row above.
	ReservedWordStandsBehindAnAssignmentPrefix bool

	// RedirectionBeforeACompound says whether a redirection may stand in
	// front of a compound command, and before which of them — see
	// [RedirectionBeforeACompoundPolicy] for the three values and the panel.
	RedirectionBeforeACompound RedirectionBeforeACompoundPolicy

	// ConditionCloserIsAWordWhereATermBegins reads a `]]` standing where a
	// condition **term** belongs as an ordinary word, so the closer is only a
	// closer once the condition has something to close over.
	//
	// The five places a term may begin are the `[[` itself, a `!`, a `&&`, a
	// `||` and a group's `(`. Measured 2026-09-18, `env -i PATH=/usr/bin:/bin
	// LC_ALL=C`, `-c` and script files:
	//
	//	                   [[ ]] ]]   [[ ]] == x ]]   [[ ]] && x ]]
	//	ksh93u+ 2012-08-01  0          1               0
	//	bash 5.3.20         refuses    refuses         refuses
	//	zsh 5.9.2           refuses    refuses         refuses
	//
	// So one column takes the text as the two-character word `]]` and runs
	// the condition over it — true for being non-empty, compared with `x` as
	// a string, joined by the connective — and the other two refuse the token
	// wherever it stands. That is the whole of the flag, and the reason it is
	// the grammar's rather than a wording: what the parser **consumes** moves.
	//
	// It reaches a term's position only. An operand is a separate question
	// and every column answers it the same way: `[[ -n ]]` and `[[ x == ]]`
	// are refusals in that column too — “]]' unexpected“ — so the closer is
	// still a closer behind an operator.
	//
	// zsh is **not** this, which is worth writing down because it looks like
	// it from one probe. That shell refuses the token as well; what it does
	// differently is report the refusal at the token *after* the closer, and
	// [Dialect.ConditionTermMissingBlamesTheTokenAfterTheCloser] is that.
	//
	// Where a newline follows the word, the newline itself is what is named,
	// and that is not this flag's — it is
	// [Dialect.ConditionNewlineMayFollowATermsFirstWord], which every column
	// but one answers the same way. `[[ ]]` reaches it here because the
	// closer is *read as the word* and a word standing alone is where that
	// question is asked (#2964, #3627).
	ConditionCloserIsAWordWhereATermBegins bool

	// ConditionNewlineMayFollowATermsFirstWord lets a condition term's first
	// word be the last thing on its line, with whatever decides the term —
	// a binary operator, the `]]`, a connective, a group's `)` — written on
	// the next.
	//
	// Additive, and one column adds it. Measured 2026-09-18, script files
	// under `env -i PATH=/usr/bin:/bin LC_ALL=C`, stdin `/dev/null`:
	//
	//	written                zsh 5.9.2  bash 5.3.20 and 3.2      ksh93u+
	//	[[ y \n ]]              runs       `newline', cond. binary  `newline'
	//	[[ y \n && -n z ]]      runs       the same                 the same
	//	[[ y \n || -n z ]]      runs       the same                 the same
	//	[[ ( y \n ) ]]          runs       the same                 the same
	//	[[ y \n == z ]]         runs       the same                 the same
	//	[[ y \n echo after      refuses    the same                 the same
	//	[[ -n x \n ]]           runs       runs                     runs
	//	[[ y == z \n ]]         runs       runs                     runs
	//	[[ ( -n x ) \n ]]       runs       runs                     runs
	//
	// So the question is asked at exactly one position — a term whose first
	// word has been read and whose **shape is not yet decided**, since a
	// binary operator may still follow it — and nowhere else. The last three
	// rows are the control: a term that is *finished* takes a newline in
	// every column, which is what says this is not a rule about newlines
	// inside conditions.
	//
	// Off is the core's answer because two of the three columns refuse, and
	// because what moves is what the parser **accepts**: on, `[[ y` and a
	// `]]` on the next line is a condition; off, it is a refusal naming the
	// newline. Reading it as core-accepts made this shell run a line bash
	// and ksh93 both reject.
	//
	// The newline is named where the parser stands **after** the blank lines
	// rather than at the first of them: three of them before an `echo` put
	// ksh93's complaint on the `echo`'s line, with `newline` still the word
	// quoted. That is the same skip
	// [Dialect.ConditionTermMissingBlamesTheTokenAfterTheCloser] measured one
	// construct over.
	//
	// Not the same question as a newline behind a *binary operator*, which
	// every column refuses and which the core already refused — `[[ 1 ==`
	// and a newline is `newline' unexpected` in ksh93 and `unexpected
	// argument `newline'` in bash. That one is an operator with no operand;
	// this one is a term with no verdict (#3627).
	ConditionNewlineMayFollowATermsFirstWord bool

	// ConditionNewlineMayPrecedeAnOperand passes over newlines standing where
	// a condition's operand belongs — behind a unary or a binary operator,
	// and between the operands of a term the shell resolves when it runs
	// (see ConditionIsResolvedWhenItRuns) — as it passes over blanks.
	//
	// Additive, and one column adds it. Measured 2026-10-03 on zsh 5.9.2
	// under `-c`:
	//
	//	[[ -n \n x ]] && echo y          y
	//	[[ a == \n a ]] && echo y        y
	//	[[ -n x \n -z "" ]]              unknown condition: -n, status 2
	//	[[ -foo a \n b ]]                unknown condition: -foo, status 2
	//	[[ a \n b ]]                     condition expected: a, status 1
	//
	// So the last row is the control: a newline behind a term's *first word*
	// is ConditionNewlineMayFollowATermsFirstWord's question, and it does not
	// make two words into one term. bash 5.3.20 and ksh93u+ refuse the first
	// four, each naming the newline.
	ConditionNewlineMayPrecedeAnOperand bool

	// ConditionTermMissingBlamesTheTokenAfterTheCloser reports a condition
	// with no term in it at the token **behind** the `]]` rather than at the
	// `]]` itself — the closer is consumed and whatever stands after it is
	// what the complaint names, at that token's own line.
	//
	// Measured 2026-09-18 over `[[ ]]` in every route, with `env -i`:
	//
	//	route                  bash 5.3.20         zsh 5.9.2
	//	-c                     `]]', line 1        `]]', line 1
	//	a file with a last NL  `]]', line 1        a newline, line 2
	//	a file without one     `]]', line 1        `]]', line 1
	//	standard input         `]]', line 1        a newline
	//	`[[ ]]` then `echo`    `]]', line 1        `echo', line 2
	//	`[[ ]]; echo after`    `]]', line 1        `;', line 1
	//	`[[ ]] echo after`     `]]', line 1        `echo', line 1
	//	`[[ ]] ]]`             `]]', line 1        `]]', line 1
	//	`[[ ]] == x ]]`        `]]', line 1        `==', line 1
	//
	// One column refuses the `]]` **as a token**, at the same place on every
	// route; the other consumes it and blames what follows, which is a
	// newline where there is one, the next word where there is one, and the
	// `]]` itself where the input simply ends. Blank lines between are
	// skipped: three of them before an `echo` put the complaint on the
	// `echo`'s line.
	//
	// One shape it does not cover, and it is measured rather than forgotten:
	// `[[ ]] && x ]]` is `condition expected: x` in that column — a run-time
	// complaint about a word, not a parse failure at a token — so the `&&`
	// there is read as the list operator it also is (#2964).
	ConditionTermMissingBlamesTheTokenAfterTheCloser bool

	// FunctionNamesRefused are the words this dialect will not let a function
	// definition bind, whatever else is true of them. Every one of them is a
	// perfectly good name, so nothing about the *spelling* is what refuses
	// it — the shell keeps the word for itself.
	//
	// Two columns have such a set and both draw it around the **special**
	// builtins rather than around the builtins: `set`, `eval`, `export`,
	// `readonly`, `shift`, `trap` and `unset` are refused in both, while
	// `true`, `read` and `cd` — regular builtins — are accepted by every
	// column including these two. The two sets are close and not equal, and
	// each difference is a fact about which builtins that shell makes
	// special, so the set is data here rather than one list with exceptions.
	// Measured 2026-09-15 on the `-c` route, a script file and standard
	// input, by defining a function of each name:
	//
	//	bash 5.3.15  defines every one of them, status 0
	//	bash 3.2.57  the same
	//	zsh 5.9.2    the same
	//	BusyBox ash  the same, in the digest-pinned alpine image
	//	dash 0.5.12  refuses 16 — the 15 POSIX marks special, and `local`
	//	ksh93u+      refuses 21 — those 15 less `times`, which is a preset
	//	             alias there rather than a builtin, plus `alias`, `enum`,
	//	             `hash`, `login`, `newgrp`, `typeset` and `unalias`
	//
	// `.` and `:` are in both of those counts and in neither set, because
	// both dialects already refuse them for their spelling — dash by the
	// name check its parentheses commit to, ksh93 by
	// interp.Semantics.PunctuatedFunctionNameIsRefused, which has a wording
	// of its own for a dot. A set that repeated them would take that
	// wording away.
	//
	// The stage splits the two, and it is the split
	// [Dialect.FunctionNameCheckedWhenTheDefinitionRuns] already records for
	// a name that is not a name: dash is a **syntax error while reading**, so
	// `printf a; export() { :; }; printf b` prints neither word and a
	// definition in a branch nothing takes is refused all the same; ksh93
	// parses the definition and complains where it **runs**, so the `printf`
	// in front of it runs, a definition inside a false branch is never
	// reached, and one inside a subshell ends the subshell alone. Which is
	// why this field carries no stage of its own.
	//
	// Nil for the five columns that refuse nothing — the three bash columns,
	// zsh, and BusyBox ash, where `printf a; export() { :; }; printf b`
	// prints `ab` at 0 (measured 2026-09-21) — which is also what a
	// hand-built dialect has.
	FunctionNamesRefused map[string]bool

	// FunctionNameIsSourceText makes a definition's name the word as it was
	// **written** — quotes, backslashes and all — rather than the text the
	// word comes to. A name nobody could object to is refused when it is
	// spelled with quotes around it, because the quotes are in the name.
	//
	// bash alone, in all three of its spellings. Measured 2026-09-12 over
	// `sh -c "function 'f' { echo p; }; f; echo st=\$?"`, and again with the
	// `'f'() { … }` spelling, which answers the same:
	//
	//	bash 5.3.15  “ `'f”: not a valid identifier “, then
	//	             `f: command not found` and st 127
	//	bash 3.2.57  the same two lines
	//	bash-as-sh   the same diagnostic, fatal at 2
	//	ksh93u+      `p`, st 0 — the quotes come off and `f` is defined
	//	zsh 5.9.2    the same
	//	dash         no keyword at all
	//
	// The name is `f`. Nothing about it is unusual except the quotes, which
	// is what makes this the *control* for the two flags either side of it:
	// [Dialect.FunctionKeywordNameIsAnyWord] and
	// [Dialect.FunctionNameIsAnyWord] are about a **wider set of names**, and
	// no set of names could exclude `f`. Every other spelling of the same
	// fact answers the same way there — `function "f"`, `function f""` and
	// `function \f` are all `not a valid identifier` naming the source text.
	//
	// It pairs with [Dialect.FunctionNameCheckedWhenTheDefinitionRuns] and is
	// not the same question: that one says *when* a bad name is refused, and
	// this one says what the name is. Without it the quotes came off here in
	// every dialect and `f` was defined — right for two columns and wrong for
	// three (#1566). With it the refused word reaches
	// [FuncDecl.RefusedName] as it always did, which is why the wording is
	// already what those three print.
	FunctionNameIsSourceText bool

	// FunctionDefinitionIsSourceText keeps a definition's own source text on
	// the declaration — see [FuncDecl.SourceText] — for the dialect that
	// writes a function back as it was **written** rather than as a tree.
	//
	// ksh93 alone. `typeset -f` there does not pretty-print: it reproduces
	// the characters the definition was spelled with, comments and odd
	// spacing and all, and ends with the character that ended the statement.
	// Measured 2026-09-13 on ksh93u+ 2012-08-01 through `cat -A`:
	//
	//	ksh -c 'f(){    echo     a   ;   }; typeset -f f'
	//	                     f(){    echo     a   ;   };
	//	ksh -c 'f() { :; }   ; typeset -f f'    f() { :; }   ;
	//	ksh -c 'eval "f() { :; }"; typeset -f f'    f() { :; }
	//	printf 'g() { :; }\ntypeset -f g' | file    g() { :; }\n
	//
	// So the span is the name through the terminator inclusive, and a
	// definition with no terminator after it — the last thing an `eval`
	// string holds — ends at its body. The other five dialects print from
	// the tree and never read this, which is why keeping the text is a
	// dialect's answer and not something every parse pays for.
	//
	// The parser is where it has to happen: the interpreter is handed a tree
	// and the source is gone by then. [Diagnostics.FunctionListingIsSourceText]
	// is the other half — this one keeps the text and that one writes it —
	// and both are needed, because a tree parsed by one dialect may be run by
	// another's vector and neither half may assume the other (#2610).
	FunctionDefinitionIsSourceText bool

	// PatternAlternation enables a bare `(a|b)` inside a pattern word, which
	// zsh has and the others do not: `a(b|c)` matches `ab` there. It is why
	// `@(abc|xyz)` is a literal `@` followed by a group in zsh rather than an
	// extended pattern — the same text, read by a different rule.
	PatternAlternation bool

	// BarePatternGroupInsideAWord opens a bare `(a|b)` group where a word has
	// already begun and refuses one where a word begins — so
	// `[[ ab == a(b|c) ]]` reads a group and `[[ a == (a|b) ]]` is a parse
	// error naming the parenthesis, in one dialect in one state.
	//
	// It is only asked where [Dialect.PatternAlternation] is off, which takes
	// a group wherever it stands; this is the narrowed reading of the same
	// construct and it is lexed by the same span scan, so exactly the same
	// text belongs to the word.
	//
	// **One shell reaches it, through an option pair**, which is why it is a
	// flag of its own rather than a consequence of the one above. Measured on
	// zsh 5.9.2 (aarch64-apple-darwin25.4.0) at `/opt/homebrew/bin/zsh`, run
	// `-f` over a script file under `set -n`, 2026-09-27, with the options
	// moved on the line before:
	//
	//	                          bare     shglob    shglob+kshglob
	//	[[ ab == a(b|c) ]]        parses   refused   parses
	//	[[ a == (a|b) ]]          parses   refused   refused
	//	let a=(5 + 3)/2           parses   refused   parses
	//	echo x=(echo hi)          parses   refused   parses
	//	print -r -- a(b ; print x one word  refused   one word, and the
	//	                                              `;` ends it
	//
	// The second row is what makes this about the **position**: the same two
	// characters are refused where a word begins and taken inside one, in the
	// same shell in the same state. The fifth is what says the *lexing* is
	// unchanged — the unterminated group runs past the blank and stops at the
	// `;`, exactly as it does with the option pair off, which is
	// [Dialect.UnterminatedPatternGroupIsAWord]'s rule and not a second one.
	//
	// What such a group then *means* to the matcher is a second question this
	// does not answer, and the measurement is worth recording beside it:
	// `[[ 'a(b' == a(b|c) ]]` matches in that state, so there the parentheses
	// are literal and the `|` outside them alternates the whole pattern. This
	// flag is the lexer's half of it.
	BarePatternGroupInsideAWord bool

	// UnterminatedPatternGroupIsAWord says that a pattern group the input
	// runs out of is the word's own text rather than unfinished input. The
	// `(` stays in the word, the parser is handed a word like any other, and
	// what refuses it — if anything does — is the *matcher*, where a pattern
	// that will not compile is refused.
	//
	// **The panel splits on it**, which is why it is a flag rather than a
	// consequence of [Dialect.PatternAlternation]. Measured 2026-09-26 from a
	// script file, each shell asked for the group spelling it has:
	//
	//	zsh 5.9.2      print -r -- a(b      bad pattern: a(b, 1
	//	                                    and `[a` standing at 0 with
	//	                                    `unsetopt badpattern`
	//	bash 5.3.20    echo a@(b (extglob)  unexpected EOF while looking
	//	                                    for matching `)', 2
	//	bash 3.2.57    echo a@(b (extglob)  the same, plus `unexpected end
	//	                                    of file`
	//	ksh93u+        echo a@(b            syntax error at line 2:
	//	                                    `(' unmatched, 3
	//	dash 0.5.12    echo a(b             Syntax error: "(" unexpected, 2
	//
	// So the two shells with *quantified* groups read the missing `)` as
	// input they have not been given yet, and the one with **bare** groups
	// reads the text as a word and refuses it later, by name. The difference
	// is visible one layer up: a refusal the lexer raises cannot be reached
	// by `setopt badpattern`, because the option is read where filenames are
	// generated and a word that never parsed never gets there (#4645).
	//
	// It says nothing about how *much* text the group takes. That is
	// scanGroupSpans's own rule and the two shells agree about it: `;`, `<`,
	// `>` and `&` end the word where they stand and whitespace, newlines and
	// `|` do not, so `print -r -- a(b; print AFTER` is `a(b` and one more
	// command in zsh 5.9.2 and here alike.
	UnterminatedPatternGroupIsAWord bool

	// ParenRunAfterPatternGroupIsText keeps a balanced `( … )` run standing
	// immediately behind a pattern group inside the word, where without it the
	// `(` ends the word and the parser reports it unexpected.
	//
	// One shell's, and the other four answer for three different reasons.
	// Measured 2026-09-28 with `echo A@(a)(b)B`, `-c` under `env -i`:
	//
	//	ksh93u+ 2012-08-01              A@(a)(b)B
	//	bash 5.3.20, extglob on and off syntax error near unexpected token `('
	//	dash 0.5.12                    Syntax error: "(" unexpected
	//	BusyBox ash 1.38.0             syntax error: unexpected "("
	//	zsh 5.9.2                      no matches found: A@(a)(b)B
	//
	// ash refuses the *quantified* spelling too — `echo A@(a)@(b)B` is the
	// same error there — so its row is about having no extended patterns at
	// all rather than about the run behind one.
	//
	// zsh's column is a **different construct wearing the same characters**:
	// a bare `(` opens a group anywhere in a word there, so that row is no
	// evidence for this one and the flag is off for it.
	//
	// # What the run has to be adjacent to
	//
	// The **construct**, not the byte before the `(`, and that was measured
	// rather than reasoned. Holding "the byte before the `(` is a `)`" fixed
	// and varying what produced it, in ksh93u+:
	//
	//	f @(a)(b)        1 | [@(a)(b)]   a pattern group
	//	f $(echo a)(b)   refused        a command substitution
	//	f "@(a)"(b)      refused        a quoted run
	//	f $((1))(b)      refused        an arithmetic expansion
	//	f \)(b)          refused        an escaped `)`
	//	f 'x)'(b)        refused        a single-quoted `)`
	//
	// Five spellings put a `)` immediately in front of the `(` and only the
	// group licenses it.
	//
	// # And it is adjacency, repeated
	//
	//	f @(a)(b)(c)     1 | [@(a)(b)(c)]  a run behind a run chains
	//	f @(a)((b))      1 | [@(a)((b))]   balanced rather than shallow
	//	f @(a)()         1 | [@(a)()]      and may be empty
	//	f @(a)x(b)       refused          one character between them ends it
	//	f @(a)(b)x(c)    refused          including after a run
	//	f @(a)@(b)       1 | [@(a)@(b)]    (control) a quantifier needs no run
	//
	// The two refusals bound it: the word ends at a `(` that is not adjacent
	// to a run this reading has already taken, so the rule cannot be "a word
	// holding a group never ends at a `(`". The control says the word was not
	// ending because a *group* ended.
	//
	// What the run then **means** is not this flag's question and is not one
	// answer: it is text in a condition and in a word, and a group in a
	// `case` arm and in a parameter-expansion operand. See
	// interp/patternOpts.noBareGroup and patternOpts.armParens, where that
	// grid is. This flag is the lexical half — whether the word ends — and
	// nothing more. See #4972.
	ParenRunAfterPatternGroupIsText bool

	// UnterminatedExpansionOperandIsAValue ends a `${` that ran out of input
	// **at the end of the input** and runs the command it was part of, where
	// without it the expansion is refused and nothing runs.
	//
	// Only once an operand has begun. Before that the expansion is refused in
	// every column, and that half is what fixes the noun: it is not "input
	// that ran out inside an expansion is a value", it is that an operand
	// consumes to the end of the input and so no character can be unexpected.
	// See Lexer.braceOperandHasBegun.
	//
	// Measured 2026-09-28, `-c` under `env -i PATH=/usr/bin:/bin` with a
	// scratch `HOME`, each probe followed by `; echo AFTER` so that whether
	// anything ran is half the answer. `s=abc`:
	//
	//	probe            ksh93u+           bash 5.3.20 / zsh 5.9.2 / dash
	//	echo "${s        refused           refused
	//	echo "${#s       refused           refused
	//	echo "${s#       abc, no AFTER     refused
	//	echo "${s#x      abc, no AFTER     refused
	//	echo "${s%x      abc, no AFTER     refused
	//	echo "${s%%x     abc, no AFTER     refused
	//	echo "${s/x      abc, no AFTER     refused
	//	echo "${s/x/y    abc, no AFTER     refused
	//	echo "${s:-x     abc, no AFTER     refused
	//	echo "${s:=x     abc, no AFTER     refused
	//	echo ${s#{}      abc, no AFTER     refused by zsh, run by bash and dash
	//
	// The first two rows are the control that makes the rest readable: the
	// same run-out with no operator is refused there too, so a row that runs
	// is an operand swallowing the rest of the line and not a shell that
	// declines to notice the end of its input. `echo "abc` is `abc; echo
	// AFTER` in every column, which is the other control — an unterminated
	// **quote** was already a word everywhere and is not what these rows are
	// about.
	//
	// **Quoting is not the noun either**, and `echo ${s#{}` is the row that
	// says so: unquoted, and the shell still runs it. Nor is the *brace* —
	// six of the rows above hold no `{` at all, and the flag is consulted for
	// every operator rather than for the one #4936 gave a bare brace a way to
	// open.
	//
	// What reaches the command is the whole remainder as the operand, so
	// `a=x; echo "[${a/x/{y}]"` writes `[{y}]; echo AFTER` as text there: the
	// replacement is everything after the second `/`. That is the sharpest
	// row and the one that says the input was consumed rather than discarded.
	//
	// The **interactive** route is not this flag's question and is left alone:
	// `Lexer.ranOut` still records that the input ended inside a `${`, so a
	// prompt asks for more rather than running a line the person is still
	// typing. Every row above is `-c`. See #4973.
	//
	// **And it is a route rule**, which is why the field is a set of routes
	// rather than a boolean. Measured 2026-10-04 on ksh93u+ 2012-08-01 with
	// `echo ${x:-a` followed by `echo after`: `-c` and `eval` run it (`a echo
	// after`), while the same text as a script file, or on standard input, is
	// “syntax error at line 1: `{' unmatched“ at status 3 with nothing run —
	// with or without a trailing newline, and with the second command on the
	// same line or the next. It is the route split [Dialect.CloseQuotesAtEOF]
	// already draws for an unterminated quote, by the same shell.
	//
	// **Only the parameter form.** A `${ cmd;}` body that runs out is refused
	// on every route there, `-c` included: `echo ${ echo hi}` and `echo ${
	// echo {a,b};}` are both “ `{' unmatched “ (see [BraceProgramBodyEnd]).
	// The body is a program, not an operand, so nothing in it consumes the
	// rest of the input.
	UnterminatedExpansionOperandIsAValue ProgramRoutes

	// PatternTopLevelAlternation reads a `|` standing outside every group and
	// bracket as an alternation of the whole pattern — `a|b` matching `a` or
	// `b` rather than the three characters.
	//
	// Read only from a bar a value supplied — `L='a|b'; [[ a = ${~L} ]]`, the
	// same value under `setopt globsubst`, or a `case` arm expanded from one.
	// Measured on zsh 5.9.2: all three match, and `[[ a = a|b ]]` is
	// `parse error near '|'` there and here alike.
	//
	// That last row used to be the whole argument — "the written spelling is
	// a parse error, so only a value can put one here" — and it is false
	// inside a `${…}`, where the braces keep the bar out of the command
	// grammar and it reaches the matcher as pattern text. zsh reads a written
	// bar there as an ordinary character: `v=abc; ${v#a|ab}` is `abc` while
	// `${v#${~L}}` with the same three characters in a value is `bc`. So the
	// provenance is arranged rather than implied, by interp's markWrittenBars
	// (#2168), and this flag still means what it says.
	//
	// Separate from PatternAlternation because the two are answered
	// independently: a dialect with bare groups need not read a bar outside
	// one, and #1331 fixed the group while leaving this — the two were
	// measured together and only one of them was about groups (#1497).
	//
	// It is a three-way rather than a bool because **two shells have the
	// reading and they do not have the same one** (#2528). See
	// [TopLevelAlternation] for the two, and for the contexts each reaches.
	PatternTopLevelAlternation TopLevelAlternation

	// BackgroundAndDisown reads `&!` and `&|` as terminators that start a
	// statement in the background and then let go of the job: nothing lists
	// it and nothing waits for it by number. zsh's, and the two spellings
	// are one operator — every probe below answers alike for both.
	//
	// Measured 2026-09-06 with `-n` over a *script file*, which is the only
	// instrument that answers this: a `-c` string reads `&!` differently,
	// and "did it parse" is not the question anyway. The panel does not
	// split the way a first look suggests:
	//
	//	`echo hi &!`   zsh disowns. bash 5.3 and ksh93 *parse* it — as `&`
	//	               followed by the `!` that negates a pipeline — and
	//	               leave the job in the table, which `jobs` then lists.
	//	               bash 3.2, bash-as-sh and dash refuse it outright.
	//	`echo hi &|`   zsh disowns. ksh93 parses it and means something
	//	               else — `echo hi &| echo done` prints only `done`
	//	               there. bash 5.3, bash 3.2, bash-as-sh and dash all
	//	               refuse it.
	//
	// So what is zsh's alone is the *disowning*, and the flag carries the
	// grammar half. What the other shells do with the same text is their
	// own grammar answering, and this flag does not reach them.
	//
	// The job is otherwise an ordinary background job, measured: `$!` is
	// still set to its process and `wait` still reports 0. Only the *table*
	// differs, which is why a later `&` job is `[1]` and not `[2]`.
	//
	// There is no corpus row for any of this, and the reason is worth
	// stating because it is not "nobody wrote one". Every snippet that puts
	// a `&!` where it can *run* reaches a second gap on the way: `!` in the
	// other shells is the pipeline negation, and what they do with a bare
	// one differs from what this shell does in both directions. `echo a &!`
	// alone is `a` in bash 5.3 and ksh93 and a syntax error in bash 3.2,
	// bash-as-sh and dash; ours accepts it everywhere. `sleep 0.4 &!` with a
	// line after it is accepted by bash 5.3 and ksh93 — the `!` negating the
	// *next* line's pipeline — and ours refuses it. So a row recording the
	// disowning would record four unrelated divergences beside it, and the
	// bare `!` is its own issue. The behavior is asserted in
	// syntax/disown_test.go and interp/disown_test.go instead.
	//
	// It does not change what happens at exit, and the first measurement
	// that said it did was an artifact: `zsh m.sh | tr …` holds the script
	// open for the whole `sleep 0.5` because the *pipe* is waiting for the
	// background job that inherited its standard output, not because the
	// shell is. Timed without a pipe, `sleep 0.5 &` and `sleep 0.5 &!` both
	// return in six milliseconds.
	BackgroundAndDisown bool

	// NumericRangePattern reads `<n-m>` in a word as a pattern matching a
	// run of digits whose *value* falls in the range, rather than as a
	// redirection: `<->` is any number, `<1-9>` a bounded one, and `<2->`
	// and `<-9>` are bounded on one side. zsh alone has it, and it is what
	// makes `[[ $1 = <-> ]]` a test rather than a parse error there.
	//
	// It reaches the lexer because `<` is a redirection operator everywhere
	// else, and the shape is the whole of the disambiguation — measured
	// 2026-09-05 on zsh 5.9.2, `<` then digits then `-` then digits then
	// `>`, and nothing else. `echo <1` reads the file `1`, `echo <a-b>` is a
	// redirection followed by a parse error at the `>`, and `<1-2-3>` and
	// `<-->` are parse errors too. Only the exact shape is a pattern.
	//
	// The digits in front of a redirection stop being a descriptor when the
	// operator turns out to be one of these: `echo 2<->` is one word there
	// and not a redirection of descriptor 2, and `{a}<->` is a word rather
	// than a descriptor the shell would pick.
	//
	// The matcher's half of the flag is read from here as well, the way
	// [Dialect.PatternAlternation] is: what a `<n-m>` matches is not a
	// grammar question, but which dialects have one to match is.
	NumericRangePattern bool

	// GlobQualifiers reads a parenthesized group where an *argument* may
	// stand as part of the word rather than as anything of the shell's:
	// `echo MY ( x )` is two words there, `MY` and `( x )`, and the second
	// is a pattern carrying a list of qualifiers. zsh alone has it, and in
	// the other four a `(` after a word is a syntax error.
	//
	// **Command position is the whole of the disambiguation**, measured
	// 2026-09-06 on zsh 5.9.2: `( x )` written where a command begins is a
	// subshell running `x`, and the same three characters after a word are
	// one argument. So the parser has to say which it is — the flag alone
	// cannot — exactly as it does for [Lexer.inPattern] and a condition.
	//
	// `setopt no_glob` is what proves the split is lexical rather than
	// interpretive: with globbing off, `echo MY ( x )` *prints* `MY ( x )`,
	// so the words are the same words and only what becomes of the group
	// has changed. The grammar half is therefore unconditional under the
	// flag, and the qualifier reading lives in the expansion, where whether
	// a word is a pattern at all is already decided.
	//
	// The group ends the word at a shell operator, which is measured rather
	// than assumed and is the reason it is not simply the balanced text:
	// `echo ( a <b )` is `parse error near `)'` there, with globbing on or
	// off, because the `<` ended the word and left the `)` with nowhere to
	// go. A `|` is the exception — `echo ( a|b )` is one word — because a
	// pattern group may hold an alternation.
	//
	// That is a rule about a group **starting a word** rather than about
	// this flag: a pattern operand's leading group ends at the same four
	// characters — `[[ $k == (a<b) ]]` is `parse error near `<'` there and
	// needs only [Dialect.PatternAlternation] — where a *regular
	// expression's* operand keeps them, in all four shells that have `=~`.
	// See Lexer.scanGroupSpans, which holds both measurements (#1175).
	//
	// The matcher's half is read from here too, the way
	// [Dialect.PatternAlternation] and [Dialect.NumericRangePattern] are:
	// which dialects read a trailing group as qualifiers is a grammar
	// question even though applying them is not.
	GlobQualifiers bool

	// DeclarationUtilities are the commands that may be given an array
	// assignment as an operand: `local a=(x y)`, `typeset -a b=()`.
	//
	// A grammar question rather than a runtime one, and name-sensitive:
	// `echo a=(x)` is a syntax error in bash and ksh93, so the parser has to
	// know which names take the form. It differs by dialect because the
	// utilities do — ksh93 has no `local` and no `declare`, and there
	// `local a=(x)` is the same syntax error `echo a=(x)` is.
	//
	// Empty means none, which is dash: it has no array literal at all.
	DeclarationUtilities map[string]bool

	// DeclarationReservedWords are the declaration commands that are
	// reserved words, whose assignments the grammar reads as assignments
	// wherever they stand among the operands. What it changes here is the
	// listing: one engine writes such a command back with a blank after it
	// when it holds one — see SimpleCmd.DeclaresByReservedWord.
	//
	// Measured 2026-10-02 on zsh 5.9.2 through `which`: `typeset a=1`,
	// `local d=2`, `export e=3`, `readonly j=5`, `integer k=6`, `float l=7`
	// and `declare m=8` come back with the blank, and so do `typeset b=1 c`
	// and `typeset c b=1` — once, at the end — while `typeset c d`,
	// `typeset "a=1"`, `typeset $x=1`, `builtin typeset a=1` and `private
	// q=(1)` come back without one: `private` takes an array operand
	// (DeclarationUtilities) and is not a reserved word (#5138).
	// ReservedWordsOff are grammar reserved words a script has switched off
	// — zsh's `disable -r` — which the parser then reads as ordinary words
	// wherever it would have read them as reserved. Nil reserves every word
	// the grammar has. A run-time change reaches the text read after it,
	// through a replaced dialect, as every grammar option does (#5267).
	ReservedWordsOff         map[string]bool
	DeclarationReservedWords map[string]bool

	// DeclarationArrayFromTheCommandWord is how the command word must be
	// written for a `name=( … )` operand behind it to be an array literal.
	//
	// Measured 2026-09-16 from script files, `env -i PATH=/usr/bin:/bin
	// LC_ALL=C`, `'typeset' a=(x y); echo "[${a[1]}]"` and the spellings
	// below, each in a script file of its own:
	//
	//	probe                   bash 5.3        zsh 5.9.2        ksh93u+
	//	typeset a=(x y)         [y]             [x]              [y]
	//	'typeset' a=(x y)       syntax error    a glob qualifier [y]
	//	\typeset a=(x y)        syntax error    a glob qualifier [y]
	//	type"set" a=(x y)       syntax error    a glob qualifier [y]
	//	cmd=typeset; $cmd a=(…) syntax error    a glob qualifier syntax error
	//
	// "a glob qualifier" is `unknown file attribute:` at 1 with the next line
	// still running: that shell reads the parenthesis as a qualifier on the
	// word `a=`, which is what it does with any argument that is not a
	// declaration's operand. bash has no such reading, so the `(` is left
	// standing behind a word and the file ends there. dash and BusyBox ash
	// have no array literal at all and the question does not reach them.
	//
	// The array route used to key on the command word with its quotes
	// removed, so every dialect took the array form from every spelling —
	// which is ksh93's answer given to two columns that refuse it (#3351).
	DeclarationArrayFromTheCommandWord DeclarationArrayWord

	// ReservedPrecommands are words that may stand in front of a simple
	// command, are taken away before it runs, and change nothing the command
	// can see. zsh's `nocorrect` is the only one measured: it turns off
	// spelling correction, which a non-interactive shell never does anyway,
	// so what is left is a word to be consumed.
	//
	// A *grammar* question rather than a runtime one, which is measured
	// rather than assumed and is the whole reason this is not a builtin like
	// `noglob` beside it (see interp.PrecommandModifier):
	//
	//	nocorrect x=1 echo hi     prints `hi` — `x=1` is still an assignment
	//	                          prefix, so the word was gone before the
	//	                          command was read
	//	noglob x=1 echo a[b]c     `command not found: x=1` — that one is a
	//	                          builtin, so what follows it is a command
	//	                          word and no longer an assignment
	//	x=nocorrect; $x echo hi   `command not found: nocorrect` — an
	//	                          expansion cannot produce it
	//	x=noglob; $x echo a[b]c   prints `a[b]c` — an expansion can produce
	//	                          that one
	//	\nocorrect echo hi        `command not found` — quoting removes the
	//	                          reservation, as it does for `if`
	//
	// It is recognized where a command word may first stand, which is after
	// a redirection or an assignment prefix as well as at the very start:
	// `>/dev/null nocorrect echo hi` and `x=1 nocorrect echo hi` both run
	// the command. And the word after it stands in command position, so an
	// alias there expands — `alias e=echo; nocorrect e hi` prints `hi`.
	//
	// Empty means none, which is the other four shells: `nocorrect` is an
	// ordinary command name there and `command not found` is the right
	// answer.
	ReservedPrecommands map[string]bool

	// ArrayLiteral enables `a=(x y)`. Absent from dash, where the `(` is a
	// syntax error rather than a different construct — so unlike `&>`, this
	// one is safe to be wrong about loudly.
	ArrayLiteral bool

	// ArrayLiteralAfterABlank reads `name= (x y)` — blanks between the `=`
	// and the `(` — as the array literal `name=(x y)` rather than as an
	// empty assignment followed by a parenthesis.
	//
	// Measured 2026-10-03 on ksh93u+ 2012-08-01: `a= (echo x)` leaves `a`
	// holding `echo` and `x`, and so does any run of blanks, an append (`a+=
	// (x)`), a subscripted name, a declaration operand (`typeset a= (x)`) and
	// an assignment prefix (`a= (x) echo hi` runs the `echo`). A newline is
	// not a blank: `a=` then `(x)` on the next line is an empty assignment
	// and then a subshell running `x`. bash 5.3.20, zsh 5.9.2 and dash refuse
	// the blank form as a syntax error, which is the grammar without the
	// flag.
	//
	// Additive: the flag makes text that is otherwise an error mean the
	// literal, and changes nothing that parses without it.
	ArrayLiteralAfterABlank bool

	// ArrayLiteralRunningOutIsUnmatched reports an array literal whose `)`
	// never came as a construct that ran out — `unexpected EOF while looking
	// for matching `)'`, blamed at the parenthesis — rather than as a token
	// the grammar did not want at the end of the input.
	//
	// The two readings are both defensible and the shells split. Measured
	// 2026-09-22 over `a=(` through `-c`:
	//
	//	bash 5.3.20   line 1: unexpected EOF while looking for matching `)'
	//	ksh93u+       syntax error at line 1: `end of file' unexpected
	//	zsh 5.9.2     parse error near `a'
	//	dash, ash     no array literal at all — the `(` is refused outright
	//
	// So bash is the column that treats it as an unfinished *bracket* and
	// the rest treat it as a refused token, which is why this is a flag
	// rather than the parser's own answer. ksh93's row was already right
	// here and is what a change without this flag would have broken.
	//
	// Only at the end of the input: a literal stopped by some *other* token
	// — `a=(1 ;` — is that token's refusal in every column, bash included.
	ArrayLiteralRunningOutIsUnmatched bool

	// CompoundAssignmentWordRunsPastItsParenthesis keeps a compound
	// assignment's word going after its closing parenthesis, so `a=(1 2)x`
	// is **one word** — and so not an array assignment at all, since no
	// assignment's value can hold an unquoted parenthesis.
	//
	// Measured 2026-09-21, one `-c` per row under
	// `env -i PATH=/usr/bin:/bin LC_ALL=C`, with a shell function standing
	// in for the builtin so the words it is handed are visible:
	//
	//	declare() { printf "<%s>" "$@"; echo; }
	//	declare a=(1 2)x     bash 5.3.20, 3.2.57  <a=(1 2)x>
	//	declare a=(1 2)x y   bash 5.3.20          <a=(1 2)x><y>
	//	let a=(5+3)x y       bash 5.3.20          <a=(5+3)x><y>
	//	a=(1 2)x             bash 5.3.20          a scalar, `(1 2)x`
	//
	// where ksh93u+ 2012-08-01 and zsh 5.9.2 end the word at the `)`, store
	// the array and go looking for a command called `x`. So the panel splits
	// and this is a grammar flag rather than a rule.
	//
	// **What is folded is rebuilt rather than copied out of the source.**
	// bash normalizes: `a=(1    2)x`, `a=( 1 2 )x` and a newline between the
	// elements all reach the utility as `a=(1 2)x`. So the elements are
	// written out again with one blank between them — the shape a rejoining
	// utility's operand already takes — and each keeps its own spans, which
	// is what makes the result a word and not a string: `v=Q; declare
	// a=($v 2)x` is `<a=(Q 2)x>` and `declare a=(1 "2 3")x` is
	// `<a=(1 2 3)x>`.
	//
	// Only where the following token *touches* the parenthesis. `a=(1 2) x`
	// is the array and a separate operand in every column.
	CompoundAssignmentWordRunsPastItsParenthesis bool

	// NestedArrayLiteral reads a parenthesized element inside an array
	// literal as a literal of its own rather than as a syntax error —
	// `a=( (1 2) (3 4) )`, which is one shell's multi-dimensional array.
	//
	// Measured on ksh93u+ 2012-08-01, 2026-09-19, a script file under
	// `env -i PATH=/usr/bin:/bin LC_ALL=C` with stdin on /dev/null:
	//
	//	a=( (1 2) (3 4) ); echo "${a[1][0]} ${a[0][1]} ${#a[@]}"   3 2 2
	//	a=( ( (1 2) (3) ) (4) ); echo "${a[0][0][1]}"              2
	//	a=( (1 2)x )                                two elements, `(1 2)` and `x`
	//	a=( "(1 2)" )                               one element, the text
	//
	// bash and dash refuse the `(` outright, and zsh reads it as a glob
	// qualifier, so the construct belongs to the one column that has it.
	// The third row is what makes it a *token* boundary rather than a word
	// shape: the close paren ends the element wherever it falls.
	//
	// [Dialect.ArrayLiteral] gates the literal itself; this gates what may
	// stand inside one.
	NestedArrayLiteral bool

	// CompoundVariableDeclarators are the command words that, standing first
	// inside `name=( … )`, make the parentheses a **compound variable's body**
	// rather than a list of array elements — and their presence at all is what
	// says the dialect has compound variables.
	//
	// Two constructs share one spelling, and what tells them apart is the
	// first word as it was *written*. ksh93u+ 2012-08-01, measured 2026-09-13
	// with `env -i PATH=/usr/bin:/bin` and a scratch HOME:
	//
	//	written                  typeset -p says
	//	c=(x y)                  typeset -a c=(x y)
	//	c=(a=1 b=2)              typeset -C c=(a=1;b=2)
	//	c=()                     typeset -C c=()            and so is an empty one
	//	c=(x b=2)                typeset -a c=(x b\=2)      the first word decides
	//	c=("a=1")                typeset -a c=(a\=1)        quoted is not an assignment
	//	w=a=1; c=($w)            typeset -a c=(a\=1)        nor is an expansion
	//	c=(typeset -i n=5)       typeset -C c=(typeset -i n=5)
	//	c=(integer n=1)          typeset -C c=(typeset -l -i n=1)
	//	c=(float n=1)            typeset -C c=(typeset -l -E n=1)
	//	c=(readonly x=1)         typeset -C c=(typeset -r x=1)
	//	c=(export x=1)           typeset -C c=(typeset -x x=1)
	//	c=(declare x=1)          typeset -a c=(declare x\=1)   not a word of this shell
	//	c=(local x=1)            typeset -a c=(local x\=1)     nor is this
	//	c=(set x=1)              typeset -a c=(set x\=1)       nor an ordinary builtin
	//
	// So the set is closed and small, and it is a *grammar* question rather
	// than a runtime one for the reason DeclarationUtilities is: the two
	// readings take a `;` differently, which is a parse and not a value.
	// Under SemicolonInAnArrayLiteral's ksh93 value one `;` *ends* the element
	// list — `a=( x; y )` is “ `y' unexpected “ — where the compound body
	// takes as many as there are members:
	//
	//	a=( x; y )               `y' unexpected
	//	c=(a=1; b=2)             typeset -C c=(a=1;b=2)
	//	c=(a=1; echo mid; b=2)   `echo' unexpected      a body holds declarations
	//	c=(a=1 && b=2)           `&&' unexpected        and nothing else
	//	c=(a=1; ; b=2)           `;' unexpected         each one needs a member
	//	c=(; a=1)                `;' unexpected
	//
	// Empty means the dialect has no compound variable, which is every column
	// but ksh93: there `c=(a=1 b=2)` is an indexed array of the two strings
	// `a=1` and `b=2`, which is also what this shell stored for every dialect
	// before #2620.
	//
	// **The empty literal is a compound too**, and the set's own emptiness is
	// what says so — there is no word in `c=()` to decide with, so what
	// decides is whether the dialect has the construct at all. Measured, and
	// the knock-ons are the reason it is worth stating: `${#c[@]}` answers 1,
	// `${c[0]}` is the tree's rendering, `${!c[@]}` is `0`, a later
	// `c+=(x y)` starts at subscript 1, and `[[ -v c ]]` is true. A prior
	// `typeset -a c` does not change any of it. The spelling is the ordinary
	// way a script starts an array, so nothing outside ksh may move — which
	// is what the emptiness of the set guarantees rather than promises.
	//
	// A **declaration's own letters** settle it ahead of either rule, which
	// is why [compoundLiteralReading] exists: `-a` and `-A` take the compound
	// reading off the table and `-C` puts it on, so `typeset -a c=(a=1 b=2)`
	// is an array of two strings and `typeset -C c=(x y)` is
	// “ `x' unexpected “. That is a parse and not a store, because the
	// readings take a `;` differently: `typeset -a c=(a=1; b=2)` is
	// “ `b=2' unexpected “ there.
	//
	// One measured row is *not* modeled and is recorded rather than
	// reproduced: `typeset -A c=(a=1 b=2)` nests the compound under the key
	// `0` — `typeset -A c=([0]=(a=1;b=2))` — where the array reading this
	// takes reaches ksh93's own `cannot append index array to associative
	// array c`, which is what it answers for `typeset -A c=(x y)`. A value
	// under a key with a kind of its own is [SubscriptedArrayLiteralNests]'s
	// question rather than this one.
	CompoundVariableDeclarators map[string]bool

	// SemicolonInAnArrayLiteral is how far a `;` between the parentheses of
	// an array literal is taken. See [ArraySemicolon], where the rows are.
	//
	// It is a grammar flag rather than a semantics axis because the three
	// answers are three different *parses* of the same characters: one
	// refuses, one ends the element list, and one produces a different number
	// of elements.
	SemicolonInAnArrayLiteral ArraySemicolon

	// CloseBraceAlwaysReserved makes `}` a reserved word wherever a word may
	// stand, not only where a command may begin.
	//
	// It is what lets zsh write `{ echo hi }` with no terminator before the
	// brace: the `}` cannot be an argument, so it can only be closing the
	// group. The same rule is why `echo }` is a syntax error there and prints
	// a brace in the other six columns, which is the half that shows it is
	// one rule rather than a special case inside brace groups.
	//
	// **Reserved reaches into the word, which is the half that was missing.**
	// A `}` that ends a word is the reserved word and not the word's last
	// character, so no blank is needed in front of it either. Measured
	// 2026-09-12 on zsh 5.9.2, each line its own `zsh -c`:
	//
	//	echo A}          parse error near `}'
	//	echo A} B        parse error near `}'
	//	echo A}|cat      parse error near `}'
	//	echo "A"}        parse error near `}'
	//	{ echo A}        A            — the group closes with no `;` and no blank
	//	echo a}b         a}b          — not at the end of the word
	//	echo }a          }a           — nor is this
	//	echo A\}         A}           — quoting takes the reserved reading away
	//	echo A"}"        A}
	//
	// A `{` earlier in the same word matches it and takes the reading away
	// too, which is brace expansion's pairing seen from the lexer: `echo {a}`
	// and `echo a{b}` both print their braces, `echo {a}}` and `echo a{b}c}`
	// are both the parse error. Only bare text pairs — `echo ${x}}` is the
	// parse error, the expansion's braces counting for nothing.
	//
	// The one carve-out is an assignment's value, where the brace is text to
	// the end: `x=a}` and `x=}` both assign, where the same words as
	// arguments — `echo x=}` — are the parse error.
	CloseBraceAlwaysReserved bool

	// CloseBraceEndsAWord is the *lexical* half of the rule above on its own:
	// a `}` that ends a word ends it, whether or not the brace is then the
	// reserved word that closes a group.
	//
	// [Dialect.CloseBraceAlwaysReserved] implies this one and a dialect that
	// sets it need not set this as well. It exists because zsh has an option
	// that takes the *reserved* reading away and leaves the lexing standing —
	// `ignoreclosebraces`, which its own manual describes as letting `}` be an
	// argument while still closing a group "in appropriate contexts", meaning
	// where a command may begin. Measured 2026-09-28 on `/opt/homebrew/bin/zsh`
	// — `zsh 5.9.2 (aarch64-apple-darwin25.4.0)`, `go version -m` says *not a
	// Go executable* — each line `eval`ed so the option is in force when the
	// text is read:
	//
	//	                  neither          ignorebraces  ignoreclosebraces
	//	print a}          parse error      a}            a }
	//	print a }         parse error      a }           a }
	//	print }a          }a               }a            }a
	//	print a}b         a}b              a}b           a}b
	//	{print A}         A                {print: not   parse error near `}'
	//	                                   found
	//
	// **The first row is the whole of it.** Under `ignorebraces` the brace is
	// an ordinary character and `a}` is one word; under `ignoreclosebraces` it
	// still ends the word — two arguments reach `print` — and is then an
	// ordinary word rather than a group's closer. A wiring that reads the
	// lexing off the reserved flag gets `a }` wrong, and with it every alias
	// whose name a `}` follows: `alias CLOSE='};'` in `$({ OPEN print bye;
	// CLOSE})` is the reference's own suite asking exactly that (#5011).
	CloseBraceEndsAWord bool

	// OpenBraceNeedsNoBlank makes a bare `{` where a command may begin the
	// reserved word on its own, however the text runs on after it.
	//
	// zsh alone, and it is the shortest spelling of a one-line function:
	// `a(){print A}` defines `a` there and is `{print: command not found` in
	// dash and ksh93 and `syntax error near unexpected token `{print'` in
	// bash 5.3, bash 3.2 and bash-as-sh. Measured 2026-09-12 on zsh 5.9.2:
	//
	//	{print A}        A
	//	{echo A; echo B} A then B
	//	{a,b}            command not found: a,b   — no brace expansion: the
	//	                                            `{` was the reserved word
	//	echo {print A}   parse error near `}'     — argument position keeps
	//	                                            the brace in the word
	//	'{'print A}      parse error near `}'     — quoted, so no group opens
	//	\{print A}       the same
	//
	// Command position is the whole of it, so a redirection's target, a
	// `case` subject, a `for` list and a pattern all keep the brace: `echo hi
	// > {a}` writes a file called `{a}` there, and `for i in {a,b}` expands
	// to two words.
	//
	// The closing half is [Dialect.CloseBraceAlwaysReserved], which the same
	// shell has and which the one-line spelling needs as well — `{print A}`
	// is three tokens and this flag reads only the first of them.
	OpenBraceNeedsNoBlank bool

	// EmptyCompoundBody lets a compound command stand with nothing in it:
	// `{ }`, `( )`, `while cond; do done`, `if cond; then fi`, and the
	// condition as well as the body — `if ; then :; fi`.
	//
	// zsh alone, and it is every shape rather than a rule about braces:
	// measured with `-n`, so that a parse is told apart from a loop that
	// never ends, dash, bash 5.3, bash 3.2 and ksh93 refuse all of them and
	// zsh takes all of them.
	//
	// Off in the core, which is what the common denominator means here: the
	// core is the language every panel shell accepts, and `{ }` is outside
	// that set because four of the five refuse it. A dialect adds it back,
	// which is the additive direction a grammar flag is for.
	//
	// Three neighbors are deliberately not this flag. A `case` with no arms —
	// `case x in esac` — is a list of *arms* rather than a command list, and
	// the one shell that refuses it on one line runs the same `case` with a
	// newline in front of the `esac`, which is
	// [Dialect.CaseTerminatorIsAPatternAfterTheHeader] and not an emptiness rule at
	// all. A command substitution's body — `x=$( )` — is a whole program
	// rather than a compound command's body, and every shell in the panel
	// takes an empty one. And a body written as a single stepped-over `;` —
	// `{ ; }` — is [Dialect.SteppedOverSeparatorIsABody]: ksh93 takes that
	// one and refuses `{ }`, so the two are measurably apart.
	EmptyCompoundBody bool

	// ShortBodyEndsOnAJoiningOperator lets a short form's body be empty
	// because a joining operator stands where it would have been: `|`, `|&`,
	// `&&` and `||` cannot begin a command, so the loop they follow is their
	// left-hand side rather than a loop whose body is missing.
	//
	// Measured 2026-09-20 on zsh 5.9.2, each line its own script file under
	// `env -i -u FPATH PATH=/usr/bin:/bin LC_ALL=C` with no standard input:
	//
	//	for i in a b; | cat; print T     `T`       the loop ran, piped, nothing
	//	for i in a b; |& cat; print T    `T`
	//	for i in a b; && print x         `x`       so the loop succeeded
	//	select o in a b; | cat; print T  menu, `T`
	//	repeat 2; | cat; print T         `T`
	//	for i in a b; & print x          refused   `&` is not one of them
	//	for i in a b; ;; print x         refused   nor is a case terminator
	//	for i in a b; do :; done; | cat  refused   nor is the long form lenient
	//
	// The last row is the control that makes this the *short* body's
	// question: the same `|` after a `done` is a syntax error in zsh and here
	// alike, so what moved is a body that was never written rather than what
	// a bar may follow.
	//
	// Off in the core, for the reason [Dialect.EmptyCompoundBody] gives: the
	// rows above are zsh's and the core is the common denominator. Ungated it
	// also answered for a dialect that had only switched [Dialect.ShortForm]
	// on, and there it took `while | do :; done` — refused at the `|` before
	// — all the way to the stop word `do`, which is precisely what
	// TestAShortBodyRefusesTheTokenThatIsThere was written to catch.
	//
	// A `;` is deliberately not in the set, and that is measured rather than
	// tidied away: `for i in a b; ; print x` prints `x` twice in zsh, so the
	// second separator is stepped over and the `print` becomes the body — an
	// empty body there would run it once. That is
	// [Dialect.SeparatorWhereACommandBelongs]'s question and not this one.
	ShortBodyEndsOnAJoiningOperator bool

	// OpenEndedAndOr lets an and-or list end with its operator: the
	// right-hand side of a `&&` or a `||` may be absent where the list it is
	// in closes.
	//
	// zsh alone, measured 2026-09-07 over a script file with a scratch
	// `HOME`, `ZDOTDIR` and `HISTFILE`. `{ : || ⏎ }` runs there and is a
	// syntax error in bash 5.3, bash 3.2, bash-as-`sh`, dash and ksh93, each
	// of them naming the `}`. It reaches every closing context and is not a
	// rule about braces: `( : || )`, `if x; then : || fi`, `while false; do
	// : || done`, `until true; do : || done`, `case x in x) : || ;; esac`,
	// `case x in x) : || esac`, `if x; then : || else … fi`, `… elif …`, a
	// function body's `}` and a command substitution's `)` all take it.
	//
	// **The operator is dropped, not stood in for.** The status is the
	// left-hand side's: `false ||` answers 1 and `true &&` answers 0, so an
	// absent operand is neither an implicit `true` — which would make the
	// first 0 — nor an implicit `false`, which would make the second
	// non-zero. So nothing in the interpreter needs a value for this; the
	// parser returns the left-hand side and there is no operator left.
	//
	// **The pipeline does not take it, in any shell.** The leniency belongs
	// to the and-or list alone, which is the discriminating half of the
	// measurement: zsh refuses `true |` at the end of input, `{ true | ; }`,
	// `( true | )` and `true | )`, and names the token it found in each. A
	// flag that covered every joining operator would accept four lines zsh
	// rejects.
	//
	// **A terminator does not close the list for this purpose.** `true || &
	// b` and `true || ;;` with no `case` open are parse errors in zsh, so
	// `&` and a bare `case` terminator are not among the tokens that may
	// stand there. The set that does is the one every enclosing construct
	// stops on, which the parser's own atListEnd spells out.
	//
	// **The end of input is a separate question and not this flag.** zsh
	// takes `true &&` with nothing after it at all when it reads a script or
	// a `-c` string, and still draws a continuation prompt for the same text
	// typed at a terminal — measured through a pty, where zsh, bash and
	// ksh93 all prompt. So whether input that ran out ends the list depends
	// on the route it arrived by, the way [Dialect.ExpandAliases] does, and
	// it cannot be answered by a flag the parser reads on its own. Until it
	// is asked where it is answered, input ending on `&&` stays unfinished
	// in every dialect, which is right for the terminal in every column and
	// right for four of the five dialects everywhere else.
	OpenEndedAndOr bool

	// OpenEndedAndOrAtTheEndOfInput is the set of routes on which the end of
	// the input ends an and-or list whose operator was the last thing read,
	// the operator being dropped as OpenEndedAndOr drops one: `echo one &&`
	// prints `one` and answers 0.
	//
	// The question OpenEndedAndOr parks, answered where it can be: by route,
	// exactly as CloseQuotesAtEOF is, so that a prompt — which is no route —
	// still draws a continuation prompt for the same text. Measured
	// 2026-10-03 on zsh 5.9.2: `-c 'echo one &&'`, `-c 'echo one ||'`, `-c
	// 'false &&'` (status 1), a script file holding `echo one &&` with and
	// without a final newline, and `eval 'echo one &&'` all end the list;
	// `-c 'echo one |'` is still a parse error, because the pipeline never
	// takes it. bash 5.3.20, ksh93u+ and dash refuse every one of them.
	//
	// Standard input is left out of zsh's set although zsh takes it there
	// too, because that route is read a piece at a time and the end of a
	// piece is not the end of the input: answering it here would end
	// `a &&` at the line break before `b` arrived.
	OpenEndedAndOrAtTheEndOfInput ProgramRoutes

	// SeparatorWhereACommandBelongs lets a `;` stand where the grammar wants
	// a command, and steps over it.
	//
	// One rule rather than several, which is what the measurement says and is
	// the whole finding. Every shape two of the panel shells accept and the
	// other four refuse is the same thing seen in a different position:
	//
	//	; b                a list beginning with one
	//	a ; ; b            one between two statements
	//	a & ; b            the same after a `&` rather than a `;`
	//	a |& ; b           and after ksh93's coprocess terminator (#1141)
	//	a && ; b           where an and-or's right-hand side belongs
	//	a || ; b           the same for the other operator
	//	a | ; b            where a pipeline's right-hand command belongs
	//
	// **The `;` is skipped, not stood in for**, which `false` makes visible
	// and `true` hides: `false || ; echo two` prints `two` in both shells and
	// `true || ; echo two` prints nothing, so `echo two` really is the `||`'s
	// right-hand side and the `;` was absorbed the way the newline in
	// `a || ⏎ b` already is everywhere. Nothing runs for the separator itself
	// — `false ; ; echo $?` answers 1 in both, so it does not even set a
	// status.
	//
	// Measured 2026-09-07 over a script file with a scratch `HOME`, `ZDOTDIR`
	// and `HISTFILE`, `-n` and then a run. dash, bash 5.3, bash 3.2 and
	// bash-as-`sh` refuse every line above; ksh93 and zsh differ from each
	// other in exactly two ways, which is what the values below record.
	SeparatorWhereACommandBelongs SeparatorSkip

	// SubstitutionBodyRefusesASteppedOverSeparator takes that reading away
	// inside the body of a `$( … )` or a `${ …;}`, leaving only the position
	// where an and-or's right-hand side belongs.
	//
	// ksh93 alone, and it is the one place that shell's own answer does not
	// reach. Measured 2026-09-17 over a script file, `env -i
	// PATH=/usr/bin:/bin LC_ALL=C ksh s.sh` with stdin from /dev/null:
	//
	//	echo a; ;                 runs — the top level takes it
	//	v=`echo a; ;`             runs — and so does the older spelling
	//	v=`; echo a`              runs
	//	v=`false || ; echo b`     runs
	//	v=$(echo a; ;)            `;' unexpected
	//	v=$(; echo a)             `;' unexpected
	//	v=$(echo a; ; echo b)     `;' unexpected
	//	v=$(echo a & ; echo b)    `;' unexpected
	//	v=$(echo a | ; cat)       `;' unexpected
	//	v=$(if ; then echo a; fi) `;' unexpected
	//	v=$( ; )                  `;' unexpected
	//	v=${ echo a; ;}           `;' unexpected
	//	v=$(false || ; echo b)    runs, and the value is `b`
	//	v=$(echo a; )             runs — a separator *terminating* a
	//	                          statement is not this question
	//
	// So the two spellings of one construct part company, exactly as they do
	// for where a refusal is located and for whether a line continuation at
	// the front is removed — and the last two rows are what keep the rule
	// from being "the body refuses a `;`": an and-or still takes one, and a
	// terminator was never this.
	//
	// zsh takes every row, so the wider value is unaffected; bash 5.3, bash
	// 3.2, bash-as-`sh`, dash and BusyBox ash step over no separator anywhere
	// and so have nothing here to narrow.
	//
	// Read by the interpreter where the body is parsed rather than by the
	// parser, because only the caller knows it is reading a body and which
	// spelling opened it. See interp's bodyDialect.
	SubstitutionBodyRefusesASteppedOverSeparator bool

	// AQuotedOperandHidesASubstitutionFromItsLine says a `'` written inside
	// a double-quoted expansion's operand keeps a substitution opened behind
	// it out of the read of the line, for a dialect that reads a body with
	// its line at all.
	//
	// The operand is read twice — the scan for the closing brace honors the
	// quotes between the braces, and the operand is then read again with
	// those quotes standing for themselves — so the two readings disagree
	// about whether there is a substitution there. Which reading the *line*
	// is held to is the question, and the columns that read a body with its
	// line — dash, BusyBox ash, bash 5.3 and that build as `sh` — split on
	// it. Measured 2026-09-19, script files under
	// `env -i PATH=/usr/bin:/bin LC_ALL=C`, standard input on the null
	// device:
	//
	//	column               `echo before; echo "${v-'$(if)'}"; echo after`
	//	bash 5.3.20          `before`, then a run-time complaint, then `after`
	//	bash 5.3.20 as `sh`  refused with the line, nothing written
	//	dash 0.5.12          refused with the line, nothing written
	//	BusyBox ash 1.37.0   refused with the line, nothing written
	//
	// The control that makes it about the quote rather than about the
	// operand is the same line with the quotes moved off the substitution —
	// `"${v-'a'$(if)}"` — which every one of those columns refuses with the
	// line.
	// And the control that says the two readings really do differ at
	// expansion is `"[${v-'$(echo hi)'}]"`, which is `['hi']` in all seven
	// columns: the quotes are characters of the result everywhere, so it is
	// only the line's read that moves.
	//
	// **bash's own answer moves with POSIX mode**, which is why the two bash
	// rows differ, and that half is not reached from here: this is the
	// dialect's standing answer and nothing swaps it at `set -o posix`. The
	// row above is the gap that leaves.
	//
	// Consulted only where [Dialect.SubstitutionBodyRead] is not
	// [SubstitutionBodyReadWhenItRuns]; bash 3.2, ksh93 and zsh never read a
	// body with a line, so there is nothing for a quote to hide it from.
	AQuotedOperandHidesASubstitutionFromItsLine bool

	// SubstitutionBodyRead says when a command substitution's body is
	// parsed — with the line that holds it, or when the substitution runs.
	// See [SubstitutionBodyRead] for the panel and for the control that
	// makes it a question about parsing.
	//
	// What the parser does for it is collect: a body cannot be parsed here,
	// because the alias tables and the run-time state a body is read under
	// belong to the shell, so the spans this answer covers are gathered into
	// [File.Substitutions] and the reader takes them from there. Nothing is
	// gathered under the core answer, which is what keeps this off the
	// common path.
	SubstitutionBodyRead SubstitutionBodyRead

	// AbsentAndOrOperandIsAnEmptyCommand supplies a command that does nothing
	// and succeeds where an and-or's right-hand side is missing after a
	// separator was stepped over.
	//
	// ksh93's answer, and it is not [Dialect.OpenEndedAndOr]'s: the two
	// shells disagree about the *meaning* of the same text, which the
	// operator's short-circuit makes visible from one side only.
	//
	//	false || ;   ⏎ echo $?   →  0   ksh93     1   zsh
	//	false && ;   ⏎ echo $?   →  1   ksh93     1   zsh
	//
	// So it is an implicit success in ksh93 — `false || :` — and a dropped
	// operator in zsh, where the status is the left-hand side's. The `&&` row
	// agrees in both because the left-hand side failed and nothing on the
	// right was going to run either way, which is why the `||` row is the
	// only one that separates them.
	//
	// It is spelled here rather than in the interpreter's vector because the
	// difference is what *stands* in the tree and not what the tree means: an
	// empty command already runs and already answers 0, so this puts one
	// there and nothing downstream needs to know why.
	//
	// Only reachable where a separator was stepped over. ksh93 refuses
	// `false ||` and `{ false || ⏎ }` outright — an and-or may not simply end
	// with its operator there — so this is not that shell's spelling of
	// OpenEndedAndOr, and setting both would accept lines neither shell does.
	AbsentAndOrOperandIsAnEmptyCommand bool

	// SteppedOverSeparatorIsABody lets a `;` the dialect stepped over stand
	// for a compound command's whole body, so `{ ; }` is a brace group that
	// runs nothing.
	//
	// ksh93u+ alone, and it is measurably not [Dialect.EmptyCompoundBody] in
	// another spelling: that shell takes `{ ; }` and refuses `{ }`, where the
	// shell with the other flag takes both. Measured 2026-09-12 with `-n`,
	// `env -i PATH=/usr/bin:/bin` and a scratch HOME:
	//
	//	{ }                        `}' unexpected  — the control
	//	{ ; }                      runs
	//	{ ; ; }                    `;' unexpected  — only one is stepped over
	//	( ; )                      runs
	//	if :; then ; fi            runs
	//	if :; then :; else ; fi    runs
	//	if :; then :; elif :; then ; fi   runs
	//	while :; do ; done         runs
	//	until :; do ; done         runs
	//	for i in a; do ; done      runs
	//	select i in a; do ; done   runs
	//
	// The third row is why this reads the step-over rather than the token:
	// how many separators may stand there is
	// [Dialect.SeparatorWhereACommandBelongs]'s count, which for this shell
	// is one, and a body written as two `;` is refused by the same rule that
	// refuses `a ; ; ; b`.
	//
	// It runs nothing and sets no status of its own — `{ ; }; echo $?` is 0
	// and `false; { ; }; echo $?` is 0 as well — so the parser leaves the
	// body empty and nothing downstream needs a value for it.
	//
	// **One neighbor is deliberately not modeled.** After such a `;` that
	// shell takes a simple command and refuses a *compound* one: `{ ; :` runs
	// where `{ ; { :`, `{ ; ( :`, `{ ; if :; then :; fi`, `{ ; while …` and
	// `{ ; ! :` each name their own first keyword. That is a fact about what
	// may follow a stepped-over separator rather than about a body written as
	// one, it predates this flag — the same lines were accepted here before
	// it — and it looks like a parser's internal state rather than a grammar
	// anybody would write down. A reader comparing against ksh93 will find
	// it, so it is written down here rather than left to be rediscovered
	// (#2231).
	SteppedOverSeparatorIsABody bool

	// EmptyBodyBlame is which token a refusal names when a terminator stands
	// where a body or a condition must have something in it.
	//
	// Every shell in the panel refuses `if & then :; fi`, at the same line,
	// and only the quoted token differs — so this is a wording and not a
	// grammar. Measured 2026-09-14, `-n` over a script file under `env -i`
	// with a scratch `HOME` and `ZDOTDIR`:
	//
	//	                        dash   bash 5.3  ksh93   zsh
	//	if & then :; fi          `&`     `&`      `then`  `&`
	//	while & do :; done       `&`     `&`      `do`    `&`
	//	if & fi                  `&`     `&`      `fi`    `&`
	//	{ & }                    `&`     `&`      `}`     `&`
	//	( & )                    `&`     `&`      `)`     `&`
	//	if & ; then :; fi        `&`     `&`      `;`     `&`
	//	if | then :; fi          `|`     `|`      `|`     `then`
	//	while | do :; done       `|`     `|`      `|`     `do`
	//	if && then :; fi         `&&`    `&&`     `&&`    `then`
	//	if | ; then :; fi        `|`     `|`      `|`     `then`
	//	if | :; then :; fi       `|`     `|`      `|`     `then`
	//	{ | }                    `|`     `|`      `|`     `|`
	//	( | )                    `|`     `|`      `|`     `|`
	//	if ;; then :; fi         `;;`    `;;`     `;;`    `;;`
	//
	// Two shells step over the terminator and name what is behind it, over
	// **different sets of terminators** — ksh93 for `&` and zsh for the
	// pipeline and and-or operators — and they do not step the same way,
	// which is what makes this an enum rather than a bool. See the values.
	//
	// The terminators each one applies it to are [Dialect.EmptyBodyBlamed].
	// A `;` is in neither set and is [Dialect.SteppedOverSeparatorIsABody]'s
	// and #2023's question instead; `;;`, `;&` and `;;&` are in neither and
	// every shell names them where they stand.
	//
	// **This models the refusal and not the acceptance.** Both shells also
	// *take* such a terminator in a position where the list may be empty —
	// ksh93 runs `if :; then & :; fi` and `{ & :; }`, zsh runs nothing of
	// the kind — and taking it is a grammar question that changes what a
	// script does rather than what a refusal says. So where the token after
	// the terminator could begin a command this names the terminator, which
	// is what every column that refuses the line writes and what this shell
	// already wrote (#2235).
	EmptyBodyBlame EmptyBodyBlame

	// EmptyBodyBlamed is the set of terminators [Dialect.EmptyBodyBlame]
	// applies to. Empty means none, which is dash's and bash's answer and
	// the substrate's own.
	EmptyBodyBlamed map[Kind]bool

	// RegexTakesAlternation makes a bare `|` part of a `=~` operand rather
	// than the end of the word. bash and ksh93 say yes, so `[[ ab =~ a|b ]]`
	// matches there; zsh says no and reports a parse error. Parentheses are
	// taken by all three unless [Dialect.RegexParenthesisIsTheShellsOwn] says
	// otherwise — a dialect without `[[ ]]` never reaches either question.
	RegexTakesAlternation bool

	// RegexParenthesisIsTheShellsOwn takes the parentheses back off a `=~`
	// operand: an unquoted `(` there is read by the word rules that apply
	// everywhere else — [Dialect.PatternAlternation] and
	// [Dialect.BarePatternGroupInsideAWord] — rather than opening the regex's
	// own group.
	//
	// Off is every dialect's answer today and the reason the flag names the
	// state a dialect has to ask for: a regular expression's group is the
	// operand's in bash, ksh93 and zsh alike.
	//
	// **One shell reaches it, through an option**, and the rule it lands in
	// is the ordinary one rather than a refusal of its own. Measured on zsh
	// 5.9.2 (aarch64-apple-darwin25.4.0) at `/opt/homebrew/bin/zsh`, run `-f`
	// over a script file under `set -n`, 2026-09-27, with the options moved
	// on the line before:
	//
	//	                          bare     shglob    shglob+kshglob
	//	[[ a =~ (a) ]]            parses   refused   refused
	//	[[ abc =~ ^(a|x)bc$ ]]    parses   refused   parses
	//	[[ abc =~ ^a.c$ ]]        parses   parses    parses
	//	[[ abc =~ "^(a)" ]]       parses   parses    parses
	//	[[ abc =~ a\(b ]]         parses   parses    parses
	//
	// The first two rows are the same split a *pattern* operand has in the
	// same states — refused where a word begins, taken inside one — which is
	// what says this is the word rules reaching the operand rather than a
	// second rule about regular expressions. The last three are the controls:
	// an operand with no bare parenthesis in it, and the two quotings, are
	// untouched in every state.
	RegexParenthesisIsTheShellsOwn bool

	// RegexGroupEndsAtAShellOperator stops a `=~` operand's group at a `;`,
	// `<`, `>` or `&` standing inside it, so the group never closes and the
	// word ends where the operator does.
	//
	// Off is the answer of every shell but one, and the one is the dissenter
	// [Dialect.RegexTakesAlternation]'s comment already names: a regular
	// expression owns its operators in bash 5.3, bash 3.2, bash-as-`sh` and
	// ksh93, all four of which match `a<b` and `a;b` against `(a<b)` and
	// `(a;b)`, and zsh refuses the `<` while parsing.
	//
	// Measured on zsh 5.9.2 (aarch64-apple-darwin25.4.0) at
	// `/opt/homebrew/bin/zsh`, run `-f` over a script file under `set -n`,
	// 2026-09-27, which is what says the four characters are the set and the
	// others are not:
	//
	//	[[ "a<b" =~ (a<b) ]]   refused        [[ "a b" =~ (a b) ]]   parses
	//	[[ "a;b" =~ (a;b) ]]   refused        [[ "a|b" =~ (a|b) ]]   parses
	//	[[ "a>b" =~ (a>b) ]]   refused
	//	[[ "a&b" =~ (a&b) ]]   refused
	//
	// It is the same four [Dialect.UnterminatedPatternGroupIsAWord] records
	// for a *pattern* group in that shell, and the same two exceptions — a
	// blank and a `|` are text in both — which is what says the two groups
	// are one construct there rather than two that happen to agree.
	//
	// Outside a group the question does not arise and is measured to not:
	// `[[ "a<b" =~ a<b ]]` is refused wherever the `<` is not inside
	// parentheses, in that shell and in this one alike, because the word
	// simply ends at the operator.
	RegexGroupEndsAtAShellOperator bool

	// RegexKeepsAnUnbalancedCloser keeps a `)` that closes nothing inside a
	// `=~` operand rather than letting it end the word.
	//
	// ksh93 alone. Measured 2026-09-23 over script files with `echo after`
	// behind the condition, against bash 5.3.15 in the digest-pinned image the
	// suite is graded in, zsh 5.9.2 and ksh93u+ 2012-08-01:
	//
	//	written          bash              zsh               ksh93
	//	[[ x =~ ) ]]     parse error       parse error       runs, `after`
	//	[[ x =~ a) ]]    parse error       parse error       runs, `after`
	//	[[ x =~ (x)) ]]  parse error       parse error       runs, `after`
	//	[[ x =~ (x) ]]   runs              runs              runs
	//
	// A **balanced** group needs no flag: the scanner takes one whole in all
	// three, so the `)` of `(x)` never reaches the question. This is only the
	// closer that closes nothing, and two of the three columns end the word —
	// and the script — on it, where this lexer kept it and reported an invalid
	// regular expression at run time with the script carrying on. A parse
	// error and a runtime complaint are different control flow, which is what
	// makes this a grammar flag rather than a wording (#4173).
	RegexKeepsAnUnbalancedCloser bool

	// ArraySubscript enables `${a[i]}`, `${a[@]}` and `${a[*]}`, `a[i]`
	// inside an arithmetic expression, and the element assignment `a[i]=v`
	// and `a[i]+=v`. Absent from dash, which has no arrays at all and calls
	// the subscript a bad substitution rather than reading it — a separate
	// flag from ArrayLiteral because the two halves are separately
	// reachable: a subscript can be written for a variable that was never an
	// array.
	//
	// The assignment shape is this flag's rather than a fourth one, and that
	// is measured rather than assumed: reading a subscript and writing
	// through one split the panel the same way, with bash 3.2, bash 5.3,
	// ksh93 and zsh on one side and dash alone on the other. zsh's refusal
	// of `a[0]=x` is not a third answer — it parses the assignment and
	// rejects the *subscript*, which is the array-base axis and belongs to
	// the semantics vector. A flag no dialect can be given a different value
	// for is a field nothing reads, which is what #564 is about.
	//
	// Where it is off, `a[0]=x` is a command name and not an assignment —
	// the shell without arrays reports `a[0]=x: not found` and carries on.
	// Getting that wrong is silent on the permissive side: the element is
	// stored, nothing is reported, and a script written against the
	// no-array dialect on purpose is told it is portable when it is not.
	ArraySubscript bool

	// SubscriptQuoteProtectsTheClosingBracket says which quoting constructs
	// written inside a `${name[ … ]}` subscript hold a `]` back from ending
	// it, so that the subscript runs to the bracket *after* the quoted text.
	//
	// The read side only. The write side is already the lexer's and is
	// already unanimous — see SubscriptSpansSeparators, whose measured table
	// ends on `m['a]b']=v`, the key `a]b` in every column that has arrays.
	// Reading `${m['a]b']}` back is the same question put to a different
	// scanner, and the panel does not answer it the same way, which is why
	// it is a field rather than a constant.
	//
	// Measured 2026-09-15 with an indexed array, so that the answer is a
	// *diagnostic* rather than a value and the two readings cannot be
	// confused. `a=(9 8 7); echo "[${a[<q>0]<q>+1]}]"`, where <q> is the
	// quoting under test: where the quote protects, the subscript is the
	// whole of `0]+1` and the shell reports an arithmetic error naming it;
	// where it does not, the subscript ends at the quoted `]`, the `+` that
	// follows is read as the alternate-value operator and the expansion is
	// the text `1]` — the answer `[1]]`, with nothing reported.
	//
	//	                      \]      '…]…'   "…]…"   $'…]…'
	//	bash 5.3.20           yes     yes     yes     no
	//	bash 5.3.20 as sh     yes     yes     yes     no
	//	bash 3.2.57           no      no      no      no
	//	ksh93u+ 2012          yes     yes     yes     yes
	//	zsh 5.9.2             yes     no      no      no
	//	dash, BusyBox ash     no subscripts at all, so the question is never put
	//
	// POSIX mode does not move it: `--posix` and the name `sh` both answer
	// with the bash row above, measured the same day. That is why there is no
	// companion field here, where QuoteProtectsTheClosingBrace needed one.
	//
	// The `$'…'` column is measured and *not* modeled, and the reason is in
	// closingBracket: bash decodes the escapes before it looks for the
	// bracket, so a `]` inside `$'…'` is a bare one by the time the scan
	// runs. Reading the run whole, as this scan does, keeps the ordinary
	// `${a[$'k']}` working in every dialect that has the construct and
	// leaves `${a[$'x]y']}` disagreeing with bash — in a different word, at
	// the same size, as it did before any of this quoted anything.
	//
	// Whether the quote's own bytes then reach the subscript is a *separate*
	// question and is not this flag's: bash keeps a single quote's, so
	// `${a['0]'+1]}` is an arithmetic error against `'0]'+1` with the quotes
	// still in it, and removes a double quote's, reporting `0]+1`. ksh93
	// removes both. This flag decides only where the subscript ends.
	SubscriptQuoteProtectsTheClosingBracket SubscriptQuoting

	// SubscriptSpansSeparators lets a subscript written at command-word
	// position hold a separator: `m[foo bar]=v` assigns the element keyed
	// `foo bar`, where a grammar without the flag ends the word at the blank
	// and runs `m[foo` as a command.
	//
	// **It is the word boundary and so it is the lexer's**, the way
	// BareSubscript is. Once a name at command position is followed by `[`,
	// the text runs to the *matching* `]` and everything in between is
	// ordinary characters of the subscript — which is more than blanks.
	// Measured 2026-09-12 on bash 5.3.15, bash 3.2.57, bash as `sh` and
	// ksh93, each row read back with `typeset -p m`:
	//
	//	m[foo bar]=v      the key `foo bar`
	//	m[foo	bar]=v    a tab the same way
	//	m[foo\nbar]=v     and a newline: the word spans the line
	//	m[a; b]=v         and a `;`, a `|`, a `>` and an `&&`
	//	m[#c]=v           a `#` is not a comment in there
	//	m[a [b] c]=v      brackets nest, so the *matching* `]` ends it
	//	m['a]b']=v        a quoted `]` closes nothing
	//
	// A name is required in front of the bracket and it is required to be a
	// name: `1m[foo bar]=v`, `m-n[foo bar]=v` and `[foo bar]=v` all still
	// end at the blank in the same four shells. So does an argument —
	// `printf '<%s>' m[foo bar]=v` prints two fields everywhere — which is
	// why this asks atCommandWord rather than applying to every word.
	//
	// The `=` is not part of the condition, because the shells do not make
	// it one: `m[foo bar]` alone is `m[foo bar]: command not found` in all
	// four, quoting back the whole word. An assignment prefix is command
	// position too, so `a=1 m[foo bar]=v` writes the element.
	//
	// Additive and not a semantics axis: zsh 5.9.2 is the panel's holdout
	// and it does not mean something else by the text, it refuses it —
	// `bad pattern: m[foo`, exit 1 — and dash and BusyBox ash have no
	// arrays to subscript at all. So four columns read the text one way,
	// one refuses it outright, and two never reach a subscript: there is no
	// second *meaning* for a semantics axis to switch between.
	//
	// Where the bracket has **no** matching `]` the word is left exactly as
	// a grammar without the flag reads it, and that is deliberate. The two
	// shells that span separators diverge there — bash refuses with
	// `unexpected EOF while looking for matching ']'` and ksh93 swallows the
	// rest of the input into the word — so there is no common answer to
	// implement, and falling back means no input that parses today parses
	// differently tomorrow.
	SubscriptSpansSeparators bool

	// ArrayLiteralShapeFollowsTheFirstElement makes a compound literal one
	// shape or the other rather than a mixture: **either** every element
	// names a subscript — `[sub]=value`, or `[sub]+=value` — **or** none of
	// them does and a bracket in front of an element is text. The first
	// element decides which, and a bare element after a subscripted one is a
	// *parse* error rather than a word.
	//
	// Measured 2026-09-14 on ksh93u+ 2012-08-01 (`/bin/ksh`), against bash
	// 5.3.15, bash 3.2.57 and zsh 5.9.2, each row read back with
	// `typeset -p a`:
	//
	//	a=([1]=A [2]=B)   typeset -A a=([1]=A [2]=B)      subscripted throughout
	//	a=(p [1]=A)       typeset -a a=(p '[1]=A')        a word list, brackets and all
	//	a=(p q [1]=A)     typeset -a a=(p q '[1]=A')      and however far along it stands
	//	a=("[1]=A" p)     typeset -a a=('[1]=A' p)        a quoted head was never a subscript
	//	a=([1]=A p)       syntax error at line 1: `p' unexpected
	//	a=([1]=A "b")     syntax error at line 1: `b' unexpected
	//	a=([1]=A $x)      syntax error at line 1: `$x' unexpected
	//
	// The refusal is the grammar's and not the store's, which the last two
	// rows are what say: the offending element is named as it was *written*,
	// with `$x` unexpanded and the quotes off `"b"`, and it is refused
	// whatever `x` holds and whatever the name held before the assignment.
	// `a=([1]=A` + newline + `p)` is refused too, at the line the bare
	// element stands on, so the literal's own newlines do not end the rule.
	//
	// **The appending spelling is shaped by the same rule and not exempt**,
	// which is where #2505 as filed was wrong. `a+=([1]=A p)` is the same
	// syntax error, and `unset a; a+=([1]=Z [2]=Y)` is
	// `typeset -A a=([1]=Z [2]=Y)` — a subscripted literal, read as one.
	// What is separately true of `+=` is a *store* rule rather than a
	// grammar one: an append whose name is already holding an indexed array
	// cannot become a keyed one, and there the subscripted reading is given
	// up and the elements go in as the words they were written as. That half
	// is [interp.Runner.literalReadsSubscripts]'s and is measured there.
	//
	// Nothing else in the panel reads a literal this way. bash and zsh place
	// a subscripted element wherever it stands and continue the bare ones
	// from it, so `a=(x [3]=y z)` is three elements there and three *words*
	// here; dash and BusyBox ash have no array literal to shape.
	//
	// The word-list shape reaches the lexer too, because the blanks inside a
	// bracket are only characters while a subscript is being read:
	// `a=(p [1 2]=A)` is the three words `p`, `[1` and `2]=A` here, where
	// `a=([1 2]=A)` is the single key `1 2`. See
	// [Dialect.SubscriptSpansSeparators], which is the flag that spans them,
	// and Parser.arrayLiteral, which stops asking it once a bare first
	// element has settled the shape.
	//
	// It is a grammar flag and not a semantics axis because the two readings
	// are not two meanings for one parse: one of them is a parse error, and
	// the other changes where the word boundaries fall.
	ArrayLiteralShapeFollowsTheFirstElement bool

	// ArrayLiteralAfterRepeatedEquals reads `name==(…)` — an assignment whose
	// value is nothing but more `=` signs with a parenthesis touching them —
	// as the array literal `name=(…)`, the extra signs dropped.
	//
	// ksh93u+. Measured 2026-10-03 under -c: `f==(a b)` and `f===(a b)` both
	// list as `typeset -a f=(a b)`, and `f==(echo hi)` leaves `$f` holding
	// `echo`; `f==a` is the plain scalar `=a`. bash 5.3.20 and dash refuse
	// the parenthesis, and zsh reads `=(…)` as its file substitution (corpus
	// row procsub/a-file-substitution-lives-as-long-as-its-command).
	ArrayLiteralAfterRepeatedEquals bool

	// SubscriptSpansSeparatorsInRedirect extends the reading above to a
	// redirection's target, so `> m[foo bar] echo hi` writes one file named
	// `m[foo bar]` where a grammar without the flag writes `m[foo` and then
	// runs `bar]`.
	//
	// Separate from SubscriptSpansSeparators because the panel splits them.
	// Measured 2026-09-13 on ksh93u+ 2012-08-01 against bash 5.3.15, bash
	// 3.2.57 and bash as `sh`, each run in an empty directory and read back
	// with `ls`:
	//
	//	> m[foo bar] echo hi      ksh93 `m[foo bar]`; every bash `m[foo`
	//
	// So the four columns that span at command position are three-to-one
	// against spanning here, and this is one shell's answer rather than the
	// common rule — which is why #2410 followed bash and stopped, and why
	// this is a flag of its own rather than a widening of that one.
	//
	// **The position is "a redirection whose target stands where a command
	// may begin", not "a redirection".** Measured the same day, and this is
	// the half that is easy to get wrong:
	//
	//	> m[foo bar] echo hi      spans — the redirection is a prefix
	//	2> m[foo bar] echo hi     spans — an IO number changes nothing
	//	{ :; } > m[foo bar]       spans — a compound command's redirection
	//	for i in x; do :; done > m[a b]   spans, the same way
	//	echo hi > m[foo bar]      **splits** — an argument stood first
	//	echo hi 3> m[foo bar]     splits, the same way
	//
	// The condition in front of the bracket is the one
	// SubscriptSpansSeparators already carries and is not restated: a name
	// and nothing else, so `> 1m[foo bar]`, `> m-n[foo bar]` and
	// `> [foo bar]` all still end at the blank in ksh93. Text *after* the
	// matching `]` stays part of the word — `> pre[1 2]post` names one file
	// — because the bracket ends the subscript and not the word.
	//
	// A `case` subject is the near miss and it is why the lexer has a flag
	// of its own for this position: it is read where a command may begin and
	// takes no assignment, exactly as a redirection prefix does, and ksh93
	// **splits** there — `case m[foo bar] in *) ;; esac` is
	// the message `bar]' unexpected.
	//
	// Additive on the same terms as SubscriptSpansSeparators: with no
	// matching `]` the word is left exactly as a grammar without the flag
	// reads it. ksh93 swallows the rest of the input there
	// (`> m[foo bar echo hi` writes a file called `m[foo bar echo hi[`) and
	// bash refuses, so there is no common answer to reach for and nothing
	// that parses today parses differently with the flag on.
	SubscriptSpansSeparatorsInRedirect bool

	// SpecialParamSubscript lets a parameter that is *not* a name carry a
	// subscript: `${@[1]}` and `${*[2]}` name one of the positional
	// parameters, and `${1[2]}`, `${0[1]}`, `${?[1]}`, `${-[1]}` and
	// `${$[1]}` reach into the value of a special one.
	//
	// One shell in the panel. Measured on zsh 5.9.2 against bash 5.3.15,
	// bash 3.2.57, bash as `sh`, ksh93 and dash: `set -- a b; echo ${@[1]}`
	// prints `a` there, where bash calls the whole expansion
	// `${@[1]}: bad substitution` when it is reached, ksh93 refuses `[' at
	// parse time and dash says `Bad substitution`. Every one of those five
	// refuses; none of them means something else by it. So this is the same
	// kind of split ArraySubscript is — a construct one grammar has and the
	// rest do not — rather than a conflict for the semantics vector.
	//
	// It is the *name* that this flag is about and not the subscript, which
	// is why it is separate from ArraySubscript: `${a[1]}` is read by four
	// of the six and `${@[1]}` by one, so a single flag could not say both.
	//
	// Where it is off the bracket is simply not consumed, and the leftover
	// text takes the ordinary route an unreadable expansion takes — deferred
	// to the run in most of the panel, refused while reading by the one
	// grammar that refuses everything else while reading. Nothing here has
	// to word a diagnostic of its own.
	//
	// `#` and `!` are unreachable in the braced spelling for the reason
	// BareSubscript records: `${#[1]}` is a length and `${![1]}` an
	// indirection, so there is nowhere to write them down.
	SpecialParamSubscript bool

	// ArraySubscriptFlags enables a parenthesized flag group at the front of
	// a *subscript* — `${a[(re)value]}`, the first element equal to the
	// operand — which is a different construct from the flag group
	// ParamExpansionFlags enables and not the same one moved. The three
	// differences are set out at the head of subscriptflags.go, and the
	// character sets are separate for the first of them.
	//
	// Additive rather than a semantics axis, and measured rather than
	// argued: the grammar that has the group falls back to arithmetic for
	// any group it cannot read, which is exactly what the four grammars
	// without it do with every group — `${a[(z)2]}` is an arithmetic failure
	// in all six shells on the panel and only `${a[(r)beta]}` divides them.
	// So there is no text this flag gives a *second* reading to; it gives a
	// reading to text that had none.
	//
	// It rides on ArraySubscript, which is what makes the brackets a
	// subscript in the first place, and on BareSubscript for the unbraced
	// form. A grammar with this and neither of those has nowhere to put a
	// group.
	ArraySubscriptFlags bool

	// CommandWordSubscriptHasNoFlagGroup takes the flag group off a subscript
	// written in a **command word** — `b[(r)y]=Q` — and leaves it on a
	// subscript written inside a substitution — `${b[(r)y]}`. With it on the
	// `(` is the shell's own operator where a command word carries one, so
	// the word ends there and the line is refused.
	//
	// The two halves are one measurement and the second is what makes this a
	// field rather than [Dialect.ArraySubscriptFlags] going off. On zsh 5.9.2
	// (aarch64-apple-darwin25.4.0) at `/opt/homebrew/bin/zsh`, run `-f` from a
	// script file, 2026-09-27, with `shglob` on and `kshglob` off:
	//
	//	b[(r)y]=Q                          parse error near `('
	//	b=(x y z); printf %s "${b[(r)y]}"  y — the search still runs
	//
	// So the group is gone from one position and not from the construct, and
	// a flag turning the whole thing off would have taken a working
	// substitution with it.
	//
	// The blanks are the control and they do not move: `b[a b]=Q` parses in
	// that state exactly as it does without the option, so this is about the
	// parenthesis rather than about a command word's subscript being read the
	// standard's way. See [Dialect.SubscriptSpansSeparators], which is the
	// flag that would have moved if it were.
	CommandWordSubscriptHasNoFlagGroup bool

	// BareSubscript lets a parameter written without braces carry a
	// subscript, and lets `$#name` mean that parameter's length: `$a[1]` is
	// an element and `$#a` is a count, where a grammar without the flag
	// reads `$a` followed by the three characters `[1]`, and `$#` followed
	// by the letter `a`.
	//
	// One shell in the panel, measured on zsh 5.9.2 against bash 3.2, bash
	// 5.3 and dash: `a=(x y z); echo $a[1]` prints `x` there and `x[1]`
	// everywhere else, and `echo $#a` prints `3` there and `0a` everywhere
	// else. So this is a *grammar* question and not a value on the semantics
	// vector — the same characters are two different words, not one word two
	// shells disagree about the meaning of — which is why it sits here beside
	// ShortLoop rather than in a dialect package.
	//
	// It is the lexer's, because the word boundary is: `$a[1]` is one
	// expansion where the flag is on and an expansion plus a glob pattern
	// where it is off, and nothing downstream can tell them apart once the
	// spans are cut. Getting it wrong is loud in one direction and silent in
	// the other — a shell without the flag globs `x y z[1]` and reports no
	// matches, while a shell with it applied everywhere would quietly turn
	// every `$dir[0-9]*` in a bash script into an element lookup.
	//
	// The subscript follows a name, `@` or `*`. Not the positional digits:
	// measured, `set -- abcd; echo $1[2]` prints `abcd[2]` there, so the
	// parameters that carry one are not simply all of them. The remaining
	// specials do take one — `$?[1]`, `$-[2]`, `$$[1]` and `$0[2]` are all
	// subscripted — and are left out here because a span has no way to write
	// two of them down: `${#[1]}` is a length and `${![1]}` an indirection,
	// so the inner text of `$#[1]` and `$![1]` would say something else.
	//
	// `$#` takes a name, a digit, `@` or `*` after it. Not `#` and not `!`:
	// `$##` prints `2#` and `$#!` prints `2!` on the same shell, so the
	// length form stops at exactly two of the specials rather than at all of
	// them.
	BareSubscript bool

	// BareParamFlags enables zsh's unbraced flag sigils: a flag character
	// between the `$` and the name, without braces. `$+v` is `${+v}`, `$=v`
	// is `${=v}`, `$~v` is `${~v}` and `$^a` is `${^a}` — the same four
	// flags the braced group already takes, written the short way.
	//
	// One shell in the panel. Measured on zsh 5.9.2 against bash 5.3.15,
	// bash 3.2.57, bash as `sh`, ksh93 and dash: `v=1; echo $+v` prints `1`
	// there and the literal `$+v` everywhere else, and the same for the
	// other three.
	//
	// It is the lexer's for the reason BareSubscript is: the word boundary
	// moves. `$=v` is one expansion where the flag is on and a literal `$`
	// followed by `=v` where it is off, and nothing downstream can tell them
	// apart once the spans are cut. Separate from BareSubscript because it
	// is a separate question — that one decides whether `[` after a name
	// belongs to the expansion, and a shell could sensibly answer the two
	// differently.
	//
	// The four do not all take the same target, which is why the lexer asks
	// what follows. `$+` takes a name or a digit and nothing else: measured,
	// `$+@`, `$+*`, `$+?` and `$+$` all print themselves, and so does `$++v`
	// — the sigil does not repeat. The other three take every target a bare
	// `$` does, specials included, and are flags even with no target at all,
	// since a bare `$=` expands to nothing there rather than printing.
	BareParamFlags bool

	// BareParamModifiers lets a parameter written without braces carry a
	// history-style modifier list: `$p:t` is the tail of `$p`, and `$b:q` is
	// each element of `$b` quoted.
	//
	// One shell in the panel. Measured on zsh 5.9.2 against bash 5.3.20,
	// bash 3.2.57, bash as `sh`, ksh93u+, dash and BusyBox ash, 2026-09-18
	// with `s='p q'`: `$s:q` is `p\ q` there and the six characters `p q:q`
	// in all six of the others — which is what this grammar already did, so
	// the flag is what one shell adds rather than what six of them lose.
	//
	// It is the lexer's for the reason BareSubscript and BareParamFlags are:
	// the word boundary moves. `$s:q` is one expansion where the flag is on
	// and an expansion followed by two literal characters where it is off,
	// and nothing downstream can tell the two apart once the spans are cut.
	//
	// **The braced spelling is not this**, which is the measurement that
	// makes the flag about the *bare* form alone: `${b}:q` leaves the `:q` as
	// text in that shell too, and `${b:q}` is the modifier written inside the
	// braces, which is ParamSubstring and needs no flag. So a grammar with
	// modifiers in braces and without this one is a coherent grammar and not
	// a half-finished one.
	//
	// **The bare form takes the letter and nothing after it**, where the
	// braced form takes a count. Measured with `p=/a/bb/c.txt`: `${p:h2}` is
	// `/a` — the head twice — and `$p:h2` is `/a/bb2`, the head once with a
	// literal `2` after it. `$p:t2` is `c.txt2` the same way, and `$s:qX` is
	// `p\ qX`. The one exception is `:s`, which takes its whole delimited
	// substitution: `$p:s/a/Z/x` is `/Z/bb/c.txtx`.
	//
	// **A letter that names no modifier is not one**, and there is no
	// complaint: `$s:zz` is `p q:zz` and `$s:` is `p q:`, where the braced
	// `${s:zz}` is refused. So the scan gives the colon back rather than
	// failing on it, which is what lets a bare `:` after an expansion go on
	// meaning whatever it meant.
	BareParamModifiers bool

	// MultiDigitPositional makes a run of digits after an unbraced `$` one
	// positional parameter: `$10` is the tenth, not `$1` followed by a `0`.
	//
	// One shell in the panel. Measured 2026-09-15 with
	// `set -- 1 2 3 4 5 6 7 8 9 ten eleven twelve` on zsh 5.9.2, bash 5.3,
	// bash 3.2, bash as `sh`, ksh93, dash and BusyBox ash: `$10` is `ten` in
	// zsh and `10` — `$1` then the character — in the other six, and `$11` is
	// `eleven` against `11` the same way. The **braced** `${10}` is the tenth
	// in every one of the seven, which is what says this is about where the
	// unbraced token ends and not about what a positional name means.
	//
	// It is the lexer's for the reason BareSubscript and MultiDigitFdNumber
	// are: the word boundary moves. `$10` is one expansion where the flag is
	// on and an expansion plus a literal `0` where it is off, and once the
	// spans are cut nothing downstream can tell the two readings apart. And
	// it is a grammar flag rather than a semantics axis for the same reason —
	// the two shells are not disagreeing about the value of a parameter, they
	// are reading different tokens.
	//
	// The run is read as a *number*, which is what decides the leading-zero
	// spellings rather than a rule about the digits: measured on the same
	// binary, `$01` is the first parameter, `$09` is the ninth, `$010` is the
	// tenth, and `$00` is the shell's own name. So the name the span carries
	// is whatever digits were written and whoever resolves it reads the
	// number — the same division FdVariablePositional makes, and the reason
	// the lexer does not normalize the text it took.
	//
	// Neighboring forms are unaffected and were measured beside it. A digit
	// run still stops at the first non-digit, so `$1a` is `$1` then `a` in
	// every column; a run still takes no bare subscript, so `$1[2]` is
	// unchanged; and `$#10` is the length of the tenth parameter in the shell
	// that has both this and BareSubscript, which falls out of the two
	// without a rule of its own.
	//
	// POSIX is the reason this is a flag rather than a fault in six shells:
	// XCU makes `$10` the first positional parameter followed by a `0` and
	// says the multi-digit form has to be braced, so the majority conforms
	// and the one shell that reads further is the extension.
	MultiDigitPositional bool

	// ChainedSubscript lets a braced expansion carry more than one subscript,
	// each reading what the one before it named: `${m[k][2]}` is the second
	// *character* of the value under `k`, and `${a[2,4][1]}` the first
	// *element* of the three the range named. Which of the two a subscript
	// counts is decided by what it is handed rather than by where it stands,
	// so a chain is the same rule applied twice and not a new one.
	//
	// One shell in the panel, measured 2026-09-08 on zsh 5.9.2 with
	// `a=(one two three); ${a[1][2]}`: this shell answers `n`, bash 5.3.15
	// and that binary as `sh` both answer `${a[1][2]}: bad substitution`,
	// dash has no arrays to subscript, ksh93 answers empty, and bash 3.2.57
	// answers `two` — the first subscript read and the second ignored, which
	// is the reading none of the others has and is not the one this enables.
	// So the text divides the panel, and this is the grammar that has it.
	//
	// Braced only. Measured on the same binary: `$m[k][2]` unbraced is one
	// subscript and the rest is a pattern — `no matches found: abc[2]` — so
	// the bare spelling keeps its single bracket, which is what
	// Lexer.bareSubscript already reads.
	//
	// It rides on ArraySubscript, which is what makes the first bracket a
	// subscript at all. `~/.zi/bin/zi.zsh` writes `${ICE[atload][1]}` ten
	// times, in the function that decides whether an `atload'!…'` ice needs
	// tracking (#1516).
	ChainedSubscript bool

	// SubscriptDotRange reads a braced expansion's subscript written
	// `lo..hi` as a range of elements rather than as one arithmetic
	// expression: `${a[1..3]}` is the elements 1 through 3, as separate
	// fields, where every other grammar hands `1..3` to the arithmetic and
	// refuses it.
	//
	// Measured 2026-09-16 with `a=(a b c d e)`, script files under `env -i`:
	// ksh93u+ 2012-08-01 answers `b c d` and `"${a[1..3]}"` is three fields;
	// bash 5.3 calls it an arithmetic syntax error, and zsh 5.9.2 a bad
	// floating point constant. An additive split, so a grammar flag.
	//
	// The `..` must be written, in a literal of the subscript — quoted or not
	// — and not produced: `x=1..3; ${a[$x]}` is the arithmetic refusal in the
	// shell with the construct, and so is `${a[1\..3]}`. The first one
	// written is the separator, which is why `(1..3)` splits inside its
	// parentheses there and is refused as unbalanced. What the range then
	// names is the run's — see interp/dotrange.go.
	SubscriptDotRange bool

	// ChainedAssignSubscript lets an assignment's name carry more than one
	// subscript, where the later ones reach *into* the value the earlier one
	// named rather than being part of its text: `a[1][2]=v` puts `v` at 2 of
	// the array held in element 1.
	//
	// One shell in the panel, and it is not the one ChainedSubscript is for.
	// Measured 2026-09-14 on ksh93u+ 2012-08-01, `env -i` with a scratch
	// HOME, read back with `typeset -p a`:
	//
	//	a[1][2]=v         typeset -a a=([1]=([2]=v) )   and ${#a[@]} is 1
	//	typeset a[1][2]=v the same
	//	a[1]=([2]=v)      the same, which is the spelling this engine had
	//
	// bash 5.3 and zsh both refuse the operand — `not a valid identifier`
	// and `no matches found`, the brackets being a pattern there — and bash
	// 3.2 declares an empty array under the base name at status 0. So four
	// answers among the shells that reach it and this is the grammar for the
	// one that nests (#2491).
	//
	// It is the *write* half alone. What `${a[1][2]}` reads in that shell is
	// a second question and a different reading from the one
	// ChainedSubscript enables — see #2830.
	ChainedAssignSubscript bool

	// EmptyAssignSubscriptIsASyntaxError refuses `a[]=v` — an assignment
	// whose brackets were written with nothing at all between them — while
	// the program is being *read*, rather than where it runs.
	//
	// One column answers here, and that it answers here is measured rather
	// than assumed. ksh93u+ 2012-08-01, script file under `env -i
	// PATH=/usr/bin:/bin LC_ALL=C` with standard input on the null device:
	//
	//	echo before
	//	if false; then a[]=6; fi
	//	echo after
	//
	// writes `before`, then “<file>: syntax error at line 2: `[]' empty
	// subscript“, and exits 3 with no `after`. The branch is never taken
	// and the refusal arrives all the same, which is what says the grammar
	// is refusing the text. bash 5.3.20 and zsh 5.9.2 both run that script
	// to the end in silence and complain only where such an assignment is
	// actually reached, so theirs is a runtime answer and is
	// interp.Semantics.EmptySubscriptToAnAssignment instead.
	//
	// Every written subscript of the assignment is looked at, not only the
	// last: `a[][2]=6` and `a[2][]=6` are both refused there, with the same
	// sentence (#3949).
	EmptyAssignSubscriptIsASyntaxError bool

	// ArithCharacterCode enables `#name` and `##c` inside an arithmetic
	// expression: the code of the first character of a parameter's value, and
	// the code of a character written out.
	//
	// One shell in the panel. Measured 2026-09-05 on zsh 5.9.2, where
	// `b=zebra; echo $((#b))` prints 122 and `echo $((##a))` prints 97
	// — against bash 5.3.15, bash 3.2.57, bash as `sh`, ksh93 and dash, every
	// one of which calls the same text an arithmetic syntax error naming the
	// operand it could not read. An operator five shells refuse and one has
	// is the additive kind of split, so it is a flag here rather than a value
	// on the semantics vector.
	//
	// It does not disturb the `base#digits` literal, which every shell with
	// arithmetic has: that `#` follows digits and is read by the number, and
	// this one stands where an operand belongs.
	//
	// Getting it backwards is the hazard the operator is worth recording for:
	// `$((#a))` looks like a length and is not one. On `a=(1 2)` it is 49,
	// the code of the `1`, where the count is `$(( $#a ))`.
	ArithCharacterCode bool

	// ArithSpecialParameterOperands reads `?`, `$` and a `#` with no name
	// after it, standing where an operand belongs, as the special parameters
	// of those names. Measured 2026-10-02 on zsh 5.9.2 with `set -- a b;
	// (exit 3)`: `$(( ? ))` is 3, `$(( ? + 1 ))` 4, `$(( 1 ? ? : 2 ))` 3,
	// `$(( # ))` 2, `$(( $ > 0 ))` 1, and `repeat '?' cmd` runs cmd three
	// times; `$(( @ ))` is `illegal character: @`. bash 5.3.20 calls the
	// first an operand it expected. They are values and not names a script
	// may assign: `$(( ?++ ))` and `$(( ? = 5 ))` are `lvalue required`.
	//
	// The `#` is the character-code operator's own spelling with no name
	// after it, so it is read there — see ArithCharacterCode, where `#a` is
	// the code of `$a`'s first character and `##a` the code of `a`.
	ArithSpecialParameterOperands bool

	// ArithFunctionCall enables `name(args)` inside an arithmetic expression:
	// a call to a *math function*, which is a name the shell has been told
	// arithmetic may call. See [ArithCall].
	//
	// Two things register one and the grammar cannot tell them apart, which is
	// why this says "a name" rather than "a shell function": `functions -M`
	// names a shell function to run, and a module may bring a table of them
	// implemented in the interpreter itself. The measurement below is of the
	// first, because it is the one a script writes.
	//
	// **Two** shells in the panel have it, and this comment said one until
	// 2026-09-12. Measured 2026-09-08 on zsh 5.9.2 and on zsh 5.9, where
	// `g(){ REPLY=$(($1+100)); }; functions -M mf 1 1 g; echo $(( mf(5) ))`
	// prints 105. Re-measured 2026-09-12 on ksh93u+ 2012-08-01, `env -i` with
	// a scratch HOME, and that shell has the construct too — with a library
	// of them built in and nothing to register:
	//
	//	echo $(( sqrt(4) ))        2
	//	echo $(( pow(2,10) ))      1024
	//	echo $(( fmod(7,3) ))      1
	//	echo $(( int(3.7) ))       3
	//	echo $(( nosuchmf(1) ))    nosuchmf(1) : unknown function
	//	echo $(( atan(1,2) ))       atan(1,2) : function has wrong number of
	//	                           arguments
	//
	// So it has the grammar *and* both sentences, and the flag is on for it
	// since 2026-09-13, when the built-in table arrived beside it — see
	// dialect/ksh/mathfunc.go, which is that shell's sixty-one. Turning the
	// grammar on without the table would have been worse than the syntax
	// error it replaced, because a name that shell *knows* would have come
	// back `unknown function`; the two go together and landed together.
	// What the flag is **not** is a fact about that shell's grammar, and it
	// was read as one: #2420 grouped five corpus rows as a wording difference
	// over a syntax error when they are a shell that parsed a call we did
	// not.
	//
	// bash 5.3, bash 3.2, bash as `sh`, dash and ash have neither the
	// registration nor a table, and read `mf(5)` as a name followed by a
	// leftover `(`. So those columns record the grammar's absence rather than
	// a different meaning for the same text, which is what makes this
	// additive.
	//
	// The `(` has to touch the name. A space between them is not a call in
	// the shell that has one either — `$(( mf ( 5 ) ))` is
	// `operator expected` there — so turning this on does not change what
	// `$(( a (b) ))` means anywhere.
	ArithFunctionCall bool

	// ArithOutputFormat enables the bracketed output-format specifier inside
	// an arithmetic expression: `$(( [#16] 255 ))` is `16#FF` and
	// `$(( [##16] 255 ))` is `FF`. It says the base the *result is written
	// in*, and a second `#` drops the `base#` in front of the digits.
	//
	// The rest of the specifier is measured rather than guessed: an `_`
	// after the base groups the digits — `[#16_4] 1048575` is `16#F_FFFF`,
	// and a bare `_` groups decimal in threes — and `_0` turns grouping off.
	// What a base *means* is not here, because it decides nothing about the
	// tree: see the ArithOutput node, and interp's IntegerBaseDigits for the
	// alphabet and the range a base is checked against.
	//
	// It is **lexical rather than positional**, which is the part worth
	// recording because the obvious reading is wrong. The specifier is not a
	// prefix operator over the expression that follows it: it may stand
	// anywhere a token may, including after a value — `$(( 2[#8] ))` is
	// `8#2` — and it takes effect even where the expression it stands in is
	// never evaluated: `$(( 0 ? [#16] 1 : 2 ))` is `16#2`. Several may
	// appear, and the *textually last* one decides, which is what
	// `$(( [#16] 255 + [#8] 1 ))` being `8#400` says. So it is read while
	// skipping blanks and lifted to the top of the tree, rather than being a
	// node where it was written.
	//
	// One shell in the panel. Measured 2026-09-12 on zsh 5.9.2, against
	// bash 5.3.15, bash 3.2.57, bash as `sh`, ksh93u+ and dash — every one of
	// which reads the `[` as an operand it cannot have and says so. An
	// operator five shells refuse and one has is the additive kind of split,
	// so it is a flag here rather than a value on the semantics vector.
	//
	// It does not disturb a subscript, and could not: a subscript's bracket
	// touches the name in front of it and is read by the name, where this one
	// stands where a token begins. `a[#8]` is the element of `a` under the
	// subscript `#8`, measured in the same shell.
	ArithOutputFormat bool

	// DoubleBracket enables `[[ ... ]]`.
	//
	// Consumed by the *parser*, not the lexer, and the reason is worth
	// stating because the opposite is the obvious guess. Inside `[[ ]]` the
	// `<` and `>` are comparisons rather than redirections, which sounds like
	// a lexer mode — but `[[` is only special in command position (`echo [[ a
	// ]]` prints `[[ a ]]`), and the lexer does not know where commands
	// begin. Lexing `<` as an operator loses nothing: the parser knows it is
	// inside `[[ ]]` and reinterprets the token. A lexer mode keyed on seeing
	// the word `[[` would break `echo`.
	DoubleBracket bool

	// DoubleBracketIsACommand reads `[[` as the *name of a command* — `test`
	// with a closing word — rather than as the conditional's keyword. Meant
	// for a dialect with DoubleBracket off; with both on, the keyword wins
	// where a command begins and this still governs every other `[[` word.
	//
	// Measured 2026-09-16 on BusyBox v1.37.0 in the pinned alpine image, the
	// one shell in the panel that answers this way. `type '[['` is `[[ is a
	// shell builtin`; `v='[['; $v -n x ]]` runs it; its operands are split
	// and globbed (`s="two words"; [[ $s == "two words" ]]` is `words:
	// unknown operand` at 2); `[[ a < b ]]` opens `b` for reading; and `[[ (
	// -n x ) ]]` is a syntax error at the `(`, because every operator the
	// shell has is still an operator.
	//
	// All but two. After an unquoted `[[` word of a simple command, `&&` and
	// `||` are *words* until an unquoted `]]` word, so the builtin receives
	// them as its connectives: `[[ -n x && -z "" ]]` is 0. The rule is about
	// the word and not about command position — `echo [[ a && b ]] && echo
	// c` prints `[[ a && b ]]` and then `c` — and it is the parser's rather
	// than the lexer's, since `for w in [[ a && b ]]` is `unexpected "&&"`
	// there. A quoted `"[["`, `\[[` or `x[[` opens nothing, `]]x` closes
	// nothing, and `;`, `|`, `&`, a newline and the redirections keep their
	// meaning inside: `[[ -n x &&` at the end of a line is `missing ]]`.
	DoubleBracketIsACommand bool

	// ProcessSubstitution is `<(cmd)` and `>(cmd)`: a command run with one end
	// of a pipe, expanding to a path the other end can be opened by.
	//
	// It is the sharpest evidence that a dialect is a runtime switch rather
	// than a build-time identity, and the reason is measured: bash 3.2 has it
	// as `bash` and loses it as `sh`, from the same binary — see
	// docs/spec/shell-matrix.md.
	ProcessSubstitution bool

	// ProcessSubstitutionToFile is `=(cmd)`: the same construct with a
	// regular file where the other two spellings have a pipe.
	//
	// Additive and to one dialect. Measured 2026-09-11: `echo =(echo hi)`
	// writes a path in zsh 5.9.2 and the other five columns refuse the `(`,
	// so there is nothing for a semantics axis to switch between — the
	// construct is either in the grammar or it is not.
	//
	// **Position is the grammar, and it is narrow.** The `=` has to begin a
	// word: `echo =(echo hi)x` appends the `x` to the path, and `echo
	// x=(echo hi)`, `echo a=b=(echo hi)`, `echo \=(echo hi)` and `echo
	// =(echo hi)=(echo hi)` are all `missing end of string` there. Quoting
	// takes it away like any other operator — `"=(echo hi)"` is its own ten
	// characters — and the one position that is not the front of a word is
	// the front of an assignment's *value*: `a==(echo hi)` assigns the path,
	// and so do `a[1]==(…)`, `a+==(…)` and `typeset a==(…)`, while the same
	// word written as an argument is the refusal above. An array literal's
	// element needs no rule of its own, `a=(=(echo hi))` being the front of
	// a word again.
	//
	// The flag is the lexer's: whether `=(` opens a substitution decides
	// where the word ends, and that is settled before any parser sees a
	// token. See Lexer.startsProcSubstFile, and
	// docs/spec/grammar/substitutions.md for the measurements.
	ProcessSubstitutionToFile bool

	// ReadFileSubstitution makes `$(<file)` the file's contents.
	//
	// A command substitution whose whole body is one input redirection and
	// nothing else — no command word, no assignment, no second redirection —
	// is a *special form* rather than a program: the file is opened and its
	// bytes become the substitution's result, with no command run and no
	// process started. “ `<file` “ is the same form in the older spelling,
	// and an explicit `0` before the operator is still it.
	//
	// It is additive because the shell without it reads the same text as an
	// ordinary command: a redirection with no command name opens the file,
	// runs nothing, and writes nothing, so the substitution is empty.
	// Measured 2026-09-10 with `printf x > f` and `printf "[%s]" "$(<f)"`:
	// `[x]` in zsh 5.9.2, bash 5.3, bash 3.2, bash as `sh`, bash `--posix`
	// and ksh93, and `[]` in dash — the sole holdout, which is what puts the
	// form in [Core].
	//
	// It is not the shell's null-command hook, and that was the fork worth
	// settling before any of this was written. zsh runs a bare redirection
	// with no command through `READNULLCMD`, so `<f` at a prompt pages the
	// file — but the special form is measurably not that route: with
	// `READNULLCMD` pointing at a function that prints `CHANGED`, `<f` prints
	// `CHANGED` and `$(<f)` still prints the file, while `$(:; <f)` and
	// `$(<f; :)` — the same redirection with company, so no longer the whole
	// body — print `CHANGED` again. A hook the form does not consult is a
	// different mechanism, and implementing this as "a null command copies
	// its input" would have made `<f` print the file in bash and ksh93, where
	// measurably it prints nothing.
	//
	// The flag is read by the interpreter rather than the parser. The body of
	// a command substitution is kept as source and parsed when it is
	// expanded, so the tree is the same either way and only the meaning
	// differs — which is why the question is asked where the substitution
	// runs.
	ReadFileSubstitution bool

	// ProcessSubstitutionInParamOperand says a `${…}` operand may carry one.
	//
	// Separate from ProcessSubstitution because the panel separates them:
	// measured, `${u:-<(:)}` is a path in bash and the five characters
	// `<(:)` in ksh93, zsh and dash — and ksh93 and zsh both have process
	// substitution everywhere else. So whether `<(` opens one is a question
	// about *where the word stands*, not only about the dialect, and every
	// operand of an expansion is on the same side of it: the pattern of
	// `${v#…}`, the replacement of `${v/…/…}` and the word of `${v:-…}` all
	// answer alike.
	//
	// It matters most where the operand is a pattern, because there the
	// wrong answer is silent. The text is pattern text in the shells that
	// say no, so `${v#<(x)}` is a `<` and — where the grammar has bare
	// groups — the group `(x)`, which matches `<x`; a reading that took the
	// substitution's *inner* text instead matched a bare `x`, which no shell
	// in the panel does, and answered with the subject quietly trimmed.
	ProcessSubstitutionInParamOperand bool

	// ProcessSubstitutionOnlyWhereACommandTakesAWord refuses `<(cmd)` and
	// `>(cmd)` **while reading** anywhere but the two places a command takes
	// a word: an argument of a simple command, and the target of a file
	// redirection. The refusal names the opener — “ `<(' unexpected “ — and
	// abandons the input, the way any other token in the wrong place does.
	//
	// ksh93 alone, of the three panel members that have the construct.
	// Measured 2026-09-13, `env -i PATH=/usr/bin:/bin` with a scratch HOME,
	// ksh93u+ 2012-08-01 over `-c`, stdin on the null device:
	//
	//	[[ x == <(:) ]]                   `<(' unexpected, status 3
	//	[[ -f <(:) ]]                     the same
	//	[[ <(:) ]]                        the same
	//	case <(:) in *) :;; esac          the same
	//	case x in <(:)) :;; esac          the same
	//	for i in <(:); do :; done         the same
	//	select i in <(:); do break; done  the same
	//	a=( <(:) )                        the same
	//	cat <<< <(:)                      the same
	//	[[ x == >(:) ]]                   `>(' unexpected, status 3
	//
	//	echo <(:)                         /dev/fd/3
	//	cat <(:)                          runs
	//	: <(:)                            runs
	//	set -- <(:)                       /dev/fd/3
	//	cat < <(:)                        runs — a redirection target
	//	for i in a; do echo <(:); done    /dev/fd/3 — the body is commands
	//
	// **The refusal is at the parse and not at the run**, which the last row
	// of the first block is not enough to show. `false && [[ x == <(:) ]];
	// echo reached` prints nothing and exits 3: the condition is in a branch
	// never taken, so a shell refusing it at the run would have printed
	// `reached`.
	//
	// **This is why the flag is not named for the condition.** #930 was filed
	// from the `[[ ]]` row alone, and a flag spelled "a condition's operand
	// may carry one" would have made this shell accept the other eight —
	// encoding a rule ksh93 does not have. The measured rule is about where a
	// *word* stands, and the condition is one of five positions that are not
	// it.
	//
	// It is a grammar flag rather than the
	// interp.Semantics.ProcessSubstitutionInCondition axis because an axis
	// cannot carry a moment. That axis says whether the substitution may
	// stand as a condition's operand and is still the right question for zsh,
	// which reads the word and then refuses it at the run, in a sentence of
	// its own and at status 2. Both shells say no; only this one says it
	// before anything has run.
	//
	// Two boundaries the measurement draws that this flag deliberately does
	// not reach, each recorded so that a later reading does not mistake them
	// for oversights:
	//
	//   - At the *start* of a command ksh93 never lexes the opener at all,
	//     `<` being a redirection operator there: `if <(:); then :; fi`,
	//     `echo | <(:)` and `! <(:)` are “ `)' unexpected “, naming the
	//     paren rather than the pair. That is the lexer's rule about `<` and
	//     not the grammar's about `<(`, and this shell reads the opener there
	//     as a word.
	//   - The opener also *ends the word before it* in ksh93 — `echo a<(:)b`
	//     writes three fields, `a`, the path and `b` — where this shell keeps
	//     one. A separate fact about word boundaries, measured here so it is
	//     not rediscovered as this rule.
	ProcessSubstitutionOnlyWhereACommandTakesAWord bool

	// ProcessSubstitutionAgainstTheRedirectionIsRefused refuses, while
	// reading, a redirection target that begins with a process substitution
	// running the other way from the redirection: `>(cmd)` after an operator
	// that reads, `<(cmd)` after one that writes, and either after a
	// duplication. The refusal names the opener, as
	// ProcessSubstitutionOnlyWhereACommandTakesAWord's does.
	//
	// ksh93 alone. Measured 2026-10-02 on ksh93u+ 2012-08-01 over a script
	// file `: OP SUBST` then `echo next`, under `env -i` (#5514):
	//
	//	<  <>  3<  3<>   then >(…)   `>(' unexpected, the script ends
	//	>  >>  >|  3>    then <(…)   `<(' unexpected
	//	<&  >&           then either  that opener unexpected
	//	<  3<            then <(…)   runs
	//	<> 3<>           then <(…)   opens the path, which is refused at run
	//	>  >>  >|  3>    then >(…)   opens the path
	//
	// bash 5.3.20 and zsh 5.9.2 read every row.
	ProcessSubstitutionAgainstTheRedirectionIsRefused bool

	// FdVariableRedirections is `{name}>file` and its family: the shell
	// picks the descriptor and the variable receives its number. Consumed
	// by the lexer, because the adjacency to the operator is the whole
	// grammar — `{fd}>f` names a descriptor and `{fd} >f` is a word.
	//
	// Three of the four have it; to dash the braces are part of an
	// ordinary word.
	FdVariableRedirections bool

	// FdVariablePositional lets the name inside those braces begin with a
	// digit, which is how a *positional parameter* names a descriptor:
	// `exec {1}>&-` closes the one whose number is in `$1`.
	//
	// zsh alone. Measured 2026-09-10: `f(){ exec {1}</dev/null; }` opens the
	// descriptor and puts its number in `$1` there, where bash 5.3 answers
	// `exec: {1}: not found` — it read the braces as a word — and ksh93
	// answers `1: invalid variable name`. So two of the three shells with
	// the parent feature refuse it, which is the same shape
	// FdVariableSubscript has and the reason this is a flag of its own
	// rather than a consequence of having the parent.
	//
	// A digit-leading name must be *all* digits, and that rule is the
	// runtime's rather than the lexer's: measured, `{1a}` and `{1_}` are
	// taken as the token and then refused with `not an identifier: 1a`,
	// where `{01}` and `{99}` work. So the lexer admits the run and
	// whoever resolves the name decides, which is also what keeps the two
	// halves from disagreeing about what a name is.
	//
	// It is needed by a real prompt theme: a scheduler handed a descriptor
	// as its first argument closes it exactly this way.
	FdVariablePositional bool

	// MultiDigitFdNumber lets a redirection's descriptor number have more
	// than one digit: `exec 10>file` opens the file on ten.
	//
	// bash and BusyBox ash read it that way. To dash, ksh93 and zsh the
	// digits are an ordinary word and the operator is a redirection of its
	// own, so `exec 10>f` is `exec 10 >f` and reports that no command called
	// `10` was found — the same answer for every width from two digits up.
	// Those three still hold descriptors above nine perfectly well; they
	// simply have no way to *write* one, and `exec {v}>f` is what puts them
	// there — the shell picks 10 or 11 and the variable says which.
	//
	// The ash column was measured late and reads the other way from what
	// this comment used to claim: 2026-09-16, BusyBox v1.37.0, `exec
	// 10>f10; echo hi >&10` leaves three bytes in `f10`, while a bare `10`
	// is still a command that is not found. That dialect sets the flag
	// since #3238.
	//
	// It is the lexer's because the token is: digits immediately before `<`
	// or `>` are either a number or the tail of a word, and nothing later can
	// tell them apart. This is the case AmpersandRedirect's comment warns
	// about, in a milder form — where the flag is off, `echo x 10>f` is not
	// an error but a different command, printing `x 10` into the file rather
	// than `x` to the terminal.
	//
	// POSIX is the reason it is a flag rather than a fact: the standard's
	// IO_NUMBER is one *or more* digits, so a shell taking `10>f` conforms
	// and so does the majority that does not. Where the panel and the
	// standard disagree the panel decides the core, which leaves this to the
	// one shell that wants it.
	MultiDigitFdNumber bool

	// FdVariableSubscript lets the name inside those braces carry a
	// subscript: `exec {a[1]}>&-` closes the descriptor that element holds,
	// which is how a coprocess's feed is closed by the array the shell put
	// its near ends in.
	//
	// A flag of its own rather than a consequence of having both features,
	// because the shell that has both still refuses this one. Measured with
	// a descriptor parked on 3 and the element holding 3: bash 5.3 and ksh93
	// close it, and zsh reads `{a[1]}` as an ordinary word — as a *pattern*,
	// in fact, so it reports no matches, and with globbing turned off it
	// reports a command not found. Either way it never reached a redirection,
	// which is what makes this the construct's absence rather than a glob
	// getting in first. bash 3.2 and dash have no `{name}` token at all.
	//
	// So two of the three shells that have the parent feature have this one,
	// and the core is what all three agree on — which leaves it to the two
	// that answer yes.
	//
	// The subscript is taken as written and never expanded, because the
	// token is one literal: `{a[i]}` and `{a[i+1]}` are read where `{a[$i]}`
	// stays a word. bash takes that last spelling and ksh93 refuses it in
	// the arithmetic, so no answer here is everyone's.
	FdVariableSubscript bool

	// CloseQuotesAtEOF is the set of routes on which an unterminated `'`,
	// `"` or backquote ends at the end of input as if the closing mark were
	// there, instead of the parse being refused: `echo "abc` prints abc.
	// `$(` and `${` are not quotes and still refuse on every route.
	//
	// A set rather than a boolean because the one shell that answers yes
	// does not answer it for the whole shell. Measured 2026-09-07, ksh93u+
	// 2012-08-01, the same five lines by every route, `echo one` first so
	// that what ran before the quote is visible:
	//
	//	route                     ksh93                 other five
	//	-c string                 runs, status 0        refuse
	//	eval string               runs, status 0        refuse
	//	script file operand       `” unmatched`, 3     refuse
	//	`.` on a file             `” unmatched`, 3     refuse
	//	standard input, or a pipe `” unmatched`, 3     refuse
	//
	// So a boolean gets one of ksh93's routes right and four wrong, and the
	// four it gets wrong are the ones a script arrives by. A truncated file
	// then ran under our ksh and was refused by the real one, silently and
	// with status 0, and everything after the opening quote was discarded
	// without a word (#1424).
	//
	// It is not the trailing newline telling the routes apart, which is the
	// reading a `-c` string invites: measured the same day, `-c` closes the
	// quote whether or not the string ends in one, and a file refuses
	// whether or not it does. The route is the whole of it.
	//
	// A quote left open at a *prompt* is neither: every shell in the panel,
	// this one included, asks for more input with PS2 rather than closing
	// or refusing. The lexer marks the input incomplete before it asks this
	// at all, which is what leaves that answer to the front end.
	CloseQuotesAtEOF ProgramRoutes

	// BackquotedBodyIsACommandString reads the body of the older
	// substitution, “ `…` “, as a program handed over as a string — the
	// route [RouteFromCommandString] names — whatever route the text holding
	// it arrived by. So every rule asked of that route applies inside the
	// body: in the shell that has this, an unterminated quote closes at the
	// end of the body, which is [Dialect.CloseQuotesAtEOF].
	//
	// Measured 2026-10-04 on ksh93u+ 2012-08-01, each line a script file
	// followed by `echo B`:
	//
	//	written                       ksh93
	//	echo "`echo 'abc`"            abc, then B
	//	echo `echo "abc`              abc, then B
	//	echo "`echo \`echo n`"        n, then B
	//	echo `echo \`echo n`          n, then B
	//	echo $(echo "abc)             `"' unmatched at status 3, nothing run
	//
	// The last row is the control: the newer spelling's body is read with the
	// line and keeps the file's refusal. The older one is text the shell
	// reads again when it runs, and it reads it the way it reads `eval`'s
	// operand. The other five columns refuse every row.
	BackquotedBodyIsACommandString bool

	// BackslashAtEndOfInput is what an unquoted backslash the input ends
	// immediately after becomes. See [EndOfInputBackslash], where the rows
	// are.
	//
	// It is a word rule rather than a route rule, which is the difference
	// from [Dialect.CloseQuotesAtEOF] just above: a script file with no
	// trailing newline and a `-c` string answer identically in every column,
	// so nothing here asks how the program arrived. It is also not a
	// *refusal* — nothing in the panel refuses, and this shell did, which
	// ended the script at the line and discarded everything after it
	// (#2680). The older substitution is where that was found: its body is
	// unescaped and re-lexed, so “ `echo \\` “ hands the inner parse the
	// text `echo \`, and the refusal came back out as the whole line's.
	BackslashAtEndOfInput EndOfInputBackslash

	// ProgramRoute is which of those ways the program *now being parsed*
	// arrived, for the rules above that ask.
	//
	// Not a grammar rule and the only field here that is not: a dialect is
	// a shell's language and this is one program's provenance. It lives on
	// the dialect because the lexer is where the question is answered and
	// the dialect is the only thing the lexer is handed. The front end sets
	// it — see [Dialect.On] — and the zero value is no route at all, so a
	// parse that never said gets the strict answer.
	ProgramRoute ProgramRoutes

	// Comments is what a `#` where a word could begin means in the program
	// *now being parsed*. The zero value is [CommentsSkipped], the rule
	// every shell reads a script by.
	//
	// The second field here that is not a grammar rule, and it is here for
	// the reason ProgramRoute is: the lexer answers the question and the
	// dialect is the only thing the lexer is handed. Where ProgramRoute is
	// one program's provenance, this is one program's *reader* — the front
	// end setting it says that the text about to be parsed was typed at a
	// prompt by a shell whose option for that is off.
	//
	// No preset writes it, and it would be wrong for one to: the shell this
	// exists for reads a `#` in a script exactly as the others do, and
	// differs only on the line a person typed. Measured on zsh 5.9.2,
	// 2026-09-12, `-f -i` on a pipe with a scratch HOME, beside the same
	// shell's script routes:
	//
	//	route                          `echo a #b`
	//	typed at the prompt            a #b
	//	`-c`, with `-i` or without     a
	//	`eval` typed at the prompt     a
	//	`.` on a file, at the prompt   a
	//	standard input, not `-i`       a
	//
	// So it is neither the route nor the shell's interactivity on its own:
	// zsh reading the same descriptor answers both ways depending on which,
	// and a `-c` string is a comment in a shell that is interactive. What
	// decides it is the front end, which is where both facts meet, and
	// which is why this is set per parse rather than chosen by a preset.
	// See repl.Shell.CommentsNeedTheOption.
	//
	// [CommentsKept] is not a mode a program can be parsed under — a
	// comment arrives as a word and the grammar has nowhere to put it — so
	// a front end has the other two. See [ShellWords], which is where the
	// third belongs.
	Comments CommentMode

	// CharacterWidth is how many bytes of the pair b, next are one character
	// of the encoding the text is written in: 2 where the two spell one, 1
	// otherwise. nil, and every byte is a character of its own.
	//
	// A hook rather than an answer, and set per parse the way [Dialect.Comments]
	// is, because it is two things the grammar cannot know. One is the locale,
	// which is run state read off the shell's own variables — a plain
	// `LC_ALL=zh_TW.Big5` on one line decides how the next is read, so no
	// preset can hold it. The other is whether this shell's reader decodes the
	// encoding at all, which is measured and differs: see
	// interp.Semantics.MultibyteCharacterIsReadWhole, where the panel divides.
	//
	// Consulted before anything else in every loop that reads text a byte at a
	// time, so that no byte of a multibyte character is ever weighed as a
	// metacharacter. It has to be, because a trail byte can be one: in Big5
	// the character U+03B1 is `a3 5c`, and a reader that walks bytes finds a
	// backslash in the middle of a letter, escapes whatever follows, and
	// changes what the script says (#4235). Big5 trail bytes reach “ ` “,
	// `|`, `{`, `}`, `[` and `]` as well, so the defect is not the backslash's
	// alone and the fix is not the backslash's either: the character is taken
	// whole.
	//
	// nil is the zero value and the right one: a single-byte encoding, a UTF-8
	// one — whose trail bytes are all above 0x7F and so can hide nothing — and
	// a caller that has no locale to offer all mean the same thing to a reader,
	// which is one byte per character.
	CharacterWidth func(b, next byte) int

	// NameTakesALetterPastASCII says whether a character past ASCII may be
	// part of a name — of an assignment, of a parameter, of an arithmetic
	// variable — at any position in it. nil is the zero value and the
	// portable answer: a name is ASCII letters, digits and the underscore.
	//
	// A function for the reason CharacterWidth above is one: what counts is
	// the locale, which is a running shell's state and not the grammar's,
	// and in one dialect an option as well. The interpreter fills it in; see
	// interp.Semantics.NamesTakeTheLocalesLetters for what was measured.
	NameTakesALetterPastASCII func(c rune) bool

	// CharacterLength is how many bytes the locale's character at the front
	// of s takes, for the one place the grammar reads a *character* of any
	// length rather than a byte or a pair: a parameter flag's delimiter,
	// which may be `£` as well as `:`. nil means a byte, which is what a
	// caller with no locale to offer has. Filled in by the interpreter for
	// the reason CharacterWidth is.
	CharacterLength func(s string) int

	// UnmatchedBlamesTheOutermost names the *enclosing* construct when the
	// input runs out inside nested ones, where the default names the
	// innermost.
	//
	// Measured 2026-09-07, `-n` over a script file, with constructs nested
	// both ways round so that neither reading can pass for the other:
	//
	//	echo $( echo "hi          `"` in bash, dash and ksh93
	//	echo "${x:-"$( echo hi    `$(` in the same three
	//	echo $(( 1 + `echo 2      the backquote in the same three
	//	echo "$( echo hi          `$(` in the same three
	//
	// Three of the four name the innermost in every arrangement. zsh names
	// the outermost in all of them — `echo "$( echo hi` is `unmatched "`
	// there, about a quote the script did write, where bash is looking for a
	// `)`.
	//
	// It is answered by the *order* the reports arrive in rather than by
	// anything looking around: the scanners recurse, so the innermost to run
	// out reports first and the enclosing ones follow it outwards. Keeping
	// the first report is the default; this makes each replace the last.
	//
	// Two refinements, measured 2026-10-04 on zsh 5.9.2. A `${` does not
	// take the blame from a quote or backquote inside it — `echo ${x:-"a`
	// is `unmatched "` — though it does from a `$(` or a nested `${`. And
	// what a `$(`'s body refused travels out with the blame and is written
	// first: `echo "$(for` is `parse error near `for'` and then
	// `unmatched "`. See Lexer.replacesUnmatched and Error.BodyRefusal.
	UnmatchedBlamesTheOutermost bool

	// Coproc is `coproc command`: the command runs in the background with a
	// pipe on each of its named streams and the shell keeps the near ends.
	// bash and zsh both have the word; ksh93 spells a coprocess `cmd |&`,
	// which is a different construct and is CoprocPipeOperator, and dash has
	// none.
	//
	// How the shell reaches the ends is not this flag — bash puts them in an
	// array and zsh speaks to them with `print -p` and `read -p` — because
	// that is what a *running* coprocess offers rather than what the parser
	// reads. It is interp.Semantics.CoprocEndsInAnArray.
	Coproc bool

	// CoprocPipeOperator is ksh93's `cmd |&`: the same coprocess Coproc
	// starts, spelled as an **operator that terminates a command** rather
	// than as a word in front of one. There is no `coproc` keyword in that
	// shell and no name to give — the two bytes are the whole of the syntax.
	//
	// It is not a second reading of PipeBothStreams, and the measurement
	// that separates them is what may *follow* the operator rather than what
	// the command prints. Measured 2026-09-07, ksh93u+ 2012-08-01, `-n` over
	// a script file under `env -i`:
	//
	//	probe                  bash 5.3   ksh93     zsh
	//	echo one | ; echo two  error `;`  error `;` accepts
	//	echo one |& ; echo two error `;`  accepts   accepts
	//	echo one & ; echo two  error `;`  accepts   accepts
	//
	// A `|` needs a command after it and ksh93's `|&` does not, exactly as a
	// bare `&` does not. zsh accepts all three because it is lenient about
	// `;` after any control operator, so the ksh column alone decides it.
	// ksh93 also takes `echo one | & echo two` with a blank between, where
	// the other five refuse — another sign the two bytes are not that token.
	//
	// It terminates the whole **and-or**, the way `&` does rather than the
	// way a pipe binds: `echo A && cat |&` puts `echo A`'s output into the
	// coprocess pipe, which a later `read -p` answers with `A`. Measured the
	// same day, and the same for `echo A | cat |&`.
	//
	// Where a dialect had both this and PipeBothStreams the pipe reading
	// would win, because parsePipeline asks first. No preset does: the two
	// are the two readings of one spelling and a dialect has one of them.
	CoprocPipeOperator bool

	// CoprocName lets a *name* stand between `coproc` and a compound
	// command, and it is bash's alone: `coproc MY { cat; }` puts the near
	// ends in MY there, and is a parse error in zsh, whose coprocess has no
	// name to give. Additive over Coproc, and asked only where that is set.
	//
	// The name is a name only before a *compound* command in the shell that
	// has it: before a simple one the first word is the command, so
	// `coproc MY cat` runs `MY cat` in both shells, and both leave whatever
	// the reader asks for afterwards unset.
	CoprocName bool

	// CoprocAfterABar lets `coproc` begin a command after a pipe bar, the way
	// any other command may. Additive over Coproc, and asked only where that
	// is set: a dialect without the word reads `coproc` as a command name
	// wherever it stands. Blair's decision on #5273: a field of its own.
	//
	// Measured 2026-10-01, as a script file and under `-c`:
	//
	//	probe                          bash 5.3.20   zsh 5.9.2
	//	echo | coproc true             runs          parse error near `coproc'
	//	echo hi | coproc { cat; }      runs          parse error
	//	echo |& coproc cat             runs          parse error
	//	a bar, a newline, then coproc  runs          parse error, on line 2
	//	inside an `if` condition       runs          parse error
	//	coproc true | cat              runs          runs
	//	echo | "coproc" true           command name  command name
	//	echo | { coproc cat; }         runs          runs
	//
	// So zsh refuses the bare word where a later element of a pipeline
	// begins, and nowhere else: the first element may be one, a quoted
	// spelling is a command name, and a group after the bar begins a list of
	// its own. An alias named `coproc` does not reach it — zsh still refuses
	// after `alias coproc=echo` — so the word is refused before the alias
	// table is asked.
	CoprocAfterABar bool
}

Dialect says which constructs the lexer accepts.

Fields are named for the construct rather than for the shell that wants it, which docs/spec/semantics.md requires and which the measurements insist on: ksh93 accepts `&>` or does not depending on which build is installed, twelve years apart under the same name, so a field called `Ksh` could not be given a value. A field called Dialect.AmpersandRedirect can.

Grammar differences are additive — a construct either parses or it does not — which is why this is a set of flags. Semantic differences, where the same syntax means different things, are not additive and do not belong here; they are the interpreter's problem and get their own vector.

func Core

func Core() Dialect

Core is the common denominator of real shells: what dash, bash, ksh93 and zsh agree on, minus dash, whose absence is the decision recorded in docs/spec/core.md.

CaseContinue is off because it is bash-only. Everything else here is something every non-dash shell in the reference panel accepts.

func POSIX

func POSIX() Dialect

POSIX is the specification's shell language and nothing else. It is deliberately narrower than any shell anyone actually runs, which makes it the right setting for a portability check and the wrong one for a runtime.

func (Dialect) On

func (d Dialect) On(route ProgramRoutes) Dialect

On returns this dialect set to parse a program that arrived by route.

A copy, so that the shell's own dialect keeps saying what the *language* is and each parse says how its text got here. One expression at the call site is the whole point: the route has to be attached at every place a program is read, and a step that is easy to leave out is one that gets left out.

func (Dialect) Reserves added in v0.0.3

func (d Dialect) Reserves(name string) bool

Reserves reports whether name is part of *this* dialect's grammar rather than a name a command could have.

It exists because the question is asked from outside the parser. A shell's `command -v` and `type` answer "reserved word" for a name the grammar claims, and an interpreter holding a written-out list of them answers for a grammar that is not the one it is running: ours told a dash script that `[[`, `]]`, `select` and `function` were runnable words, and answered `time` with the keyword where dash has none and a script reaching for `command -v time` wants the path of `/usr/bin/time` (#2918). The list is here, beside the flags that decide it, so there is one of it.

Measured 2026-09-15 with `command -v` and with `type`, on bash 5.3.15, zsh 5.9.2, ksh93 93u+, dash 0.5.12 and BusyBox ash: every shell claims exactly the constructs it has, and a word it does not have falls through to the ordinary search — which is what makes `command -v time` a path in dash and a keyword everywhere else.

Two rows of that measurement are **not** the grammar, and are deliberately not here: `]]` is named by bash alone among the four shells that have the construct, and `in` is named by everyone but zsh. Both shells *have* what the word is part of and decline to report it, so the answer is a fact about the report rather than about what parses. See #2981.

Two of its members are spelled as operators rather than words and so are not in [reservedWords] at all. `[[` and `]]` are the conditional command's two ends, and `coproc` opens a construct a dialect either has or does not; each is claimed by the same flag the grammar reads.

type DollarForms added in v0.0.8

type DollarForms uint8

DollarForms is a set of the forms a `$` introduces — what the character behind it says the construct is.

A set rather than a count, because the two shells that narrow it narrow it to sets neither of which contains the other: see Dialect.ContinuationStopsADollarAt, which is the one question that asks it.

const (
	// DollarBareParameter is `$x`, `$1`, `$#`, `$@` — a name, a positional
	// or a special parameter written with no delimiter of its own.
	DollarBareParameter DollarForms = 1 << iota
	// DollarBraces is `${…}`, every operator inside it included.
	DollarBraces
	// DollarParens is `$(…)` and `$((…))`, which the character after the
	// first parenthesis tells apart long after this question is settled.
	DollarParens
	// DollarBrackets is `$[…]`, the older arithmetic spelling, where
	// [Dialect.DollarBracketArith] gives the dialect the form at all.
	DollarBrackets
	// DollarQuotes is `$'…'` and `$"…"`, which are quoting rather than
	// expansion but are introduced by the same character.
	DollarQuotes

	// NoDollarForm is the empty set, spelled so a dialect can say it
	// deliberately rather than by leaving a field out.
	NoDollarForm DollarForms = 0
	// EveryDollarForm is all five.
	EveryDollarForm = DollarBareParameter | DollarBraces | DollarParens | DollarBrackets | DollarQuotes
)

func (DollarForms) Has added in v0.0.8

func (f DollarForms) Has(form DollarForms) bool

Has reports whether form is in the set. A caller asks with exactly one form, which is what it knows.

type Elif

type Elif struct {
	Cond  []*Stmt
	Then  []*Stmt
	Start Pos
}

Elif is one `elif … then …` in a chain.

func (*Elif) End

func (e *Elif) End() Pos

func (*Elif) Pos

func (e *Elif) Pos() Pos

type EmptyBodyBlame added in v0.0.2

type EmptyBodyBlame uint8

EmptyBodyBlame is which token a refusal names when a terminator stands where a body or a condition must have something in it. See Dialect.EmptyBodyBlame for the measured table.

const (
	// BlameTheTerminatorItself names the terminator where it stands, which
	// is what dash and bash do and what the substrate answers with nothing
	// said.
	BlameTheTerminatorItself EmptyBodyBlame = iota
	// BlameTheTokenAfterIt names whatever stands next, one token on, with no
	// separators stepped over. ksh93 for `&`: `if & then` is `then`, `if & ;
	// then` is `;` and `{ & }` is `}` — the token after it whatever kind it
	// is.
	//
	// A newline is the exception and it is an acceptance rather than a
	// wording: `if &` with the `then` on the next line *runs* there, so
	// there is no refusal to name. This names the terminator, which is what
	// this shell already wrote.
	BlameTheTokenAfterIt
	// BlameTheKeywordAfterIt names a reserved word, and how far it looks for
	// one depends on which list the terminator stood in. zsh for `|`, `|&`,
	// `&&` and `||`, measured 2026-09-14:
	//
	//	if | :; then :; fi        `then`   a condition, read to its keyword
	//	if | :; :; then :; fi     `then`   however much stands in between
	//	if | { :; }; then :; fi   `then`   a `}` on the way is not it
	//	if | ; then :; fi         `then`
	//	if | :; fi                `fi`     the construct's own closer
	//	while | :; done           `done`
	//	until | :; do :; done     `do`
	//	if :; then | fi           `fi`     a body, and the keyword is next
	//	for i in 1; do | :; done  `|`      a body, and a command is next
	//	{ | }                     `|`      the closing brace is not named
	//	( | )                     `|`
	//	case x in x) | ;; esac    `|`
	//
	// So a **condition** is named at the reserved word that ends its header,
	// read forward to; a **body** is named at the reserved word standing
	// next, and at the terminator otherwise. The closing brace is the one
	// reserved word never named — measured rather than assumed, and what
	// parts this from naming any stop word.
	//
	// One measured row this does not reach: `if | || :; then :; fi` is `||`
	// there, which is neither a reserved word nor the terminator. It is the
	// shell's own recovery showing through, it is what this shell already
	// wrote, and a rule built to catch it would have to name an operator in
	// a position where `if | ; then :; fi` names the keyword.
	BlameTheKeywordAfterIt
)

type EndOfInputBackslash added in v0.0.1

type EndOfInputBackslash uint8

EndOfInputBackslash is what an unquoted backslash the input ends immediately after becomes — a line continuation with no line to continue. See Dialect.BackslashAtEndOfInput.

No shell refuses it, which is the half of this with no axis in it: all eight columns run the line and exit 0. What the backslash *becomes* is where they part, and they part three ways among the dialects, so a bool could be given a value for neither ksh93 nor the pair it sits between.

Measured 2026-09-13 over `-c`, and over a script file with no trailing newline, which answer alike. A file that *ends* in a newline is a different question with one answer: the backslash is then an ordinary continuation, and every column drops it and prints `[x]` and `[]`.

written            bash 5.3  bash as sh  bash 3.2  dash    ksh93   zsh    zsh as sh  ash
printf "[%s]" x\   [x\]      [x\]        [x]       [x\]    [x]     [x]    [x]        [x\]
printf "[%s]" \    [\]       [\]         []        [\]     [\]     []     []         [\]
printf "[%s]" a \  [a][\]    [a][\]      [a]       [a][\]  [a][\]  [a][]  [a][]      [a][\]

The third row reuses one conversion rather than writing two, and that is the only spelling that can see the fourth reading: `printf "[%s][%s]" a \` prints `[a][]` in bash 3.2 as well as in zsh, because a format with two conversions fills a missing operand with the empty string and so cannot tell a word that is empty from a word that is gone. Reused, the format is printed once per operand, so `[a]` alone says the operand is not there.

bash 3.2 holds that fourth reading and it is recorded rather than given a value: it drops the *word* along with the backslash, where zsh keeps an empty one — `[a]` against `[a][]` on the third row. No dialect preset is bash 3.2, so inventing a value for it would put a reading in the vector that nothing could ask for. The row it is visible in is in the corpus, so docs/spec/measurements.md carries the column.

const (
	// EndOfInputBackslashIsLiteral keeps it: the word ends with a protected
	// backslash, wherever in the word it stood. bash 5.3, bash as `sh`,
	// dash and BusyBox ash, and the core answer because it is what four of
	// the six columns that are a dialect do and because it is the reading
	// that loses nothing — the text the writer typed is still in the field.
	EndOfInputBackslashIsLiteral EndOfInputBackslash = iota

	// EndOfInputBackslashIsDropped removes it and keeps the word, which is
	// an empty field when the backslash was all of it. zsh 5.9.2, as itself
	// and as `sh`.
	EndOfInputBackslashIsDropped

	// EndOfInputBackslashIsLiteralOnlyAtAWordStart keeps it where the
	// backslash is the first thing in the word and drops it anywhere else.
	// ksh93u+, and measured rather than guessed at: `printf "[%s]" "x"\`
	// and `printf "[%s]" 'q'\` are both `[x]`/`[q]` there, so it is having
	// read *anything* into the word that decides it and not the character
	// in front of the backslash. `printf "[%s]" \\\` is `[\]` — the escaped
	// pair is read first, so the trailing one is no longer at a start.
	EndOfInputBackslashIsLiteralOnlyAtAWordStart
)

type Error

type Error struct {
	Pos  Pos
	Kind ErrorKind
	Msg  string

	// Construct is the compound command left open — `if`, `for`, `case`,
	// `{`, `(` — and ConstructLine is the line it began on. One shell names
	// both.
	Construct     string
	ConstructLine int
	// Innermost is the unclosed keyword nearest the failure, which is the
	// construct itself until a clause of it has been entered: `if` alone is
	// `if`, and `if cond; then` is `then`. Another shell names this instead.
	Innermost string
	// Expected is the word that would have closed it — `fi`, `done`, `esac`.
	Expected string
	// ExpectedIsAClass says Expected names a *class* of token rather than
	// one spelling: a word of any spelling, where `fi` is one word and no
	// other.
	//
	// It is read by the two dialects that print what they expected, and
	// they part the two the same way they part a refused token from a
	// refused class — a spelling is quoted and a class is bare. Measured
	// 2026-09-19, script file under `env -i PATH=/usr/bin:/bin LC_ALL=C`
	// with standard input on the null device: dash 0.5.12 answers `for in
	// x; do :; done` with `word unexpected (expecting "do")` and `case ; in
	// x) ;; esac` with `";" unexpected (expecting word)`, and BusyBox ash
	// 1.37.0 the same both times in its own word order. See
	// Diagnostics.SyntaxExpectingClass.
	ExpectedIsAClass bool
	// LastToken is the last token consumed before the input ran out, which
	// is what the remaining shell names.
	LastToken string
	// CondWords are the words of the `[[ ]]` condition the parser could not
	// read — the whole innermost group, from where it began to its closer,
	// as each word was **written**.
	//
	// One dialect's refusal is about the group rather than about the token it
	// stopped on: it names a word chosen by how many there are, and picks
	// between two sentences by what the second of them looks like. See
	// Diagnostics.ConditionExpected, which is the only reader.
	//
	// Set only where the group began with a plain word, which is the form
	// that refusal is about: `[[ -n -z x ]]` is a one-operand test with an
	// operator for an operand and is refused by naming the token, so nothing
	// is recorded for it.
	CondWords []string
	// CondTermMissing says the refused token stood where a condition was to
	// **begin** — after the `[[` itself, after a `!`, after a connective, or
	// just inside a group — rather than anywhere a condition had already
	// been read.
	//
	// One dialect words the two apart and one of its wordings is *nothing at
	// all*. See Diagnostics.CondCommandPreamble for the measurement.
	CondTermMissing bool

	// CondTermUndecided says the refused token is the newline behind a
	// condition term whose first word has been read and whose shape is not
	// yet settled — a binary operator may still follow it. One dialect words
	// that position as a statement about the operator it was still waiting
	// for rather than as a token the grammar did not want:
	// `unexpected token `newline', conditional binary operator expected`.
	// See [Dialect.ConditionNewlineMayFollowATermsFirstWord].
	CondTermUndecided bool
	// CondGroupsOpen is how many `(` of the condition had been entered and
	// not closed when it failed. The same dialect writes one line per open
	// group in front of the rest.
	CondGroupsOpen int

	// CondGroupCloserWanted says the refusal happened where a condition
	// group's `)` was wanted rather than inside one of its terms. One
	// dialect names that closer in the same sentence as the token it did not
	// get, and counts the group as reported rather than as still open:
	//
	//	[[ ((1 -eq 1) ]]   unexpected token `]]', expected `)'
	//	[[ ( -n x ; ]]     unexpected token `;', expected `)'
	//	[[ ( -t X          unexpected token `EOF', expected `)'
	//	[[ ( x & ) ]]      the `&' stood behind a one-word term instead, so
	//	                   the sentence is CondTermUndecided's and the group
	//	                   is listed under CondGroupsOpen
	//
	// Measured on bash 5.3.20, 2026-09-22. See
	// Diagnostics.CondGroupCloserExpected.
	CondGroupCloserWanted bool

	// RanOutInsideACommandSubstitution says the input ran out with a `$( … )`
	// still open around the construct this failure names.
	//
	// Read where a refusal is *located* rather than where it is worded: a
	// `${ … }` operand inside double quotes is read a second time, and a `'`
	// that quoted in the first read quotes nothing in the second, so the
	// second read can open a substitution the line's own read never saw. The
	// shell reaches that read when the word expands — the line is behind it
	// by then — so one column names the route and counts the line
	// differently for it.
	//
	// **The construct and not the token**: `"${v+'$('}"` and `"${v+'bar}"`
	// both run out on a `'`, and only the first ran out inside a
	// substitution. A rule reading the token alone cannot tell them apart
	// (#4201).
	RanOutInsideACommandSubstitution bool

	// RanOutOnACommandSubstitution says the construct that ran out **is**
	// that substitution rather than something written inside it.
	//
	// The pair is read together and only where the failure is located: a
	// text handed to a reader that runs out is named on the line *after* it
	// in one column, and a substitution's body is one more such text, so a
	// quote left open inside a body and the body itself are one line apart.
	// Measured over five shapes apiece — `"${v+'$('}"` names the quote and
	// `"${v+'$('\'}"`, where the escaped quote closes it again, names the
	// substitution two lines below the command instead of one (#4201).
	RanOutOnACommandSubstitution bool
	// Class is what sort of token Token is, when the kind is ErrUnexpected.
	Class TokenClass
	// TokenOpener is the operator the unexpected token *began* with, where
	// the token is a construct whose text is not its own spelling. Empty for
	// everything else, which is nearly everything.
	//
	// One construct needs it. An arithmetic command standing where the
	// grammar has no command is named two ways in the panel — measured
	// 2026-09-11 on a script holding `x=1` and `(( 1 )) (( 2 ))`:
	//
	//	bash 5.3   syntax error near unexpected token ` 2 '
	//	zsh 5.9.2  parse error near ` 2 '
	//	ksh93u+    syntax error at line 2: `((' unexpected
	//	bash 3.2   syntax error near unexpected token `('
	//
	// So two of them quote the expression the construct held, blanks and
	// all, and one quotes the operator that opened it. Token cannot say
	// both, and which is written is the dialect's answer rather than the
	// parser's — see Diagnostics.SyntaxUnexpectedNamesTheOpener.
	TokenOpener string
	// TokenSource is the unexpected token as it was **written** — quotes,
	// backslashes and the text of an expansion included — where Token is
	// what the word comes to once the quoting is off.
	//
	// Two spellings because the panel splits three ways over the same word,
	// measured 2026-09-12 on a script holding `if true; then echo t; fi W`:
	//
	//	W          bash 5.3 / bash32 / bash-as-sh / zsh   ksh93
	//	"zzz"      `"zzz"`                                `zzz`
	//	'a b'      `'a b'`                                `a b`
	//	a\"b       `a\"b`                                 `a"b`
	//	$x         `$x`                                   `$x`
	//	"$x"       `"$x"`                                 `"$x"`
	//
	// so bash and zsh echo the source always, ksh93 echoes it only where the
	// word holds an expansion, and dash names no word at all. Which of the
	// two is written is the dialect's answer rather than the parser's — see
	// Diagnostics.UnexpectedWordNaming — so both travel, the way
	// TokenOpener does.
	//
	// Set for a word token and empty for everything else, whose source and
	// whose spelling are the same characters.
	TokenSource string
	// CondReadText is, for a token refused inside `[[ ]]`, the source text
	// the reader had taken in when it stopped: the run ending one byte past
	// the token, blanks behind it dropped, back to the nearest blank,
	// newline, `;`, `|` or `&`. `(a` for the `(` of `(ab)`, `;` for the `]]`
	// of `]];`. One dialect names that rather than the token — see
	// Diagnostics.CondSyntaxNamesTheTextRead and conditions.md.
	//
	// Empty outside a condition, and where the token came out of an alias,
	// whose positions do not index the input.
	CondReadText string

	// AliasSource is the alias body the failure was inside, where an alias
	// put the offending token there, and empty otherwise.
	//
	// It exists because the shell that echoes the offending line back echoes
	// **the alias's text and not the line the script wrote**. Measured on
	// bash 5.3.15, 2026-09-12, with `alias f='a= (x y)'` and three different
	// lines using it:
	//
	//	echo hi; f; echo bye    `a= (x y)'
	//	f arg1 arg2             `a= (x y)'
	//	f; g                    `a= (x y)'
	//
	// So it is the borrowed text alone rather than the line reconstructed
	// with the expansion in it — nothing the script wrote around the alias
	// word appears. Slicing the source for the failure's line cannot produce
	// that, and a spliced token carries the position of the *word it
	// replaced*, so the slice gives the line holding the alias word back
	// (#2413).
	AliasSource string
	// TokenHoldsExpansion says the unexpected word carried an expansion —
	// `$x`, `${x}`, `$(…)`, “ `…` “ or `$((…))`. One dialect reads it as
	// the question of which of the two spellings above to write.
	TokenHoldsExpansion bool

	// Redirect says the unexpected token was itself a redirection operator.
	//
	// One dialect words that case separately — dash says `redirection
	// unexpected` where the other three name the token — so the fact has to
	// travel with the error rather than being worked out from the text.
	Redirect bool
	// FuncBody says the grammar was waiting for a function's body, and that
	// the body never began — `f() ;` and `f()` rather than `f() {` with the
	// input running out inside the braces.
	//
	// One dialect locates this one failure by counting from the parentheses
	// rather than from the top of the file, which is what FuncBodyLines
	// carries and what says the fact has to travel with the error the way
	// Redirect does rather than being worked out from the text.
	FuncBody bool
	// FuncBodyLines is how many lines stand between the parentheses a
	// function body was due after and the failure that says it never came.
	// Zero where the two are on one line, which is the answer the dialect
	// below writes as no line at all.
	//
	// **One dialect numbers this failure from the parentheses**, and that is
	// measured rather than derived from the absent line the earlier reading
	// modeled. Measured 2026-09-21 on zsh 5.9.2 from a script file, `env -i
	// PATH=/usr/bin:/bin LC_ALL=C zsh -f s.sh` with standard input on the
	// null device:
	//
	//	`f() ;`                      zsh: parse error near `;'      — no line
	//	`foo()` and a newline        s.sh:1: parse error near `\n'
	//	`foo()` and two blank lines  s.sh:3: parse error near `\n'
	//	`: a` ⏎ `: b` ⏎ `foo()`      s.sh:1: parse error near `\n'
	//
	// The last row is the discriminator: the parentheses are on line 3 of
	// the file and the failure is still `1`, so the number is a distance and
	// not a position. `if true` is the control — an input that ran out the
	// same way, reported at the file's own line — which is what says this is
	// the function body's rule and not an end-of-input one.
	//
	// The line was simply dropped here before (#3961), on the reading that
	// zsh omits it for this failure. That reading is this one with the
	// distance nought, and it left `foo()` alone on line 1 of a file
	// reported at line 2.
	FuncBodyLines int
	// Expr is the whole arithmetic expression a failure was inside, and
	// Token the part of it the failure is attributed to. Every shell quotes
	// the first; only one names the second.
	Expr  string
	Token string
	// EndLine is the line *after* the input's last, which is where one shell
	// considers the end of input to be: `eval "if"` is line 2 there and line
	// 1 in the other three. Input that already ends in a newline puts Pos on
	// that line anyway, so the two agree there and differ only when the text
	// stops mid-line.
	EndLine int
	// HoldsProgram says the construct the input ran out inside holds a
	// *program* rather than a parameter or an expression. Only one opener is
	// written the same way for both — `${x}` and `${ cmd;}` — so it is the
	// only construct that needs the fact carried rather than read off Token.
	//
	// One dialect blames the two forms at different lines. Measured
	// 2026-09-12 on bash 5.3.15 over a two-line file:
	//
	//	echo ${ echo hi   line 3 — the line after the input's last
	//	echo ${x          line 1 — the line the `${` is on
	//
	// which is the same split it draws between `$( )` and `$(( ))`, and the
	// reason ParseFailureLine already keys the convention on the opener. The
	// command form is a fifth member of the set that holds a program, and
	// the brace is what hides it (#1425).
	HoldsProgram bool
	// BodyRefusal is what the program between the delimiters had to say for
	// itself when it was read on its own, for a construct that holds one and
	// never closed. Nil where the body read to the end of the input without
	// complaint, and nil for every construct that holds no program.
	//
	// It exists because one dialect writes it. A `$( … )` that never closes
	// is two messages there and one here, and the first of the two is
	// *exactly* what the body answers as a program of its own — the same
	// sentence at the same line. Measured 2026-09-21 on zsh 5.9.2 from a
	// script file, `env -i PATH=/usr/bin:/bin LC_ALL=C zsh -f s.sh` with
	// standard input on the null device, the body written twice: once as
	// `v=$(echo hi; X` with no closer at all and once as a script of its own:
	//
	//	X                 as a program of its own      inside `v=$(echo hi; `
	//	`for`             s.sh:2: … near `\n'          s.sh:2: … near `\n'
	//	`{`               s.sh:2: … near `\n'          s.sh:2: … near `\n'
	//	`if true`         s.sh:2: … near `\n'          s.sh:2: … near `\n'
	//	`case x in y)`    s.sh:2: … near `\n'          s.sh:2: … near `\n'
	//	`()`              s.sh:1: … near `\n'          s.sh:1: … near `\n'
	//	`foo()`           s.sh:1: … near `\n'          s.sh:1: … near `\n'
	//
	// The last two rows are what says the line is the body's own and not the
	// end of the input: both are a function body that never came, which that
	// dialect numbers from its parentheses — see FuncBodyLines.
	//
	// The discriminator is a body that *parses*: `v=$(echo hi` with no
	// closer writes the quote alone there, so this is the body's refusal
	// where the body has one rather than a second line on every unterminated
	// substitution.
	//
	// Carried rather than re-derived, because the read that found it has
	// already happened: [Lexer.parseToClose] is a real parse of the rest of
	// the input, and its refusal was being thrown away.
	BodyRefusal *Error
	// BraceNameStop is the token that stood where an unterminated `${…}` in
	// the *parameter* form could read no further — `newline` for a newline,
	// and the character itself for a space or a tab, spelled the way
	// [Kind.String] spells a token. Empty where the input simply ended, where
	// an operator had already been read and the rest of the braces is a word,
	// or where the braces held a program.
	//
	// Two dialects answer an unterminated `${x` differently from an
	// unterminated `${x:-a}`, and neither difference can be read off the
	// opener: both are `${`. Measured 2026-09-12 over a two-line file,
	// `-n`, `env -i` with a scratch HOME:
	//
	//	echo ${x        ksh93 “syntax error at line 1: `newline' unexpected“
	//	echo ${x        dash  `2: Syntax error: Missing '}'` — a line early
	//	echo ${x:-a     ksh93 “syntax error at line 1: `{' unmatched“
	//	echo ${x:-a     dash  `3: Syntax error: Missing '}'`
	//	echo ${x        ksh93 “syntax error at line 1: `' unexpected“ — the
	//	                space, for a name a space stopped
	//
	// A `${` whose name never began takes it too: `echo ${` and a newline is
	// dash's line-early answer as well, so the stop is about what stood
	// where the expansion stopped rather than about a name being there.
	BraceNameStop string
	// BraceNameStopFollowsAPrefix says that stop came after a `#`, `##` or
	// `!` written in front of the name, or after an `@` the expansion was
	// still reading an operator letter for, rather than after a bare
	// parameter name.
	//
	// The two dialects that read BraceNameStop want different sets, which is
	// why the fact is carried apart from the stop rather than folded into it.
	// Measured beside the rows above: `echo ${#x` is dash's *ordinary* line
	// — no line early — where `echo ${#` is a line early, because a bare `#`
	// is the parameter and a `#` in front of a name is the length operator.
	// ksh93 says “newline' unexpected“ for both.
	BraceNameStopFollowsAPrefix bool
	// BraceRanOutInTheName says an unterminated `${…}` in the parameter form
	// stopped before it had read an operator: `${`, `${x`, `${#x`, with flags
	// `${(e)x`, and a name followed by anything that is not an operator —
	// `${x b`, `${x.`, `${x;` — where `${x:-` has read one and is reading a
	// word. Measured 2026-10-02 on zsh 5.9.2 through `(e)`: after the name,
	// `/ # % - = + ? :` are `closing brace expected` and every other
	// character tried is `bad substitution`; a `[` is the subscript's own
	// refusal. One dialect words the two apart where the text is read at
	// expansion time, and not on the command line. See
	// interp.Diagnostics.RawTextBraceRanOutInTheName.
	BraceRanOutInTheName bool
	// BraceParamFormUnclosed says the refusal is an unterminated `${…}` in
	// the parameter form — the one BraceNameStop and BraceRanOutInTheName
	// describe — and not some construct inside one. A here-document body
	// read at expansion time reports this one at the command's line rather
	// than as a sub-parse of the body. See
	// interp.Diagnostics.HeredocBraceRanOutInTheName.
	BraceParamFormUnclosed bool
	// FlagGroupWordTail is the rest of the *word* a refused expansion flag
	// group stands in: the source from the character after the group's
	// closing `)` to the end of the word, the expansion's own `}` and any
	// text written after it included.
	//
	// One dialect quotes that text back instead of the `(` it refused.
	// Measured on ksh93u+ 2012-08-01, 2026-09-12, `env -i` with a scratch
	// HOME, `-c`:
	//
	//	echo ${(U)x}                  x}                 the brace as well
	//	echo ${(U)x} after            x}                 and no further
	//	echo a${(U)x}b c              x}b                through the literal
	//	echo ${(@f)"$(printf ab)"}    "$(printf ab)"}    quotes and all
	//
	// Row two is the discriminating one: to the end of the *line* would have
	// carried ` after` with it, and the expansion alone would have carried
	// nothing in rows three and four.
	//
	// It is the flag group alone and not every `${…}` that dialect refuses:
	// `echo ${~x} after`, `${=x}`, `${^x}` and `${+x}` each name the one
	// character, measured in the same run.
	//
	// Two neighbors are measured and *not* modeled. Quotes are dropped from
	// the tail — `${(U)"x"}` gives `x}` and `"[${(U)x}]"` gives `x}]` —
	// which is what flagGroupTail does, but only where the tail holds no
	// expansion: with one in it the shell keeps the quotes it was written
	// with and then writes a second `"` where the word's closing quote
	// stood, so `"${(U)x}$w"` gives `x}$w""` where this writes `x}$w"`. And
	// a flag group holding a nested `(` — `${(l(3))x}` — goes back to
	// naming the `(`, which is where the `)` search stops.
	//
	// Empty where the word could not be recovered, for the reasons
	// ParamExpr.FlagsErrTail is: the report then names the `(` as it always
	// did.
	FlagGroupWordTail string
	// NestedInTheNamePosition says the refused `${…}` opened with a second
	// `${` where its parameter's name belonged — `${${v}}`, `${#${v}}`,
	// `${${v}[2]}` and `${${v}:-x}` are all this shape, and a `${` with
	// anything else after it is not.
	//
	// Carried because one dialect names a token that is in no part of the
	// input for it. Measured 2026-09-14 on ksh93u+ 2012-08-01, which has no
	// nested expansions:
	//
	//	echo ${${v}}        syntax error at line 1: `!' unexpected
	//	echo "${${v}}"      the same
	//	echo ${#${v}}       the same
	//	echo ${${v}[2]}     the same
	//	echo ${${}}         the same
	//	echo ${$v}          ${$v}: bad substitution — a `$` alone is not it
	//	echo ${$$}          `$' unexpected
	//	echo ${x${v}}       `$' unexpected — the `${` has to open the name
	//
	// The last three are what make this the *pair* of characters rather than
	// a `$` in the name position or a `${` anywhere in the expansion. See
	// interp.Diagnostics.NestedNameIsBlamedOnTheBang, which is what reads
	// it; every other dialect leaves the token the character it really was.
	NestedInTheNamePosition bool
	// EofLine is the line the input actually ran out on, in the lexer's
	// own count — the same point EndLine names in the next-line
	// convention. Two dialects report this one for an unmatched quote.
	EofLine int
	// CommentRanOut says the input ended inside a comment with no newline
	// of its own, in the dialect that counts that comment as ending a line —
	// see [Dialect.CommentRunningOutCountsANewline]. Read where the refusal
	// is worded as the end of the input being unexpected.
	CommentRanOut bool
	// contains filtered or unexported fields
}

Error is a parse failure with its position, kind, and enough of the state it failed in that a dialect can word it its own way.

The context fields are set where the parser has them and are empty otherwise. A caller that only wants a sentence still has Msg.

func (*Error) Error

func (e *Error) Error() string

type ErrorKind

type ErrorKind int

ErrorKind classifies a parse failure.

It exists so a dialect can word the failure its own way without matching on message text. The set is small on purpose: it distinguishes only the cases the panel *words differently*, not every way a parse can fail. dash says "Bad substitution" for anything wrong inside `${ }` and "Syntax error: …" for everything else, so those are the two.

const (
	// ErrSyntax is a parse failure with no more specific classification.
	ErrSyntax ErrorKind = iota
	// ErrBadSubstitution is a parse failure inside `${ }`. Every shell in
	// the panel has a distinct message for it, and none of them mentions
	// which operator was wrong.
	ErrBadSubstitution
	// ErrUnterminated is input that ran out with a construct still open.
	//
	// It is its own kind because the panel does not merely word it
	// differently — each shell names a *different part* of the same failure.
	// For `if true; then echo x`:
	//
	//	bash   unexpected end of file from `if' command on line 1
	//	dash   end of file unexpected (expecting "fi")
	//	ksh93  `then' unmatched
	//	zsh    parse error near `x'
	//
	// Four views of one state rather than four states, which is why Error
	// carries the construct, the innermost unclosed keyword, what was
	// expected and the last token seen: each dialect names the one it names.
	ErrUnterminated
	// ErrArithOperand is an arithmetic expression that wanted a value and
	// found something that could not be one: `$((%))`, `$((1+&2))`. Every
	// shell in the panel words this as an *arithmetic* failure rather than as
	// a syntax error, which is why it is its own kind — dash calls it
	// "expecting primary" and bash "operand expected", both inside the shape
	// they use for a division by zero.
	//
	// Token is the text from the refused byte to the end of the expression,
	// which is what the two shells that name anything here name.
	ErrArithOperand
	// ErrArithOperandEnd is an arithmetic expression that wanted a value and
	// ran out of text instead: `$((1+))`, `$((~))`.
	//
	// A separate kind because two of the panel word the two apart, and they
	// are the two that say the least otherwise: ksh93 has "more tokens
	// expected" against "arithmetic syntax error", and zsh "operand expected
	// at end of string" against "operand expected at `%'". bash words both
	// identically, which is why nothing had noticed — it is the column a
	// conformance number is usually read against.
	//
	// Token is the operator that was left wanting, which is what bash names.
	ErrArithOperandEnd
	// ErrArithOperator is an expression with something left over that could
	// have been an operand: `$((1 2))`, and in dash also `$((1,2))`, whose
	// comma it does not have. dash calls it "expecting EOF" and zsh
	// "operator expected".
	ErrArithOperator
	// ErrArithBadOperator is text where an operator belonged that could not
	// be one at all — `1 @`, or `1.5` in a dialect without floats, where the
	// `.5` is neither an operator nor part of the number.
	//
	// A separate kind because one shell words the two differently: bash says
	// "arithmetic syntax error in expression" when an operand stands where an
	// operator belonged and "invalid arithmetic operator" when the text could
	// not be either. The other three have one wording for both.
	ErrArithBadOperator
	// ErrArithConditionalThen is `? ` with nothing after it to be the value
	// the condition chooses when it is true: `$(( 1 ? ))`.
	//
	// Its own kind because two of the four shells word it apart from an
	// ordinary missing operand — bash "expression expected" and ksh93
	// "':' expected for '?' operator", against their own "operand expected"
	// and "more tokens expected" — while zsh and dash reuse theirs. Token is
	// the `?` and everything after it, which is what bash names.
	ErrArithConditionalThen
	// ErrArithConditionalColon is a conditional whose two values are not
	// parted by a `:`: `$(( 1 ? 2 ))`. All four word it, and each its own
	// way. Token is the value that was read and everything after it.
	ErrArithConditionalColon
	// ErrArithConditionalElse is `:` with nothing after it to be the value
	// the condition chooses when it is false: `$(( 1 ? 2 : ))`.
	//
	// Apart from ErrArithConditionalThen because ksh93 parts the two — its
	// then is the colon complaint and its else the ordinary end of input —
	// and apart from ErrArithOperandEnd because bash parts *those*. Token is
	// the `:` and everything after it.
	ErrArithConditionalElse
	// ErrArithColonWithoutQuestion is a `:` standing where no `?` opened a
	// conditional: `$(( 1 : 2 ))`, and `$(( 1 ? 2 : 3 : 4 ))` after a
	// complete one.
	//
	// Two readers arrive here. Under Dialect.ArithColonIsAToken the colon is
	// read as a math token and the failure comes after the second value has
	// been found; everywhere else it is text left over, which is where
	// leftoverKind sends it. The kinds are one because the dialects that
	// word it apart word it the same in both readings — and because one of
	// them, ksh93, gives a stray colon a shape no other leftover gets, which
	// only a kind of its own can carry (#2224).
	//
	// Token is the text from the colon to the end of the expression. A
	// dialect with no sentence for it falls back to the leftover-text one,
	// which is what it said before this kind reached it.
	ErrArithColonWithoutQuestion
	// ErrArithCharacterMissing is the character-code operator with nothing
	// after it to take the code of: `$((##))`, or a backslash at the end of
	// the expression. Its own kind because the one dialect that has the
	// operator words it as neither an operand nor an operator failure —
	// `character missing after ##` — and a dialect without the operator never
	// reaches it at all.
	ErrArithCharacterMissing
	// ErrArithIllegalByte is a byte the dialect's arithmetic reader refuses as
	// part of no token at all, reported *at the byte* rather than as a
	// missing operand.
	//
	// Its own kind because one shell in the panel has a third sentence for it
	// — `illegal character: @` — and gives that sentence only where the token
	// stream could legally have ended: at the start of the expression, or
	// where an operator belonged. Where an operator has just been consumed
	// and an operand is wanted, the same byte gets the operand sentence
	// instead, so the byte alone does not decide. See Dialect's
	// ArithBytesRefusedOutright, which is the table; the position is the
	// parser's to know.
	//
	// Token is the single refused byte, which is what that shell names — not
	// the text from it to the end of the expression, which is what the
	// operand failures name.
	ErrArithIllegalByte
	// ErrArithBadOutputFormat is an output-format specifier the dialect that
	// has the construct could not read: `[#]`, `[##]`, `[foo]`, `[# 16]`,
	// `[#16 ]`. See Dialect's ArithOutputFormat for what the construct is.
	//
	// Its own kind because the sentence is about the *specifier* and not
	// about an operand or an operator — `bad output format specification` —
	// and it is raised where a value has not been wanted yet, so neither of
	// the operand kinds could carry it. A dialect without the construct never
	// reaches it: there, the `[` is an operand failure as it always was.
	//
	// Token is the specifier as written, brackets included.
	ErrArithBadOutputFormat
	// ErrArithBadBaseSyntax is a bracketed group holding nothing but digits —
	// `$(( [16] 255 ))` — which the one dialect with output formats words
	// differently again, as `bad base syntax`.
	//
	// Its own kind rather than a shape of ErrArithBadOutputFormat because the
	// two are measurably apart in that dialect and are told apart by one
	// character: `[16]` is this and `[#16]` is a format. A reading that
	// folded them would word half of the pair wrongly and nothing else would
	// notice, since both are failures either way.
	//
	// Token is the group as written, brackets included.
	ErrArithBadBaseSyntax
	// ErrArithMissingCloseParen is a parenthesised sub-expression that read a
	// complete value and then met something that could neither continue it
	// nor close the group: `$(( (echo a) ))`, where `echo` is the value and
	// `a` is neither an operator nor a `)`.
	//
	// It is an *arithmetic* failure and not a syntax error, which is the
	// whole reason it is here: every shell in the panel reports it the way it
	// reports a division by zero — the expression quoted, the ordinary
	// arithmetic status — and one of them ends the script at that status
	// rather than at a parse failure's.
	//
	// Its own kind because two dialects have a sentence for the unclosed
	// group that they give no other leftover — `missing `)'` and
	// `expecting ')'` — where the rest say what they say about any text an
	// expression could not use. A dialect without one falls back to the
	// leftover wording, which is what it would have said had the group
	// closed.
	//
	// Token is the text from the leftover to the end of the expression, which
	// is the same span the leftover kinds name.
	ErrArithMissingCloseParen
	// ErrUnexpected is a token where the grammar wanted something else. The
	// panel names the token three ways and one of them names its *class*
	// instead — dash says "word unexpected" for an ordinary word and quotes
	// a reserved word or an operator — so the class travels with it.
	ErrUnexpected
	// ErrUnmatched is input that ran out inside a quote, a command
	// substitution or a `${`. Its own kind because the panel splits three
	// ways over the same EOF: two dialects name the delimiter (one the
	// opener, one the closer), one echoes the text near it, and one closes
	// a quote quietly and runs what it got — which is a grammar flag, so
	// the error never exists there. Token is the opener as written, `'`,
	// `"`, a backquote, `$(` or `${`; Expected is what would have closed
	// it; LastToken is the text from the opener to the end of its line.
	ErrUnmatched
	// ErrCondOperand is an operand a conditional operator cannot take: a
	// token where `[[ ]]` wanted a word, so `[[ $k == (a|b) ]]` in a dialect
	// with no bare pattern groups, or `[[ -n ]]` with nothing after the
	// operator at all.
	//
	// Its own kind because one dialect words it as a statement about the
	// *operator* — "unexpected argument `(' to conditional binary operator" —
	// rather than as a token the grammar did not want, and the others use
	// the wording they use for any such token. Token is the offending one
	// and Expected is `unary` or `binary`, which is the only part of the
	// sentence that varies within the dialect that has one.
	ErrCondOperand
	// ErrForName is a `for` whose variable is not a name. Its own kind
	// because the panel does not word it as an unexpected token: three of the
	// four say something about the *name* and only the fourth blames the
	// word it found.
	ErrForName
	// ErrForArithHeader is a C-style `for` header that does not hold two
	// separators: `for (())`, `for ((;))`, `for ((i=0))`, `for ((1;2))`.
	//
	// Its own kind because every shell in the panel refuses it and none of
	// them words it as a token the grammar did not want. Measured 2026-09-12
	// on `for ((i=0)); do :; done`:
	//
	//	bash 5.3.15   syntax error: arithmetic expression required
	//	bash 3.2.57   the same sentence
	//	bash-as-sh    the same sentence
	//	ksh93u+       `))' unexpected
	//	zsh 5.9.2     parse error near `i=0'
	//
	// so one of the three sentences is about the expression that was not
	// there, one blames the closer, and one names the text of the last part
	// — and names nothing where that part is empty, which is why Token and
	// LastToken are both carried.
	//
	// Token is the header as written, `((` and `))` included and the blanks
	// and newlines inside it kept, which is what one dialect echoes back on
	// a second line. LastToken is the last part trimmed, which is what
	// another names.
	ErrForArithHeader
	// ErrForArithSeparator is a C-style `for` header with *more* than two
	// separators: `for ((;;;))`, `for ((1;2;3;4))`.
	//
	// Apart from ErrForArithHeader because the panel splits twice over it.
	// It splits on whether the header is refused at all — bash refuses,
	// where ksh93 and zsh take the header and fold everything past the
	// second `;` into the third expression, so the refusal arrives at run
	// time and only if that expression is ever evaluated. That half is
	// Dialect.ForArithExtraSeparators. And in the dialect that does refuse
	// it, the sentence is not the one above: bash says “ `;' unexpected “
	// where it says `arithmetic expression required` for too few. Two
	// refusals rather than one, so two kinds (#2225).
	//
	// Token and LastToken carry what ErrForArithHeader's do.
	ErrForArithSeparator
	// ErrHeredocOutsideSubstitution is a here-document opened inside a
	// substitution that holds a program — `$( )` or `${ ; }` — whose body is
	// not inside it, which is what `echo $(cat <<EOF)` leaves: the text ends
	// on that line and the body would have to come from the lines after the
	// *enclosing* command.
	//
	// Its own kind because the one column that refuses it words it as a
	// statement about the here-document rather than as a token the grammar
	// did not want, and because it is refused while the line is read — the
	// commands in front of it have already run. Token is `<<` with the
	// delimiter's quoting off, which is what that sentence quotes. See
	// [Dialect.HeredocBodyMustBeInsideTheSubstitution], which carries the
	// panel.
	ErrHeredocOutsideSubstitution
	// ErrEmptyAssignSubscript is `a[]=v` — an assignment whose brackets were
	// written with nothing at all between them — in the one grammar that
	// refuses it while reading. See
	// [Dialect.EmptyAssignSubscriptIsASyntaxError] for the measurement, and
	// interp.Semantics.EmptySubscriptToAnAssignment for the columns that
	// answer the same text where it runs instead.
	//
	// Its own kind because the sentence is its own: that shell names the
	// brackets rather than the word, and says `empty subscript` where every
	// other refusal of a token it did not want says `unexpected`. Token is
	// the two brackets.
	ErrEmptyAssignSubscript
	// ErrArithAssignToNonPlace is an arithmetic assignment whose left side is
	// not something a value can be stored in: `$(( 7=4 ))`, `$(( 1+2=3 ))`,
	// `$(( (1)=2 ))`.
	//
	// Its own kind because every shell in the panel has a sentence for it and
	// none of them is the leftover-text one this used to earn here — bash
	// `attempted assignment to non-variable`, zsh `bad math expression: lvalue
	// required`, ksh93 `assignment requires lvalue`, measured 2026-09-22 under
	// `env -i PATH=/usr/bin:/bin`. Token is the operator and everything after
	// it, which is what bash names: `=4 ` for `7=4 `.
	ErrArithAssignToNonPlace
	// ErrHeredocCount is more here-documents on one command than the dialect
	// allows — see [Dialect.HeredocMax], which is one shell's bound and is zero
	// everywhere else.
	//
	// Its own kind because the sentence names no token: bash writes `maximum
	// here-document count exceeded` and nothing else, where every refusal of a
	// token it did not want names the token. Pos is the operator that went over
	// the bound, which is on the command's own line.
	ErrHeredocCount
	// ErrArithUnmatchedCloseParen is a `)` left over after a complete
	// expression, closing no group: `x="3)"; $(( x ))`. Its own kind because
	// the three shells that word leftovers apart each have a sentence for
	// this one — zsh `bad math expression: unexpected ')'`, ksh93
	// `unbalanced parenthesis`, and bash its ordinary `arithmetic syntax
	// error in expression` where `1 @` is `invalid arithmetic operator` —
	// measured 2026-10-02.
	ErrArithUnmatchedCloseParen
)

type Expr

type Expr interface {
	Node
	// contains filtered or unexported methods
}

Expr is an and-or expression: either a single pipeline or a binary tree of them.

type File

type File struct {
	Stmts []*Stmt
	Last  Pos

	// Refused is a failure that gives up **this line** rather than the file,
	// and it is nil for every line that read cleanly.
	//
	// One construct raises it: a syntax error inside a compound assignment's
	// parentheses, where the shell that has them reads the parentheses as
	// part of a word and their contents as a list of their own. A failure in
	// that list is reported, the command it belongs to is thrown away
	// unrun — the function `f() { a=(p & q); }` is *not* defined — and the
	// shell goes on reading at the next line. See
	// Dialect.CompoundAssignmentErrorGivesUpTheLine for the panel.
	//
	// Only NextLine hands one back, because only a shell reading a program as
	// it runs it has a next line to go on to. Parse, which reads the whole
	// file at once and runs none of it, carries the first one into the
	// parser's own error instead: there, the refusal *is* the answer, which is
	// what `bash -n` reports and exits 1 for.
	Refused error

	// Substitutions are the command substitutions read while this file was
	// parsed, in reading order, for a dialect that parses their bodies with
	// the line that holds them. Empty for every other dialect and for a file
	// holding none.
	//
	// The parser cannot parse a body itself: a body is read with the alias
	// tables, the options and the dialect as the *shell* holds them at that
	// moment, and none of those are here. So what crosses is the list, and
	// the shell reads each body before it runs the line — which is the whole
	// of [Dialect.SubstitutionBodyRead] (#2857).
	//
	// Spans rather than pointers into the tree, because a span read out of an
	// expansion's operand is copied on its way into the word that holds it
	// and a pointer taken here would name the copy that was left behind.
	// Nothing writes through them; the list is an index, not a second tree.
	Substitutions []Span

	// CarriedHeredocs are the substitutions on this file's lines whose own
	// text opened a here-document it could not feed, and the operators whose
	// bodies the lexer holding the *enclosing* command went on to read from
	// the lines after it.
	//
	// Empty in every dialect but the two that read a body that way; see
	// [Dialect.HeredocBodyFromAfterTheCommand] for the panel and
	// [Lexer.carryHeredocsOut] for the seam. A list beside the tree rather
	// than a field on [Span], and that is a **size** decision and not a
	// design preference: a Span is 64 bytes, which is a Go size class, and a
	// startup keeps 185,000 of them — a slice header on it costs 3MB of
	// resident memory to carry a list that is empty on all but a handful.
	// See TestTheTwoShapesATreeIsMadeOfStayWithinTheirBudget.
	CarriedHeredocs []CarriedHeredoc
}

File is a whole parsed input.

func Parse

func Parse(src string, d Dialect) (*File, error)

Parse parses src completely.

func (*File) End

func (f *File) End() Pos

func (*File) Pos

func (f *File) Pos() Pos

type ForArithClause

type ForArithClause struct {
	Init, Cond, Post ArithExpr
	// InitText, CondText and PostText are the three parts as written. A part
	// containing an expansion has no tree until it runs, so the text is what
	// the interpreter reads it from.
	InitText, CondText, PostText string
	Body                         []*Stmt
	Header                       string
	Start, Stop                  Pos
	// contains filtered or unexported fields
}

ForArithClause is `for ((init; cond; post)) do … done`.

A separate node from ForClause rather than a variant of it, because it iterates on a condition rather than over a list: the two share a keyword and nothing else.

func (*ForArithClause) End

func (c *ForArithClause) End() Pos

func (*ForArithClause) PartsAsWritten

func (c *ForArithClause) PartsAsWritten() (init, cond, post string)

PartsAsWritten is the three parts with the blanks the script wrote around them, which the fields do not keep.

ForArithClause.InitText, CondText and PostText are trimmed because they are also what the printer lays a header out from, and untrimmed parts print back with their blanks doubled — see syntax/printroundtrip_test.go, whose promise is that printing a program gives the same program. A diagnostic wants the other answer: bash quotes a failing part back as it was written, so `for (( $x ;;))` with x=`echo hi` is `((: echo hi : …` there. So the spelling is re-split from ForArithClause.Header, which already holds the header verbatim and which SameProgram already skips for being a spelling.

Empty parts and a Header that is not this clause's both come back as the trimmed fields, so a tree built by hand answers as it always did.

func (*ForArithClause) Pos

func (c *ForArithClause) Pos() Pos

type ForClause

type ForClause struct {
	// Names is the loop's variables, and there is always at least one. More
	// than one is zsh's — see [Dialect.ForMultipleNames] — and the loop then
	// takes that many words from the list on every pass, so the count is the
	// stride and not a decoration.
	//
	// A slice rather than a name and a tail, because a `for` binds a *list*
	// of names and one of them being first is not a fact about the language.
	// Two fields would have been two things to keep in step, and every reader
	// would have had to ask which it wanted.
	Names []string
	// Items is the word list. HasItems is what distinguishes an absent list
	// from an empty one, which is a real difference and not a nicety: with
	// `in` omitted the loop iterates the positional parameters, and with `in`
	// present and nothing after it, nothing. A nil slice cannot say which.
	Items    []*Word
	HasItems bool
	Body     []*Stmt
	// RefusedName is the word standing where a name belonged when it was not
	// one, in the dialect that checks it when the loop runs — see
	// [Dialect.ForNameCheckedWhenTheLoopRuns]. It is the source text, quotes
	// and expansion and all, because that is what the complaint quotes:
	// `for $n` refuses `$n` and not `n`.
	//
	// Set instead of Names rather than beside it: there is no name to bind,
	// so a clause carrying one would be a clause the interpreter could try
	// to run. Empty is the ordinary case and the only one in five of the six
	// dialects.
	RefusedName string
	// Header is `for i in 1 2` as written, kept for the same reason
	// ArithCmdClause keeps its expression: it is what a diagnostic quotes.
	// bash prints it under `set -x` unexpanded — `for i in $x`, quotes and
	// all — so nothing rebuilt from the tree would match it.
	Header string
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

ForClause is `for name [in words] do … done`.

func (*ForClause) End

func (c *ForClause) End() Pos

func (*ForClause) Pos

func (c *ForClause) Pos() Pos

type FuncDecl

type FuncDecl struct {
	Name string
	// NameWord is the name as it was *written*, kept only where the dialect
	// expands one — see [Dialect.FunctionNameExpands]. Nil is the ordinary
	// case and means Name is the whole of it.
	//
	// It exists because Name is a string and the name may not be one until
	// the shell runs: `w=foo; _p_${w}() { … }` defines `_p_foo`, and the
	// literal text of that token is `_p_w`, which is a different function
	// and a plausible one. Flattening it was silent — status 0, a function
	// defined, and the one the script asked for missing.
	NameWord *Word
	// RefusedName is the word standing where a name belonged when it was not
	// one, in the dialects that check it where the definition runs — see
	// [Dialect.FunctionNameCheckedWhenTheDefinitionRuns]. It is the source
	// text, quotes and expansion and all, because that is what the complaint
	// quotes: `function _p_${w}` refuses `_p_${w}` and not `_p_w`.
	//
	// Set instead of Name rather than beside it, exactly as
	// [ForClause.RefusedName] is: there is no name to bind, so a declaration
	// carrying one would be a declaration the interpreter could define. Empty
	// is the ordinary case and the only one in four of the six dialects.
	RefusedName string
	// AlsoNamed holds the second and later names of a definition that gave
	// several — `function clipcopy clippaste { … }` — and is empty for the
	// one-name definition every dialect has. See
	// [Dialect.FunctionMultipleNames].
	//
	// The extra names live only in the declaration and never in the function
	// table: a defined function has exactly one name, which is why this is a
	// field beside Name rather than Name becoming a list. The interpreter
	// makes one definition per name, and each of them answers `$0` with its
	// own — which is the whole of why a script writes this instead of two
	// definitions with the same body.
	AlsoNamed []FuncName

	// Keyword records that the `function` word was used, which is not
	// universal: dash rejects it, and ksh93 rejects the hybrid form with
	// parentheses as well.
	Keyword bool
	Body    Command
	Start   Pos

	// SourceText is the definition as it was **written** — from the name
	// through the character that ended the statement — and is recorded only
	// where [Dialect.FunctionDefinitionIsSourceText] says the dialect writes
	// one back that way.
	//
	// Only there for the same reason [Stmt.Text] is only on a background
	// statement: every other node can be re-read from the input it came
	// from, and one shell's `typeset -f` is the one place a definition has
	// to be shown back long after the source was let go of. ksh93 reproduces
	// the characters, comments and odd spacing and all; the other five print
	// the tree through their own function layout and leave this empty.
	//
	// Empty is also what a hand-built tree has, and what a definition the
	// parser refused has, so a listing that reads it must fall back to the
	// layout rather than write nothing.
	SourceText string
}

FuncDecl is `name() compound` or `function name compound`.

The body is a compound command rather than specifically a brace group, so it can be any of them and can carry its own redirections.

func (*FuncDecl) End

func (c *FuncDecl) End() Pos

End is the body's end, or the declaration's own start where there is no body.

A definition the parser refused keeps its node — the name and the parentheses were read, and the error is what the caller acts on — so a FuncDecl with a nil body is a normal product of a failed parse rather than a malformed tree. Two extent computations reach one through a failure and neither can know it did: a coproc takes its own end from the command after the word, and a short-form loop body takes it from the statement it read. Both dereferenced the body that was never there (#911).

The extent is empty rather than wrong. Nothing downstream of a failed parse measures a declaration that has no body, and an empty extent at the name keeps the one invariant a reader may hold: Pos is never past End.

func (*FuncDecl) Pos

func (c *FuncDecl) Pos() Pos

type FuncName

type FuncName struct {
	Name string
	Word *Word
}

FuncName is one name in a definition's name list, read the way a first name is read.

The two fields are the same split FuncDecl.Name and FuncDecl.NameWord make, and for the same reason: a name holding an expansion is not text until the shell runs, and its literal spelling is a plausible wrong answer. Word is nil for the ordinary case, where Name is the whole of it.

type FunctionHeader

type FunctionHeader uint8

FunctionHeader is how a function declaration's header is spelled.

The three values are the three answers measured, and the spelling matters beyond taste in exactly one dialect: `typeset` in a `function f { … }` body declares a local there and assigns the global in an `f() { … }` body, which is the axis Semantics.TypesetLocalNeedsKeywordFunction records. So the default keeps what was written, and a listing that respells says so.

const (
	// FunctionHeaderAsWritten writes the keyword where the declaration had
	// one and `()` where it did not, which is the spelling that reads back
	// to the same tree.
	FunctionHeaderAsWritten FunctionHeader = iota
	// FunctionHeaderKeywordAndParens writes both, whichever was used:
	// `function f () `. The hybrid spelling, which parses to the keyword
	// declaration in every grammar that takes it.
	FunctionHeaderKeywordAndParens
	// FunctionHeaderParens writes `f () ` and never the keyword. Correct
	// only where the two bodies run alike, which is every dialect but one.
	FunctionHeaderParens
)

func (FunctionHeader) String

func (h FunctionHeader) String() string

type Group

type Group struct {
	List  []*Stmt
	Start Pos
	Stop  Pos
	// contains filtered or unexported fields
}

Group is `{ list; }`, which runs in the current shell.

func (*Group) End

func (c *Group) End() Pos

func (*Group) Pos

func (c *Group) Pos() Pos

type HeredocBodyOutside added in v0.0.10

type HeredocBodyOutside uint8

HeredocBodyOutside is which parenthesized substitution spellings read a here-document's body from the lines after the enclosing command.

An enumeration rather than a bool because ksh93 does it for one spelling and bash 5.3 for two, and a bool would have made those one answer. See Dialect.HeredocBodyFromAfterTheCommand for the panel.

const (
	// HeredocBodyStaysInsideTheSubstitution is the quiet reading four
	// columns take: the substitution is empty and the body's own lines are
	// run as commands. The zero value, and what the core keeps.
	HeredocBodyStaysInsideTheSubstitution HeredocBodyOutside = iota
	// HeredocBodyAfterProcessSubstitution carries the body for `<( )` and
	// `>( )` only. ksh93's, whose `$( )` is refused while reading instead —
	// see [Dialect.HeredocBodyMustBeInsideTheSubstitution].
	//
	// **Nothing distinguishes this from the value below in the one dialect
	// that holds it, and that is said here rather than left to be
	// rediscovered.** ksh93's `$( )` is refused before the carry is reached,
	// so moving that preset to the wider value changes no output and fails
	// no test — a mutation run on 2026-09-20 to check. It records the
	// measurement rather than deciding an answer, and it is what keeps the
	// two flags independent: a dialect that carried `$( )` *without* the
	// refusal would be reading bash's answer under ksh93's name.
	HeredocBodyAfterProcessSubstitution
	// HeredocBodyAfterEitherParenthesizedSpelling carries it for `$( )` as
	// well. bash 5.3's, and not bash 3.2's.
	HeredocBodyAfterEitherParenthesizedSpelling
)

type HeredocDelimiterSubstitution added in v0.0.18

type HeredocDelimiterSubstitution uint8

HeredocDelimiterSubstitution is what a here-document's **delimiter** may be written as. See Dialect.HeredocDelimiterSubstitutions.

Nothing in a delimiter expands in any shell in the panel — `<<$d` waits for a line reading `$d` — so what the columns disagree about is whether the parser will *accept* the shapes an expansion is written in.

Measured 2026-09-27 under `-c`, `env -i PATH=/usr/bin:/bin LC_ALL=C`, the program being `: <<$D` / `body` / `END` / `echo alive`, against bash 5.3.20, zsh 5.9.2 (`-f`), ksh93u+ 2012-08-01, dash 0.5.12 and BusyBox ash 1.37.0 in alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b; `go version -m` on each of the four native ones: *not a Go executable*.

$D            bash  zsh  ksh93     dash        ash
$(echo E)     warns 0    refuses   refuses     refuses
$((1))        warns 0    0         refuses     refuses
a$(echo E)b   warns 0    refuses   refuses     refuses
"$(echo E)"   warns 0    refuses   0           0
`a`           warns 0    0         0           0
`a b`         warns 0    0         refuses     refuses
"`a b`"       warns 0    0         0           0
${v}          warns 0    0         0           0
$v            warns 0    0         0           0

bash's warning is the control that says no column expands a delimiter: it is `here-document at line 1 delimited by end-of-file (wanted `$(( 1/0 ))')`, the *literal* word being looked for, and we write it identically.

Two rules, not one, and the `"$(echo E)"` row is what parts them (#4691).

const (
	// HeredocDelimiterTakesEverySubstitution scans each substitution spelling
	// for its extent, keeps the source it covered as literal text, and
	// refuses nothing. bash, zsh and the core.
	HeredocDelimiterTakesEverySubstitution HeredocDelimiterSubstitution = iota

	// HeredocDelimiterScansNoUnquotedSubstitution reads the unquoted part of
	// a delimiter as plain text, so no substitution is scanned there at all.
	// dash and BusyBox ash.
	//
	// One reading with two visible consequences, which is why it is a
	// statement about the *scanning* rather than a refusal of a spelling:
	//
	//   - a `$(` is not a substitution, so the `(` stands where no word may
	//     have one and is an unexpected token — the same sentence those two
	//     say about a `(` anywhere else. `$((1))` goes the same way, which is
	//     what says the rule is the two characters and not the expression.
	//   - a backquote does not open one either, so it does not protect a
	//     blank: `<<`a b“ ends the delimiter at the space and leaves the
	//     backquote open, which then swallows the rest of the input and ends
	//     at EOF. `<<`a“ has no blank in it and is simply a delimiter with
	//     backquotes in its name.
	//
	// A quoted run inside a delimiter is unaffected — `<<"$(a b)"` and
	// `<<"`a b`"` are both taken — which is the row that makes this a
	// statement about the unquoted part rather than about the word.
	HeredocDelimiterScansNoUnquotedSubstitution

	// HeredocDelimiterRefusesACommandSubstitution scans as the core does and
	// then refuses a `$( )` **however it is written**, quoted or not. ksh93.
	//
	// A command substitution and nothing else: `$((1))` is taken, a backquote
	// is taken, and `${v}` is taken. The refusal is that shell's
	// here-document sentence, which is
	// [Dialect.HeredocBodyMustBeInsideTheSubstitution]'s.
	//
	// **The rule that shell really holds is wider than this one**, and the
	// wider half is deliberately not claimed here. Measured the same day:
	// `: <<END $(echo hi)` — a substitution written *after* a delimiter that
	// is an ordinary word — is refused with the same sentence, while
	// `: $(a b) <<END`, with the substitution in front of the operator, runs.
	// So what that shell refuses is a `$( )` standing anywhere behind a
	// here-document operator whose body has not been read yet. Claiming that
	// needs the operator's reach to outlive the word it is written beside,
	// which is a seam this grammar does not have; the delimiter is the half
	// #4691 measured and the half this answers.
	//
	// **And the token its sentence quotes is approximated.** That shell
	// quotes `<<` and a string taken from *inside* the substitution at an
	// offset that moves with the text: `$(echo E)` is quoted as “ `<<E' “,
	// `$(export q)` as “ `<<rt' “ — a suffix of `export` — and
	// `$(a=1 b c)` as “ `<<=1' “, which is not a word of the program at
	// all. A quoted string that falls in the middle of a word is an artifact
	// of that parser's own buffer rather than a fact about the language, and
	// reproducing it would mean deciding what it had in hand, which
	// CLEANROOM.md forbids. So the token written here is `<<` and the
	// delimiter as it stands, which is the convention every other
	// here-document refusal in this grammar already follows.
	HeredocDelimiterRefusesACommandSubstitution
)

type HeredocDelimiterTabs added in v0.0.6

type HeredocDelimiterTabs uint8

HeredocDelimiterTabs is what `<<-` does with tabs the *delimiter* was written with. See Dialect.StrippedHeredocDelimiter.

The operator strips leading tabs from every body line, and that part is unanimous. A delimiter can only begin with a tab if it was quoted — `<<- '<tab>EOF'` — and then the stripped lines can never be spelled like it, so each shell has had to decide what such a document ends at. Three answers, measured 2026-09-16 from script files under `env -i`, standard input the null device, the body printed by `cat`:

delimiter written  <tab>EOF   <tab>EOF   <tab>EOF        <tab><tab>EOF
end line           <tab>EOF   EOF        <tab><tab>EOF   <tab><tab>EOF
bash 5.3.20        ends       runs out   runs out        ends
zsh 5.9.2          ends       ends       ends            ends
ksh93u+ 2012       ends       ends       ends            ends
dash 0.5.12        runs out   runs out   runs out        runs out
BusyBox ash 1.37   runs out   runs out   runs out        runs out

So bash compares the line as written as well as the stripped line, zsh and ksh93 strip the delimiter's tabs as they strip the line's, and dash compares the stripped line against the delimiter as written, which nothing can equal. A tab anywhere but the front of the delimiter is unanimous and is not this question, and neither is `<<` without the dash, which strips nothing in any column. BusyBox ash was measured the same day through the pinned Alpine image and answers as dash does.

const (
	// HeredocDelimiterTabsAreKept compares the stripped line against the
	// delimiter as it was written, so a delimiter that opens with a tab is
	// never met and the body runs to the end of the input. dash, BusyBox
	// ash, and the core.
	HeredocDelimiterTabsAreKept HeredocDelimiterTabs = iota

	// HeredocLineAsWrittenMeetsTheDelimiter also compares the line as it was
	// written, before its tabs were stripped: `<tab>EOF` ends a `<tab>EOF`
	// document and `EOF` does not. bash 5.3.
	HeredocLineAsWrittenMeetsTheDelimiter

	// HeredocDelimiterTabsAreStrippedToo strips the delimiter's leading tabs
	// as the lines' are stripped, so any line reading `EOF` once its tabs are
	// gone ends a `<tab>EOF` document. zsh 5.9.2 and ksh93u+.
	HeredocDelimiterTabsAreStrippedToo
)

type IfClause

type IfClause struct {
	Cond  []*Stmt
	Then  []*Stmt
	Elifs []*Elif
	Else  []*Stmt
	// HasElse distinguishes `else` with an empty body from no `else` at all.
	HasElse bool
	Start   Pos
	Stop    Pos
	// contains filtered or unexported fields
}

IfClause is `if … then … [elif …] [else …] fi`.

Cond is a list rather than a single command, and its *last* command decides: `if false; true; then` takes the branch.

func (*IfClause) End

func (c *IfClause) End() Pos

func (*IfClause) Pos

func (c *IfClause) Pos() Pos

type Kind

type Kind uint8

Kind classifies a token.

Operators get a kind each rather than a shared kind with the text attached. The parser switches on them, and a dialect decides whether some of them exist at all, so both want a value rather than a string comparison.

const (
	// TokEOF is returned once the input is exhausted, repeatedly.
	TokEOF Kind = iota

	// TokWord is a word, possibly built from differently quoted spans. Its
	// Spans field is what carries the quoting; see [Token].
	TokWord

	// TokIONumber is a run of digits immediately followed by a redirection
	// operator, which makes it a file descriptor rather than an argument.
	// `echo 1>b` writes an empty file and `echo 1 >b` writes "1"; one space
	// changes what the digit is.
	TokIONumber

	// TokNewline is significant: it terminates a command like `;` does, and it
	// is where a pending here-document's body begins.
	TokNewline

	// TokArithCmd is `(( expr ))` used as a command. Its Text is the expression,
	// scanned raw: what is inside is an arithmetic expression rather than a
	// command list, so tokenizing it as commands would lose it.
	TokArithCmd

	// Control operators.
	TokAmp        // &
	TokAmpBang    // &!   background it and let go of it
	TokAmpPipe    // &|   the same, spelled the other way
	TokAndAnd     // &&
	TokPipe       // |
	TokOrOr       // ||
	TokPipeAmp    // |&   pipe both streams; two bytes with no blank between
	TokSemi       // ;
	TokDSemi      // ;;
	TokSemiAmp    // ;&   fall through to the next case body
	TokDSemiAmp   // ;;&  keep testing later case patterns
	TokSemiPipe   // ;|   the same, spelled zsh's way
	TokLeftParen  // (
	TokRightParen // )

	// Redirection operators.
	TokLess      // <
	TokGreat     // >
	TokDGreat    // >>
	TokLessAmp   // <&
	TokGreatAmp  // >&
	TokLessGreat // <>
	TokClobber   // >|
	// The clobber-override marker in its other spellings. Core has `|` on
	// `>` alone; one dialect takes `|` or `!` after any of the four write
	// operators, which is [Dialect.ClobberOverrideMarker] and these seven
	// tokens. Each is its own kind because the printer writes a redirection
	// back with the spelling it was read with.
	TokClobberBang      // >!
	TokDGreatClobber    // >>|
	TokDGreatBang       // >>!
	TokAmpGreatClobber  // &>|
	TokAmpGreatBang     // &>!
	TokAmpDGreatClobber // &>>|
	TokAmpDGreatBang    // &>>!
	// The same four operators written with the ampersand *after* the `>`
	// rather than before it, plus the appending base they hang off. One
	// dialect spells both-streams twice over and the two spellings are the
	// same operator measured row for row; see
	// [Dialect.ReversedAmpersandRedirect]. `>&` itself is not here because
	// it is core — it is POSIX duplication, and whether a word that is not a
	// number makes it a write to a file is interp's GreatAmpTarget.
	TokDGreatAmp        // >>&   both streams, appending
	TokGreatAmpClobber  // >&|
	TokGreatAmpBang     // >&!
	TokDGreatAmpClobber // >>&|
	TokDGreatAmpBang    // >>&!
	TokDLess            // <<
	TokDLessDash        // <<-
	TokTLess            // <<<  herestring
	TokAmpGreat         // &>   both streams
	TokAmpDGreat        // &>>  both streams, appending
	// TokGreatSemi is `>;`, one dialect's write that only lands if the
	// command succeeded: the output goes to a temporary file beside the
	// target and is renamed over it at the end, or thrown away. The `;` is
	// part of the operator and has to be tight against the `>` — `> ; f` is
	// a syntax error where `>; f` is not. See
	// [Dialect.RenameOnSuccessRedirect].
	TokGreatSemi // >;
	// TokLessHash and TokGreatHash are `<#` and `>#`, one dialect's
	// **file-position** redirections: they move where a descriptor next
	// reads or writes rather than deciding what it is aimed at. The operand
	// is a word, and the one this reads is an arithmetic command — `exec
	// 3<#((0))` is a seek to the start — so the parser takes a TokArithCmd
	// where every other redirection takes a target. See
	// [Dialect.SeekRedirect].
	TokLessHash  // <#
	TokGreatHash // >#
)

func (Kind) IsHeredoc

func (k Kind) IsHeredoc() bool

IsHeredoc reports whether k begins a here-document, whose body is read from the lines after the current one rather than from the token stream.

func (Kind) IsRedirect

func (k Kind) IsRedirect() bool

IsRedirect reports whether k is a redirection operator. A redirection may appear anywhere in a simple command — before the command name or between its arguments — so the parser lifts these out of the word list wherever it finds them rather than expecting a suffix.

func (Kind) IsSeek added in v0.0.7

func (k Kind) IsSeek() bool

IsSeek reports whether k is one of the file-position operators, whose operand is an arithmetic command rather than a target. See Dialect.SeekRedirect.

func (Kind) String

func (k Kind) String() string

String returns the operator's spelling, or a name for the non-operators.

type Layout

type Layout struct {
	// Indent is one level of indentation, and Nested repeats it once per
	// enclosing block. Without Nested every block is indented the same,
	// however deep.
	Indent string
	Nested bool
	// Lines puts each statement of a block on a line of its own, whatever
	// the source did. Nothing else here applies without it.
	Lines bool

	// Separator goes after every statement of a block but the last.
	Separator string
	// KeywordTerminator goes after the *last* statement of a body closed by
	// a keyword — `fi`, `done`, `else`. A body closed by a bracket takes
	// nothing, in every shell measured.
	KeywordTerminator string

	// ThenOnItsOwnLine, DoAfterWordsOnItsOwnLine and DoAfterCommandOnItsOwnLine
	// say whether the keyword that opens a body starts a line of its own.
	//
	// Three fields and not one, because a shell may answer them differently:
	// one keeps `then` with its `if` and moves the `do` of a loop over words
	// while keeping the `do` of a loop over a command.
	ThenOnItsOwnLine           bool
	DoAfterWordsOnItsOwnLine   bool
	DoAfterCommandOnItsOwnLine bool

	// BraceOpenSuffix follows the `{` that opens a block — a space, or
	// nothing.
	BraceOpenSuffix string
	// BareTimeSuffix follows a `time` that has no pipeline after it, which is
	// a construct with nothing to separate it from and so needs saying.
	//
	// Measured 2026-09-23 on bash 5.3.20: a function whose whole body is
	// `time` prints back as `    time ` — the keyword, then the space the
	// pipeline would have been written after. It is visible because `type`
	// and `declare -f` write a body back, and a suite file compares one.
	BareTimeSuffix string
	// OutermostBraceOpensALine puts the first statement of the outermost
	// block on a line of its own. A block inside one always does.
	OutermostBraceOpensALine bool

	// CaseHeaderSuffix follows the `in` of a `case`.
	CaseHeaderSuffix string
	// CaseArmsOnOneLine keeps a `case` arm's pattern, body and terminator
	// together instead of giving each a line.
	CaseArmsOnOneLine bool
	// CasePatternsParenthesised writes an arm's pattern with the opening
	// parenthesis that the grammar allows and most shells leave out.
	CasePatternsParenthesised bool
	// CaseAlternativesSpaced writes a blank on each side of the `|` between
	// an arm's alternatives, as both listings do: `a|b)` comes back `a |
	// b)` in bash 5.3.20's `declare -f` and `(a | b)` in zsh 5.9.2's
	// `which`, measured 2026-10-02 (#5138). An arm one dialect read as a
	// single pattern is written as it read it — see CaseItem.Grouped —
	// which is what keeps zsh's `( a | b )` as `(a|b)`.
	CaseAlternativesSpaced bool

	// SubshellBodyOnItsOwnLines gives a `( … )` the shape a brace group
	// gets: the parenthesis ends its line, the statements are indented one
	// level further, and the closing parenthesis starts a line of its own.
	//
	// One engine writes `( exit 1 )` and the other writes the three lines,
	// in the same listing and from the same tree, which is what makes this
	// a field.
	SubshellBodyOnItsOwnLines bool
	// SubshellTerminatesItsLastCommand writes a one-line `( … )` the way a
	// brace group is written, every command ending in `; ` — the text zsh
	// 5.9.2 labels a job with: `( :; )` for `(:)`, and `( ( :; ); )` for
	// `( (:) )`, measured 2026-10-02 with `TIMEFMT='%J'; time …`.
	SubshellTerminatesItsLastCommand bool

	// BlankLineAfterAHereDocumentBody writes the newline that opens the next
	// line even where a here-document body has already ended the line,
	// leaving a blank line the source never had.
	//
	// Measured, and not a defect in either engine: one leaves the blank line
	// between the body and whatever follows it — a statement, a `fi`, the
	// `}` that closes the function — and the other writes the next line
	// straight onto the one the delimiter ended.
	BlankLineAfterAHereDocumentBody bool

	// BackgroundKeepsTheLine lets the statement after a `&` follow it on the
	// same line rather than starting one of its own.
	//
	// A `&` is a terminator, so `a & b` is already two statements and an
	// arrangement that puts each on a line of its own is free to break there.
	// One engine does and another does not, which is the whole of why this is
	// a field: `f() { echo bg & ( exit 1 ); echo $?; }` comes back with the
	// subshell on the `&`'s line in one listing and on the next line in the
	// other.
	BackgroundKeepsTheLine bool

	// ElifWrittenAsANestedIf writes an `elif` out as an `else` whose body is
	// an `if` of its own.
	//
	// The two are the same program and are not the same tree, so this is the
	// one field here that a round trip cannot survive — which is what makes
	// it a field rather than the printer's default. Measured: one engine
	// expands every `elif` of a listed body and the other keeps the word, and
	// neither is a taste this package may hold.
	ElifWrittenAsANestedIf bool

	// ParameterBracesAsWritten keeps the braces of a `${x}` that was written
	// with them, instead of dropping the pair a plain name does not need.
	//
	// Off by default because the *word* printer is a diagnostic's, and a
	// diagnostic quotes a redirect target the way every shell does: `$e:
	// ambiguous redirect`, never `${e}`. A listing is the other caller and
	// wants the spelling back — `echo "${x}"` reprinted as `echo "$x"` is a
	// different program the moment the next character continues a name, and
	// a person reading the listing cannot see which of the two they have.
	//
	// [Span.Bare] is what records the spelling; this decides whether the
	// printer reads it.
	ParameterBracesAsWritten bool

	// PipeBothWrittenOut writes the `2>&1` that a `|&` stands for, and a
	// plain `|` after it, rather than the operator the script used.
	//
	// The redirection is in the tree either way — see [Redirect.PipeBoth] —
	// so this decides which of the two the reader is shown. Both engines that
	// say a body back write it out, and one of them cannot read the operator
	// at all: `|&` arrived in bash 4, so a listing that kept it would hand
	// bash 3.2 a syntax error for a function it can run.
	PipeBothWrittenOut bool

	// BodyIsAlwaysBraced writes the command handed to [PrintWith] as a brace
	// group where it is not one already.
	//
	// A function's body is the caller: `f() ( … )` and `f() if …; fi` are
	// declarations both engines that list a body write back with `{ … }`
	// around them, so what a person reads is the shape a listing always has
	// rather than the shape the author wrote. It is a normalization and not a
	// round trip — the braces are a node the source did not have — which is
	// why it is asked for rather than assumed.
	BodyIsAlwaysBraced bool

	// AnonymousFunctionInParentheses writes a nameless function as `() {`
	// whichever spelling it was written in, and the keyword standing alone
	// as `() {` around an empty body. A listing's normalization, like the
	// braces above. Measured 2026-10-02 on zsh 5.9.2 through `functions`:
	// `function { : } a` lists as `() {` … `} a`, and a bare `function` as
	// an empty `() {` block (#5148).
	AnonymousFunctionInParentheses bool

	// DoAfterArithmeticOnItsOwnLine is the third of the three `do` questions
	// above, for the arithmetic `for`.
	//
	// A third field and not a reuse of either, because one engine answers it
	// differently from both: it keeps `do` on the line of a `while` and of a
	// `for x in …` and gives the arithmetic form's `do` a line of its own.
	// Measured 2026-09-16 on bash 5.3.20, `declare -f` over `f() { for
	// ((i=0;i<2;i++)); do echo $i; done; }` — `for ((i=0; i<2; i++))` then
	// `do` on the next line, where a `for i in a b` in the same listing is
	// `for i in a b` then `do` and a `while` is `while …; do`.
	DoAfterArithmeticOnItsOwnLine bool

	// EmptyArithmeticForExpressionIsOne writes the `1` that a missing
	// expression of an arithmetic `for` stands for, instead of the nothing
	// the source wrote.
	//
	// All three places, and the value is the same in each: `for ((;;))`
	// comes back `for ((1; 1; 1))` and `for ((i=0;;i++))` comes back
	// `for ((i=0; 1; i++))`. Measured 2026-09-16 on bash 5.3.20 through
	// `declare -f`; the engine that lists a body as source text writes the
	// text back and the third writes the empty expressions as they stood.
	//
	// A normalization rather than a round trip — the `1` is not in the tree
	// — which is why it is asked for and not assumed. It is also the
	// program: an omitted condition is true, and `1` is how that is spelled.
	EmptyArithmeticForExpressionIsOne bool

	// ArithmeticForExpressionsAsWritten writes each of the three expressions
	// of an arithmetic `for` with the blanks the script put *after* it,
	// instead of the trimmed text the fields hold.
	//
	// Both engines that list a body keep that half and drop the other:
	// measured 2026-09-16 on bash 5.3.20 through `declare -f` and on zsh
	// 5.9.2 through `functions`, `for (( i=0 ; i < 3 ; i++ ))` comes back
	// `for ((i=0 ; i < 3 ; i++ ))` and `for ((   i=0;i<3;i++   ))` comes back
	// `for ((i=0; i<3; i++   ))`. So the leading blanks go — the `; ` this
	// writes supplies one — and the trailing blanks stay, which is what makes
	// `i++ ))` rather than `i++))`.
	//
	// The parts come from [ForArithClause.PartsAsWritten], which re-splits
	// the header; a tree built by hand has no header and prints as it always
	// did. Off by default, since a formatter wants the blanks normalized and
	// a round trip wants them not doubled.
	ArithmeticForExpressionsAsWritten bool

	// HereDocumentWordSingleQuoted writes a delimiter that carries any
	// quoting as its literal text inside one pair of single quotes,
	// whichever quoting the source used.
	//
	// The quoting of a delimiter means one thing — the body is literal — so
	// a listing that normalizes it loses nothing a reader needs, and the one
	// engine that does it normalizes every spelling: `<<"EOT"`, `<<\EOT`
	// and `<<E"O"T` all come back `<<'EOT'`, and `<<"E T"` comes back
	// `<<'E T'`. An unquoted delimiter stays bare. Measured 2026-09-16 on
	// bash 5.3.20 through `declare -f`.
	HereDocumentWordSingleQuoted bool

	// HereDocumentStripMarkOmitted writes a `<<-` back as `<<`.
	//
	// The dash asks the *reader* to strip a leading tab from every body
	// line and from the delimiter. A listing has already done that — the
	// body it holds is the stripped one — so the mark has no work left to
	// do, and one shell takes it off while another leaves it on.
	//
	// Measured 2026-09-29 by defining `f() { cat <<-x <<-y … }` with real
	// tabs and asking each shell to print it back:
	//
	//	zsh 5.9.2     `cat <<x <<y`    body at column 0
	//	bash 5.3.20   `cat <<-x <<-y`  body at column 0
	//	ksh93u+       `cat <<-x <<-y`  body still tab-indented
	//
	// **Two columns reach this and the third does not**, which is why it is
	// a bool rather than a third named form: ksh93 writes the definition's
	// own source text back — see Diagnostics.FunctionListingIsSourceText —
	// so its tabs survive because nothing reprinted them. The zero value
	// keeps the mark, which is bash's answer and the safe one for a
	// fallback path that might print a body nobody stripped.
	HereDocumentStripMarkOmitted bool

	// RedirectDescriptor is what a written-back redirection does with the
	// descriptor on its left when that descriptor is the operator's own
	// default — `1` for the writing operators, `0` for the reading ones.
	//
	// Three columns and three answers, measured 2026-09-29 by defining a
	// function and asking each shell to print it back:
	//
	//	written     zsh 5.9.2   bash 5.3.20   ksh93u+
	//	>&2         >&2         1>&2          >&2
	//	1>&2        >&2         1>&2          1>&2
	//	<&3         <&3         0<&3          <&3
	//	1>out       > out       > out         1>out
	//	>out        > out       > out         >out
	//
	// So they disagree on two independent things and no single verb covers
	// them: bash **adds** the default to a duplication and **drops** it from
	// a file redirection, zsh drops it from both, and ksh93 says back
	// exactly what was written in either. That is why this is a named form
	// rather than a bool — a "normalize descriptors" switch would have to be
	// on and off at once for bash.
	//
	// **Before #4436 this printer had one unconditional rule, measured on
	// bash, applied to every column — and it was not bash's either.** The
	// duplication half was bash's and the file half was not, so the tree
	// printed `1>&2` for zsh, which drops it, and `1> out` for bash, which
	// does not. ksh93 came out right by accident of the same mixture. A
	// single column's measurement had been generalised to a panel, and it
	// survived because the two columns it was wrong about were wrong in
	// ways nothing had a row for. The table above is the row.
	RedirectDescriptor RedirectDescriptorForm

	// CoprocessDefaultName is written where a coprocess over a **compound**
	// command was given no name of its own, and is empty where the name is
	// left out.
	//
	// A name may only be written before a compound command — with a simple
	// one the first word is the command — so the two shapes are two
	// constructs and the engine that lists a body writes them apart.
	// Measured 2026-09-22 on bash 5.3.20 through `type`:
	//
	//	coproc ( : )              coproc COPROC ( : )
	//	coproc { :; }             coproc COPROC { … }
	//	coproc while …; done      coproc COPROC while …; done
	//	coproc cat                coproc cat
	//	coproc NM ( : )           coproc NM ( : )
	//
	// The name is not in the tree for the first three: nothing was written
	// there, and what the coprocess ends up called is the runner's answer.
	// So this is a normalization the caller asks for, in the caller's own
	// spelling — a shell whose default is called something else says so
	// here rather than being hard-coded into the printer.
	CoprocessDefaultName string

	// BlankBeforeAWordlessRedirection writes the blank that separates a
	// command from its redirections even where there is no command in front
	// of it — `> out` written back as ` > out`.
	//
	// A command may be nothing but redirections, and then the blank has
	// nothing to separate. The two engines answer it differently and both
	// answers are visible in a listed body, measured 2026-09-22 over
	// `f() { > out; }`: bash writes `> out` and zsh writes ` > out`. It
	// reaches the inside of a substitution too, which is where it costs
	// something a reader notices — `echo $(< x1)` comes back `echo $( < x1)`
	// with the blank and `echo $(< x1)` without it.
	BlankBeforeAWordlessRedirection bool

	// BlankAfterAnAssignment writes a blank after every assignment word,
	// which then stands in for the separator to whatever follows it —
	// `url=x` written back as `url=x `.
	//
	// One engine listing a body does this and the other does not. Measured
	// 2026-09-29 on zsh 5.9.2 through `which`, one function per row, the
	// trailing `|` marking the end of the line:
	//
	//	url=x              url=x |             print y      print y|
	//	a=1 b=2            a=1 b=2 |           true         true|
	//	a=1 print x        a=1 print x|        unset x      unset x|
	//	typeset -x a=1     typeset -x a=1 |    print url=x  print url=x|
	//	local url=x        local url=x |
	//	url=x > /dev/null  url=x  > /dev/null|
	//
	// **The assignment word is the noun, not the command**, and two rows say
	// so in opposite directions. `a=1 print x` takes no trailing blank, so it
	// is not a command holding an assignment that gains one; and
	// `typeset -x a=1` takes one although its *last* word is the assignment
	// only because the operand was written last. A rule keyed on the command
	// agrees with every other row in this table.
	//
	// Two blanks appear where a redirection follows, because that one is
	// written separately from the separator this stands in for. That row is
	// what says the blank belongs to the word rather than being the ordinary
	// separator moved.
	//
	// `print url=x` is the control that keeps it to assignments the grammar
	// read as assignments: the same characters as an argument to a command
	// that takes none are just a word.
	BlankAfterAnAssignment bool

	// AnsiCQuotedWordIsItsValue writes a `$'…'` as an ordinary single-quoted
	// string holding the characters it stands for, decoded by this function.
	//
	// A function and not a flag, because **what an escape comes to is the
	// dialect's answer** and nothing under syntax may hold one: `\e`, `\E`,
	// `\cX`, `\u` and an unknown escape are each a semantics axis, so the
	// only correct decoder is the caller's. Nil leaves the word spelled as
	// it was written, which is what every caller but one wants — a formatter
	// that rewrote `$'\t'` into a literal tab would be changing the
	// program's text.
	//
	// Measured 2026-09-16 on bash 5.3.20 through `declare -f`: `echo
	// $'a\tb'` comes back as `echo` and the two characters in single
	// quotes, `x=$'\t'` the same, and so do `echo ${x-$'\t'}`, a `case`
	// subject and a `[[ ]]` operand. A quote in the value comes back as the
	// closed-escaped-reopened spelling every shell writes. ksh93 and zsh
	// write the word back as it was written and leave this nil.
	AnsiCQuotedWordIsItsValue func(string) string

	// KeywordBodiesTakeTheirOwnLines lays out every construct a reserved word
	// closes — `fi`, `done`, `esac` — as Lines would, while a list's own
	// statements follow the source's line breaks and a block the brackets
	// close stays where it was written.
	//
	// It is the arrangement one engine reprints the **inside of a command
	// substitution** with, and it is a third answer rather than either of the
	// two above. Measured 2026-09-19 on bash 5.3.20 through `declare -f`:
	// `$(a >/dev/null;b)` comes back `$(a > /dev/null; b)` on one line and
	// `$(echo one` + newline + `echo two)` keeps its newline, which is what
	// Lines cannot do; `$(if true; then echo x; fi)` comes back over three
	// lines with `echo x` indented and `fi` at the margin, which is what
	// leaving Lines off cannot do. A brace group and a subshell stay on the
	// line either way — `$({ a; b; })` and `$( (a; b) )` come back
	// `$({ a; b; })` and `$( ( a; b ))` — so the split is the closing token
	// and not the construct's depth.
	//
	// Meaningless with Lines on, which already breaks everything.
	KeywordBodiesTakeTheirOwnLines bool

	// CommandSubstitutionIsReprinted writes the inside of a `$( … )` back
	// through this function rather than as the source text the span holds.
	//
	// A [Span] of kind [CommandSubst] keeps its body unparsed, so a printer
	// has nothing to lay out and writes the characters back; one engine
	// re-reads the body and prints it through the same listing, so
	// `$(a >/dev/null;b)` comes back `$(a > /dev/null; b)`. Doing that here
	// would mean the printer re-entering the parser with a dialect it does
	// not hold, so the caller supplies the whole step: parse with its own
	// grammar, print with its own arrangement, hand back the text.
	//
	// `false` means the body was not re-read — it does not parse, which is
	// reachable because a body is not read until the substitution runs — and
	// the span is written back exactly as it stands. That is best effort by
	// design: a listing that refused to print a program it can otherwise run
	// would be a worse answer than a body written out as the author typed it.
	//
	// Asked of `$( … )` alone. A backquoted substitution is written back as
	// written — measured on bash 5.3.20, “ `a >/dev/null;b` “ comes back
	// unchanged where the `$( )` spelling beside it is reprinted — and so is
	// a body whose first character is `(`, because the parentheses that come
	// back decide whether the text re-reads as arithmetic: `$((a; b))` is
	// written back as it stands there and `$( (a; b) )` becomes
	// `$( ( a; b ))`.
	//
	// ksh93 and zsh write every substitution back as it was written and leave
	// this nil, as does a formatter, which must not edit what it lays out.
	CommandSubstitutionIsReprinted func(string) (string, bool)

	// ParameterExpansionIsReprinted writes a `${ … }`'s own text back through
	// this function before it is written out.
	//
	// The same shape as CommandSubstitutionIsReprinted and for the same
	// reason one construct over: a [Span] of kind [ParamExp] holds its
	// operand as **text**, so a substitution written inside one — the word a
	// `-` substitutes, the replacement of a `/`, and every other operand
	// whose operand can hold one — is not a tree this printer can reach. The
	// one engine that lays a substitution's body out reaches inside a
	// `${ … }` too: measured 2026-09-20 on bash 5.3.20 through `declare -f`,
	// `echo ${v-$(a;b)}` comes back `echo ${v-$(a; b)}` and
	// `echo ${v-$(a >/dev/null;b)}` comes back
	// `echo ${v-$(a > /dev/null; b)}`, the redirection respaced with the
	// separator.
	//
	// Handed the span's text and not its parts, because what the caller has
	// to do is find the substitutions in it — which needs its own grammar —
	// and reprint them without touching a byte of anything else. `false`
	// means nothing was reached and the text is written as it stands, which
	// is the same best-effort answer the field above gives.
	//
	// Asked of the braced spelling alone: a bare `$x` has no operand to hold
	// anything. Left nil by every arrangement that does not reprint a
	// substitution's body either.
	ParameterExpansionIsReprinted func(string) (string, bool)

	// FunctionHeader is how a function declaration's header is spelled: the
	// `function` keyword, the `()`, or both.
	FunctionHeader FunctionHeader

	// BraceAfterAFunctionHeaderOnItsOwnLine gives the `{` that opens a
	// declared body a line of its own, the way the first statement of a
	// block does under OutermostBraceOpensALine.
	//
	// Only where the header was written with `()`; the keyword form already
	// decides this for itself, since a name list is greedy and only a
	// newline ends it.
	BraceAfterAFunctionHeaderOnItsOwnLine bool

	// StatementsShareALineOutsideADeclaration joins the statements of a block
	// with Separator and a blank rather than giving each a line, and keeps a
	// brace group or a subshell on the line it started — everywhere but inside
	// a function declaration's body, where Lines applies as it stands.
	//
	// One field with a boundary in its name, because the boundary is what was
	// measured. A canonicalizer that writes a whole script back uses one
	// arrangement for a declaration's body and another for everything outside
	// one, and the two differ in exactly this: `{ echo a; echo b; }` written at
	// file scope comes back on one line, and the same group inside `f() { … }`
	// comes back over four. It is not a depth rule and not a rule about the
	// outermost command — `if true; then { echo a; echo b; }; fi` at file scope
	// expands the `if` and leaves the group alone, and `if true; then f() {
	// echo a; echo b; }; fi` expands the declaration's group while joining the
	// `if`'s own body. The declaration is the whole of the boundary.
	//
	// What a keyword opens still takes lines of its own: `if a; then` ends its
	// line, the body is indented, and `fi` starts one. So this is narrower than
	// turning Lines off, which flattens the construct as well.
	StatementsShareALineOutsideADeclaration bool

	// FileFollowsTheSourceUnits writes the statements of a *file* the way the
	// input laid them out: a statement that begins on a later line than the one
	// before it ended begins a line of its own, and a gap of any size between
	// two of them becomes exactly one blank line.
	//
	// The top level only, which is what makes it a separate answer from Lines.
	// Inside a block the same arrangement ignores the source's lines entirely —
	// `if true; then` over three input lines comes back with its body joined on
	// one — so a file's own line structure is preserved at the level a reader
	// has by the line and nowhere below it.
	//
	// A gap before the *first* statement counts, which is how a shebang and a
	// leading comment block leave a blank line behind in an engine whose tree
	// holds no comments.
	FileFollowsTheSourceUnits bool

	// TrailingBlankLine ends the output with a blank line.
	//
	// Its own field rather than part of the one above, because the two come
	// apart on a route that exists: an engine that writes a file back unit by
	// unit and then meets a parse failure writes every unit it read and no
	// blank line at the end, where the same engine reaching the end of the
	// input writes one.
	TrailingBlankLine bool

	// TranslatedWordWrittenPlain writes a `$"…"` back as an ordinary
	// double-quoted string, dropping the mark — see [Span.Translated].
	//
	// Off by default, because the mark is in the tree and a round trip has
	// to give it back: `$"x"` reprinted as `"x"` is a different tree, and
	// the one option that lists these strings would not see it.
	//
	// On for a *listing*, which is measured and was not obvious: the engine
	// that says a function body back drops the mark in all three of the
	// places it says one — shown to a person, written into the environment
	// for a child, and writing a whole script back — so a body reprinted out
	// of a running shell holds plain quotes. The mark is an invocation-time
	// fact there, and it has already been used by the time a body can be
	// listed.
	TranslatedWordWrittenPlain bool
}

Layout is how a printed block is arranged.

Every field is a junction where the shells differ, and the type exists so that this package decides none of them. The zero value keeps the source's own line structure and adds nothing, which is what a round trip wants; a caller that needs a particular shape fills the fields in.

That is the whole point of it being data. An arrangement measured from one shell and written into this package would be that shell's taste living where nothing is supposed to know a shell — and the differences are not small: one puts `then` on the line of its `if` and another on a line of its own, one terminates statements with `;` and another with nothing at all.

type LeadingIndex

type LeadingIndex struct {
	// Index is the subscript as written, flag group included.
	Index *Word
	// Flags is the group it opened with, nil when there was none.
	Flags *SubscriptFlags
	// Range is the pair it was written as, nil when it was not one. The same
	// field ParamExpr.IndexRange is, and for the same reason: a link that
	// carried only the text would have its pair separated out of the
	// *expanded* text at the run, where a comma that arrived through a
	// substitution reads as a separator (#2160).
	Range *SubscriptRange
	// Text is the subscript as the source spelled it, for the reason
	// ParamExpr.IndexText is.
	Text string
}

LeadingIndex is one subscript of a chain: everything ParamExpr.Index and ParamExpr.IndexFlags hold for the last one, for one of the ones before it.

A pair rather than a bare word, because a subscript is the two together — `${a[(r)x][1]}` opens with a flag group, and a link that carried only the text would search nothing and read `(r)x` as arithmetic.

type Lexer

type Lexer struct {
	// contains filtered or unexported fields
}

Lexer turns shell source into tokens.

It is written from docs/spec/grammar/tokenization.md. Two properties are requirements rather than niceties, because a shell re-parses the current line on every keypress to highlight it:

  • It never panics, on any input.
  • Incomplete input is distinguishable from invalid input, because a half typed line is the normal case at a prompt and is not an error yet.

func NewLexer

func NewLexer(src string, d Dialect) *Lexer

NewLexer returns a Lexer over src.

func (*Lexer) CommentRanToTheEnd added in v0.0.12

func (l *Lexer) CommentRanToTheEnd() bool

CommentRanToTheEnd reports whether the last comment this lexer skipped was ended by the text running out rather than by a newline.

It answers the comment's half of the seam [Parser.carryOpenWord] answers for a quote: an alias value is substituted into a line, so a `#` in it that nothing closes comments out the rest of *that line* and not only the rest of the value. Measured 2026-09-22, with `shopt -s expand_aliases` and `alias c=#` on the line before:

echo pre; c; echo post     bash 5.3.20 writes `pre` and nothing else
c foo                      nothing at all
`alias c='echo #'`, c foo  an empty line — the argument is commented out

This shell read the value as no tokens at all and then went on reading the input, so the first of those was a syntax error at the `;` (#4147).

func (*Lexer) Err

func (l *Lexer) Err() error

Err reports why lexing stopped early, or nil.

func (*Lexer) Incomplete

func (l *Lexer) Incomplete() bool

Incomplete reports whether the input ended in the middle of something that could still be finished — an unclosed quote, a trailing line continuation. A prompt should ask for another line; a script should report an error.

func (*Lexer) Next

func (l *Lexer) Next() Token

Next returns the next token. At the end of input it returns TokEOF forever.

func (*Lexer) Open

func (l *Lexer) Open() string

Open is what the lexer was inside when the input ran out, spelled as it is written: `'`, `"`, `${`, “ ` “, `<<`. Empty when the input was whole, or when what ran out was the parser's rather than the lexer's.

One thing rather than a stack. What the lexer is inside nests through recursion — a substitution runs a parser of its own — so the innermost is the one this level knows about, and the outer ones are the callers'.

func (*Lexer) OpenHeredocExpands added in v0.0.11

func (l *Lexer) OpenHeredocExpands() bool

OpenHeredocExpands reports whether the here-document Lexer.OpenInnermost names was opened with an **unquoted** delimiter, so that its body is shell text rather than literal lines.

False for every other answer, and for a quoted delimiter. Both spellings are `<<` to OpenInnermost, because what the *next line begins inside* is the same either way; this is the second half of that question, and it is asked by a reader that has to know whether it resolved a line continuation in the body it just took.

func (*Lexer) OpenInnermost added in v0.0.7

func (l *Lexer) OpenInnermost() string

OpenInnermost is Open for a reader that needs the **innermost** thing still open rather than the outermost: the here-document inside a `$(`, where Open says `$(`.

Measured 2026-09-16 on bash 5.3.20 from a script with history expansion on: `echo $(cat <<EOF` / `echo !!` / `EOF` / `)` writes `echo !!` — the body line is a here-document's and is not expanded, inside a substitution exactly as outside one — where reading only the outer construct expanded it. See driver's history gate, the one caller.

func (*Lexer) Tokens

func (l *Lexer) Tokens() []Token

Tokens reads the whole input. It is a convenience for tests and for callers that are not streaming; it stops at TokEOF or at the first error.

type LoopClause

type LoopClause struct {
	// Until inverts the sense of the condition.
	Until bool
	Cond  []*Stmt
	Body  []*Stmt
	Start Pos
	Stop  Pos
	// contains filtered or unexported fields
}

LoopClause is `while … do … done` or `until … do … done`.

func (*LoopClause) End

func (c *LoopClause) End() Pos

func (*LoopClause) Pos

func (c *LoopClause) Pos() Pos

type ModifierArg added in v0.0.8

type ModifierArg int

ModifierArg is what a modifier letter may carry after it inside its own segment.

const (
	// ModifierNothing: the letter is the whole segment, and anything after
	// it is the complaint that names nothing.
	ModifierNothing ModifierArg = iota
	// ModifierCount: an optional decimal count, which `h` and `t` alone
	// take. `${x:h2}` is the head twice over and `${x:h:2}` is a `2` that
	// names no modifier — the digit belongs to the letter or to nothing.
	//
	// The **braced** spelling alone. A bare `$x:h2` is the head once with a
	// literal `2` after it, measured; see Dialect.BareParamModifiers.
	ModifierCount
	// ModifierSubst: a delimited pattern and replacement, `s/l/r/`, whose
	// delimiter is whatever byte follows the letter.
	ModifierSubst
)

type NamespaceClause added in v0.0.10

type NamespaceClause struct {
	// Name is the word standing where the namespace's name belongs, **as it
	// was written** and never expanded.
	//
	// Measured rather than assumed: `n=ns; namespace $n { x=1; }` reports
	// `.$n: invalid variable name` and runs nothing, so the shell is judging
	// the text rather than a value. A word that is no name at all is
	// therefore carried here and refused when the clause runs, at status 1 —
	// `namespace .ns { x=1; }` is `.ns: is not an identifier` and not a parse
	// error, which is the same stage split [ForClause.RefusedName] records.
	Name  string
	List  []*Stmt
	Start Pos
	Stop  Pos
	// contains filtered or unexported fields
}

NamespaceClause is `namespace NAME { list }`, a name-resolution region over a compound named after it.

One column in the panel has the word — ksh93 — and it is reserved there only where a command may begin, which is what Dialect.NamespaceBlock gates: `namespace=5` assigns and `echo namespace` prints the operand in all six columns, and both have to go on doing so.

The body is a list rather than a Group node, the way a TryClause's halves are: the braces are mandatory punctuation of this production, so a nested group node would carry no fact the construct does not already state. Redirections belong to the construct — `namespace ns { echo a; } > out` sends the body's output there, measured 2026-09-19 on ksh93u+ 2012-08-01.

func (*NamespaceClause) End added in v0.0.10

func (c *NamespaceClause) End() Pos

func (*NamespaceClause) Pos added in v0.0.10

func (c *NamespaceClause) Pos() Pos

type Node

type Node interface {
	Pos() Pos
	End() Pos
}

Node is anything in the tree.

type Open

type Open struct {
	// Word is the keyword that opened it: `if`, `for`, `case`, `{`, or a
	// clause's own word such as `then` or `do`.
	Word string
	// Line is where it was opened, which is what a diagnostic names when the
	// construct began further up than the failure.
	Line int
	// Construct distinguishes a compound command from a clause of one. `if`
	// is a construct and the `then` inside it is not.
	Construct bool
}

Open is one thing the parser is inside.

type ParamExpr

type ParamExpr struct {
	// Name is the parameter: a name, a digit, or a special such as @ * # ?.
	Name string
	// Index is the `[…]` subscript, nil when absent. Its base is a dialect
	// question — bash and ksh93 count from 0, zsh from 1 — so nothing here
	// interprets it.
	Index *Word
	// EnclosedInDoubleQuotes records that the expansion was written inside
	// double quotes, which the subscript word above cannot say: it is read
	// again from the raw text between the brackets, so a `\"` in it is the
	// same span either way.
	//
	// The difference is the outer quoting's, and one reading of a subscript
	// sees it. Measured 2026-10-01 on zsh 5.9.2, a script file under `env -i`
	// with both `a"b` and `a\"b` planted as keys: `"${h[a\"b]}"` looks up
	// `a"b` and `${h[a\"b]}` looks up `a\"b`, while the search
	// `"${a[(r)a\"b]}"` still finds `a\"b` — so the double quotes spend the
	// escape for a key and not for a pattern. bash 5.3.20 and ksh93u+ look up
	// `a"b` both ways (#5270).
	EnclosedInDoubleQuotes bool
	// InsideASubscript records that the expansion was written inside another
	// expansion's subscript, where a `\"` in its own subscript is spent
	// before that subscript is read as a key, as double quotes spend it. Its
	// search pattern is spent too where the expansion around it is also
	// inside double quotes, which neither of the two does on its own.
	// Measured 2026-10-02 on zsh 5.9.2 (`-f`, `LC_ALL=C`), with both `k"m`
	// and `k\"m` planted as keys of `h` holding `A` and `B`, and `A` and `B`
	// as keys holding `fromA` and `fromB`:
	//
	//	$h[k\"m]            B       the escape kept, at the top
	//	$h[$h[k\"m]]        fromA   and spent one level in
	//	"$h[$h[k\"m]]"      fromA   quoted or not
	//	"$h[(i)k\"m]"       k\"m    a pattern at the top keeps it
	//	"$h[$h[(i)k\"m]]"   A       and one level in, quoted, does not
	//	$h[$h[(i)k\"m]]     B       while unquoted it still does
	//	$h[$h[k\\m]]        fP      no other escape moves: `\\`, `\a` and `\$`
	//	                            read one level in as they do at the top
	//
	// See SubscriptFlags.PatternSpendsAnEscapedQuote, the fact for the
	// pattern.
	InsideASubscript bool
	// BareIndexText is that same subscript read as *text*: the `[`, whatever
	// stands between the brackets, and the `]`, lexed as a word in the
	// expansion's own quoting. Nil unless the expansion was written without
	// braces and carried a subscript at all.
	//
	// Two readings of one span, kept side by side because nothing here can
	// choose between them. A grammar with BareSubscript reads `$a[1]` as an
	// element; the same grammar can be told at run time that an unbraced
	// name's brackets are ordinary characters, and then the same text is the
	// parameter followed by three of them. Measured on the shell with the
	// construct: the answer is the one in force when the word is *expanded*
	// and not when it was read — a function body written under one answer
	// and called under the other takes the caller's.
	//
	// So the parser records both and the run picks, which is also why this
	// is a word rather than a string: the brackets are text, and text in a
	// word is expanded, split and matched like any other.
	BareIndexText *Word
	// Bare records that the expansion was written **without braces** — `$x`
	// and `$7` rather than `${x}` and `${7}`. It is [Span.Bare] kept on the
	// node, because one diagnostic is decided by the spelling and reaches
	// the node alone: bash writes the `$` back in front of a parameter whose
	// name is not a name — `$7: unbound variable`, `$!: unbound variable` —
	// and drops it for the braced spelling of the very same parameter.
	//
	// False on a node this package did not parse, which is the reading a
	// synthesized reference wants: nothing constructs one for a positional.
	Bare bool
	// Length is `${#x}`.
	Length bool
	// Indirect is `${!x}`. bash alone means indirection by it; ksh93 means
	// something else and does not error, so a dialect that lacks it must
	// refuse rather than guess.
	Indirect bool
	// Prefix is `${!name@}` or `${!name*}`, which yields the *names* of the
	// variables beginning with name rather than any value. It carries the
	// `@` or the `*`, which differ exactly as `$@` and `$*` do: one field
	// each, or one field joined.
	Prefix byte
	// Member is the dotted member path written after the subscript's closing
	// bracket — the `.p` of `${a[1].p}` — with its leading dot, and empty
	// where none was written.
	//
	// The construct is one dialect's compound variable held in an array
	// element: `a[1]=(p=1 q=2)` there, and the member reads back as the
	// ordinary name `a[1].p`, which is what that shell's own `${!a[1].@}`
	// answers and what its whole-shell listing writes beside the array.
	// Before a `]` no field is needed, because `.` is a name byte where the
	// dialect has the construct and `c.p` is one Name; after one the dot has
	// no reading, which is the gap this fills. See [Dialect.DottedName] and
	// [Assign.Member], the same field on the writing side.
	//
	// A lone `.` is a member path too, and it is the one `${!a[1].@}` needs:
	// the names *under* the element, exactly as `${!c.@}` reads the names
	// under `c` with the dot on the end of the Name.
	Member string

	// Inner is the expansion standing where a name would — the `${v}` of
	// `${${v}#a}` — and is nil for the ordinary shape. Name is empty when it
	// is set, because the inner expansion is the whole of that position:
	// `${x${v}}` is a bad substitution rather than a name with one appended.
	//
	// A *word* rather than a ParamExpr, so the shape says only "an expansion
	// stands here" — the inner may be a parameter expansion, a command
	// substitution or an arithmetic one, and all three were measured
	// accepted in the same position.
	Inner *Word

	Op ParamOp
	// Colon records the `:` that extends the test from "unset" to "unset or
	// empty". It is the whole difference between the two rows of
	// conditionals.
	Colon bool

	// Arg is the operand: the word for `-=?+`, the pattern for `#%/`, the
	// offset for a substring. It is a *word* and not a string, because it is
	// itself expanded — `${u:-$(echo sub)}` yields sub.
	Arg *Word
	// Arg2 is the replacement for `/`, or the length for a substring.
	Arg2 *Word
	// ArgText and Arg2Text are the two operands of a substring range as they
	// were **written** — the raw source between the braces, before the lexer
	// took a quote character off anything.
	//
	// Set for that operator alone, because a modifier list is the only
	// operand in the grammar that reads its own *text* rather than a value,
	// and the shell it belongs to reads a brace's body raw. Measured on zsh
	// 5.9.2, 2026-09-12, from a script file so the outer quoting is not what
	// is being tested (#1860):
	//
	//	x="a'.'b"; ${x:s/'.'/:/}   a:b     the pattern is the three
	//	                                   characters `'.'`, and matches
	//	y='a.b';   ${y:s/'.'/:/}   a.b     so it misses a bare dot
	//	w='aQb';   ${w:s/'Q'/X/}   aQb     with no metacharacter in sight
	//	v="a'b";   ${v:s/'/X/}     aXb     one quote is one character
	//	r='a$b';   ${r:s/'$'/X/}   a$b     and a `$` between them is text
	//	a=X; x=aXb; ${x:s/$a/Q/}   aXb     as is a `$` without them
	//	u='a  b';  ${u:s/'  '/_/}  a  b
	//
	// Rows one and two are each other's control: the pattern the quotes are
	// part of matches the value that holds them and misses the one that does
	// not. Row six is the sharper claim — the text is not expanded either,
	// so a word joined from spans is wrong in a second way — and it is why
	// this is the source rather than a reconstruction with the delimiters
	// written back.
	//
	// Empty for a node the parser did not fill, where the reconstruction is
	// all there is. See interp/modifier.go.
	//
	// The two set operators and the two zips fill ArgText too: their operand
	// is the name of an array, and a name that is not one is refused in the
	// characters it was written in — `${x:^ y}` names ` y`, space and all.
	// See interp/arrayzip.go.
	ArgText, Arg2Text string
	// OperandUnreadable is the failure of the operand's **second read**, kept
	// here instead of refusing the file, and nil for every expansion anybody
	// writes.
	//
	// A `${ … }` written inside double quotes is read twice. The scan for the
	// closing brace honors the quotes inside the braces, so `"${v-'$('}"`
	// closes at its own `}` and the line parses; the operand between the `-`
	// and the `}` is then read *again*, and in that read the two single
	// quotes are characters rather than quoting, so the `$(` is a command
	// substitution with nothing to close it. That the quotes stop quoting on
	// the second read is not new and is not this: `"${v+'bar}"` is the same
	// complaint here and in bash alike.
	//
	// What is this is **when** the second read happens. Measured 2026-09-12
	// against bash 5.3.15 and bash 3.2.57, from a script file and through
	// `-c` alike:
	//
	//	v=SET; echo "${v-'$('}"        SET, and nothing is said
	//	unset v; echo "${v-'$('}"      the complaint, and the line is given up
	//	f() { echo "${v-'$('}"; }      defined, silently
	//	bash -n                        takes both
	//
	// So the operand is read when the expansion **reaches** it, and a branch
	// nothing takes is never read at all. A failure there is not the file's
	// to report — the same rule [Parser.parseArithLater] applies to an
	// expression, one operand over — and the run reports it as the failed
	// expansion it is.
	//
	// The panel does not divide on this, because only one column gets far
	// enough to ask: bash 5.3 and bash 3.2 defer, and zsh, ksh93 and dash
	// end the expansion at the first `}` instead, so the construct is a
	// stray quote to them long before an operand exists. bash in POSIX mode
	// is the same three-way — it does not honor the quotes in the brace
	// scan either, and refuses the file at parse time.
	//
	// The error is kept rather than the text re-read later because nothing
	// expands in between: the second read of the same characters cannot come
	// out any other way, so recording it is the whole of the deferral.
	OperandUnreadable error

	// Arg2Enclosed is the *replacement* operand read a second way: as
	// content of the quoting around the expansion, rather than as a word of
	// its own. Nil unless the two readings could differ, which is what makes
	// it the question rather than an alternative to Arg2.
	//
	// Both readings are kept for the reason ArithIndex.Sub keeps the raw text
	// beside the parsed expression: which of the two applies is not a
	// property of the text. The panel divides on it — bash 5.3, that build as
	// `sh` and ksh93 read the operand as a word, so `"${s/a/'$v'}"` is
	// `x$vy`, while bash 3.2 and zsh read it as double-quoted content, where
	// the quotes are two characters of the result and the `$v` between them
	// is still substituted, giving `x'VAL'y`. Unquoted, all five agree with
	// the first reading, which is what says the disagreement belongs to the
	// enclosing context. Answered by
	// interp.Semantics.ReplacementOperandTakesTheEnclosingQuoting (#1209).
	//
	// The pattern operand never takes it: its quotes quote in every column,
	// unanimously, which is why only Arg2 has a second reading.
	Arg2Enclosed *Word

	// RawTail is the source from this expansion's `${` to the end of the word
	// it stands in, kept so that the run can divide that word again.
	//
	// Where a `}` ends a `${ … }` is a grammar question — see
	// [Dialect.QuoteProtectsTheClosingBrace] — and POSIX mode moves the
	// answer in one dialect. The mode is a run-time state, and the word may
	// have been read long before it was entered: one function body, parsed
	// once, answers `[Vx}y]` and then `[Vx}yb'}]` as `set -o posix` moves
	// under it. So the reading cannot be settled while the word is cut, and
	// the text has to survive the cut.
	//
	// It is a *tail* and not a second tree because the re-read is bounded by
	// the word. Measured 2026-09-14: the leftover characters join the
	// enclosing word rather than re-tokenizing it, so `"${v-'a}" x "'}"`
	// stays one argument under both modes and a `;` swallowed into it never
	// becomes a statement. The tree's shape is a fact about the parse in bash
	// too; only one already-cut word's internal division is the run's.
	//
	// Set only where the dialect has somewhere to move to — see
	// [Dialect.QuoteProtectsTheClosingBraceInPosixMode] — and only for an
	// expansion whose body holds a quote the scan had to rule on, so the
	// dialects with no POSIX mode carry nothing. Empty everywhere else, and
	// [interp] reads it only when the run's reading differs from RawTailRead.
	//
	// On [ParamExpr] rather than on [Span] or [Word] for a priced reason:
	// syntax/nodesize_test.go budgets Word at 48 bytes and Span at 64, and
	// one measured interactive startup keeps 149,937 Words and 185,748 Spans,
	// so a 16-byte string on Span is 3.0MB of resident memory (#2073). ArgText
	// and Arg2Text are here for the same kind of reason. The string is a slice
	// of the input and allocates nothing (#2604).
	RawTail string
	// RawTailRead is the reading RawTail was cut under, which is what says
	// whether the run has to cut it again.
	//
	// Kept beside the text rather than derived from the dialect, because the
	// dialect field it would be derived from is the one POSIX mode replaces:
	// a word parsed *inside* the mode and expanded outside it moves back, and
	// there would be nothing left to compare against. Measured — bash answers
	// that case with the default reading, and the corpus row
	// core/a-quoted-brace-in-a-word-operand-parsed-in-posix-mode-and-expanded-outside-it
	// is where it is recorded. Meaningless where RawTail is empty.
	RawTailRead BraceQuotePolicy

	// All is `//`, replacing every match rather than the first.
	All bool
	// Anchor is '#' for `/#` or '%' for `/%`, and 0 otherwise. It is
	// independent of All: `${v//#a/X}` carries both, because one shell reads
	// an anchor after the global spelling too and the rest read the
	// character as the pattern's first byte — see
	// interp.Semantics.GlobalReplacementAnchors, which is where the two
	// readings are told apart.
	Anchor byte
	// Transform is ParamTransform's letter — one of Q E P A a K k L U u.
	// It is a byte rather than a word because it is part of the operator:
	// nothing expands it, and `${x@$op}` is a bad substitution.
	Transform byte

	// HasFlags marks a parenthesized flag group at the front of the
	// expansion — `${(U)x}` — which one dialect's grammar has and the rest
	// call a bad substitution. It is a marker of its own because the group
	// may legally be empty: `${()x}` is `${x}`.
	HasFlags bool
	// Flags is the group's letters, in written order, with any separator
	// arguments stripped: `${(Uq)x}` carries "Uq" and `${(s.:.)x}` "s".
	//
	// The `+` or `-` a `q` ate is stripped too, into QuoteModifier, so that
	// every `-` left in here is the sort flag and nothing has to ask which
	// reading a given one had.
	Flags string
	// QuoteModifier is the `+` or `-` the group's `q` ate — `q-` for minimal
	// quoting and `q+` for the extended form — and 0 when the group has
	// neither. It is not in Flags, because the same two characters name a
	// flag apiece there: `-` is the signed-numeric sort, and `+` is nothing
	// at all.
	//
	// Which of the two readings a written `-` had is settled here, once, by
	// scanParamFlags. Measured on zsh 5.9.2, 2026-09-08, with
	// `b=(-1 -10 -3 2 10)`:
	//
	//	${(o-)b}     -10 -3 -1 2 10   signed, so a lone `-` is a sort flag
	//	${(oq-)b}    -1 -10 -3 10 2   lexical: the `q` ate the `-`
	//	${(oq--)b}   -10 -3 -1 2 10   signed: the *second* `-` is the flag
	//	${(oq+-)b}   -10 -3 -1 2 10   signed: a `q+` had already taken it
	//	${(qU-)v}    A\ B             adjacency is literal, not associative
	//
	// The character is always the one directly behind the group's first `q`;
	// the scan refuses it anywhere else. So a reading that needs its
	// position has it as one past that `q` rather than as a second copy of
	// this rule.
	QuoteModifier byte
	// SplitSep and JoinSep are the arguments of the `s` and `j` flags. An
	// empty SplitSep with an `s` in Flags is meaningful — it splits into
	// characters — which HasFlags plus the letter already distinguish from
	// no `s` at all.
	SplitSep string
	JoinSep  string
	// PadLeft and PadRight are the arguments of the `l` and `r` flags, which
	// pad a word out to a width. Whether either flag was written at all is
	// Flags' question, exactly as it is for `s`: a group may name a width of
	// zero, and a zero width is not the same as no padding — see ParamPad.
	PadLeft  ParamPad
	PadRight ParamPad
	// ShellSplitOpts is the option letters the shell-word split runs with:
	// what the `Z` flag's delimited arguments came to, `${(Z+Cn+)v}`
	// carrying "Cn".
	//
	// It is the *accumulated* set rather than one argument's text, because
	// the group may write the flag more than once and may write its
	// argumentless spelling, `z`, in among them. Measured on zsh 5.9.2, the
	// one shell with the flag, on `$'a # h\nb'`:
	//
	//	${(Z+C+Z+n+)v}   a  b          two `Z` arguments union
	//	${(Z+n+Z+C+)v}   a  b          in either order
	//	${(zZ+n+)v}      a # h  b      a `Z` behind a `z` still counts
	//	${(Z+n+z)v}      a # h ; b     but a `z` behind it clears the set
	//	${(Z+C+zZ+n+)v}  a # h  b      clearing what stands before it only
	//	${(Z+n+Z::)v}    a # h  b      and an empty argument adds nothing
	//
	// So `z` is not "the `Z` flag with no letters" written once and for all:
	// it is a reset, and the reset happens where it is written. Reading the
	// last argument alone — which is what one assignment per `Z` came to —
	// answers the first two rows with `n` only and loses the dropped
	// comment.
	//
	// Empty is meaningful and is not the same as no split at all. `${(Z::)v}`
	// on `a  b` is the value unchanged, double space and all, where
	// `${(z)v}` is `a b` — so an empty option list leaves the flag doing
	// nothing, while `z` splits with no options set. Which of the two a
	// group means is Flags' question: a `z` in it splits, a `Z` in it splits
	// when this is non-empty.
	ShellSplitOpts string
	// MatchIndex is the `I` flag's argument as written — an arithmetic
	// expression naming which match a substring search or a substitution
	// acts on — and empty where the group wrote none. The last one written
	// is the one read.
	MatchIndex string
	// EscapeOpts is the option letters the `g` flag's escape reading runs
	// with: `o` for octal escapes that need no leading zero, `e` for the
	// `\M-x` family, and `c` for `^X`. `${(g:oe:)v}` carries "oe" and the
	// bare `${(g::)v}` carries nothing at all.
	//
	// Accumulated rather than assigned, for the reason ShellSplitOpts is:
	// a group may write the flag twice and the sets union. Measured on zsh
	// 5.9.2, the one shell with the flag, on `X\EY` and `X\101Y`:
	//
	//	${(g::g:e:)v}   the escape character   an empty argument adds none
	//	${(g:e:g::)v}   the escape character   and takes none away
	//	${(g:o:g::)w}   A                      so `o` survives the second
	//
	// Empty is meaningful and is not the same as no `g` at all: `${(g::)v}`
	// reads escapes with no option set, where a group with no `g` reads none.
	// Which of the two it is, is Flags' question.
	EscapeOpts string
	// FlagsErrPos is the 1-based position, counted from the `$`, of the
	// first character the flag group could not read, and 0 when it read
	// cleanly. The error is the interpreter's to report — reached in a
	// branch never taken, it is no error at all, which is measured — so the
	// node carries the position and Src carries the text.
	FlagsErrPos int

	// BareIndexUnclosed says an unbraced `$name` was written with a `[`
	// directly behind it that the word never closes, so the lexer gave the
	// brackets back as text. Set only where the dialect has the construct
	// and the name could carry one.
	//
	// It is a fact about the *word*, not about the expansion, which is why
	// the parser fills it rather than the lexer: a `[` is unclosed only once
	// the word has ended, and scanBareParam has not seen the end of the word
	// when it gives the characters back.
	//
	// The run decides what it means, because the run decides whether the
	// brackets were a subscript at all — see interp.Semantics's
	// BareSubscriptIsASubscript. Measured on zsh 5.9.2, 2026-09-12, with
	// `setopt noglob` so a bad pattern cannot be what is reported, and
	// `a=(xx yy zz)`:
	//
	//	$a[1 2]      invalid subscript   the word ends at the blank
	//	$a[          invalid subscript
	//	x$a[1        invalid subscript   mid-word, so not about the head
	//	"$a[1"       invalid subscript   nor about quoting
	//	$nosuch[1    invalid subscript   nor about the value
	//	${a}[1       `xx yy zz[1`        braced, so not this construct
	//	$a]1[        `xx yy zz]1[`       nothing directly behind the name
	//	$a[$(: ]; echo 2)]  text         a `]` later in the word closes it
	//	setopt ksharrays; $a[   `xx[`    the brackets are not a subscript
	//
	// Rows one to five and row nine are the pair that says it is the axis
	// and the word between them, and row eight is why the test is "the word
	// closes it" rather than "the lexer took it": the lexer gives those
	// characters back too, and that one is not an error in any column.
	BareIndexUnclosed bool

	// FlagsErrTail is the source of the *word* the group stands in, from
	// this expansion's `$` to the end of that word, and is set only where
	// FlagsErrPos is. It is what the report quotes.
	//
	// Measured on zsh 5.9.2, 2026-09-12, and it is the word rather than the
	// line — which is what the issue behind it had recorded (#1647):
	//
	//	print -r -- "[${(Z:x:)v}]"   `${(Z:x:)v}]"`     to the end of the word
	//	echo ${(g:x:)v} tail         `${(g:x:)v}`       and no further
	//	echo A${(Z:x:)v}B ; echo n   `${(Z:x:)v}B`      through the literal
	//	echo "${(!)v}"$'q'tail       `${(!)v}"$'q'tail` across three quotings
	//	echo $( echo ${(!)v} in )    `${(!)v}`          the inner word's end
	//
	// Row two is the discriminating one: a line would have carried ` tail`
	// with it, and a bare expansion would have carried nothing in rows one,
	// three and four.
	//
	// Empty where the word could not be recovered — a word the grammar
	// supplied rather than one a script wrote has no source to slice, and an
	// alias body is lexed apart from the input these offsets count in. The
	// report falls back to the expansion alone, which is what it always said.
	FlagsErrTail string

	// IndexFlags is the parenthesized flag group the subscript opened with,
	// nil when there was none. Index keeps the subscript as written, and the
	// operand behind the group is IndexFlags.Arg — see Subscript, which is
	// what a reading should use.
	IndexFlags *SubscriptFlags

	// IndexRange is the subscript read as a *pair* whose ends each carry a
	// flag group of their own, and nil for every other subscript — including
	// a pair of plain arithmetic ends, which is read from the subscript's
	// text at the run as it always was. IndexFlags is nil where this is set,
	// because the group at the front belongs to the pair's first end rather
	// than to the subscript as a whole.
	//
	// Index still keeps the whole subscript as written. See SubscriptRange.
	IndexRange *SubscriptRange

	// IndexDots is the subscript read as the two ends of a `lo..hi` range,
	// split at the first `..` the source wrote, and nil for every other
	// subscript. Only a grammar with [Dialect.SubscriptDotRange] fills it.
	//
	// Split while reading rather than at the run because the separator has to
	// have been *written*: measured on ksh93u+ 2012-08-01, `x=1..3;
	// ${a[$x]}` is the arithmetic refusal `invalid character in expression`,
	// where `${a[1..3]}` and `${a["1..3"]}` are both the range. Index still
	// keeps the whole subscript, so any reading that does not know the range
	// sees the text it always saw. See interp/dotrange.go.
	IndexDots *SubscriptRange

	// IndexText is the subscript as it was written, before any expansion —
	// the text between the brackets, and empty where there were none.
	//
	// The same field Assign carries and for the same reason: a refusal that
	// quotes a subscript back quotes what was *typed*, and by the time it is
	// known the word has been expanded. See Assign.IndexText.
	//
	// The last subscript's, as Index is. A chain's earlier links are in
	// Leading and none of them reaches a refusal that names one.
	IndexText string

	// Leading holds the subscripts written *before* Index, in written order,
	// where the grammar lets several be chained: `${m[k][2]}` carries `k`
	// here and `2` in Index.
	//
	// Index is the last one and not the first, which looks backwards and is
	// the whole point. Every question anything asks about a subscripted
	// expansion — whether it is the whole array, whether it is a range, how
	// many fields it makes, whether `${#…}` is a count or a width — is a
	// question about the *final* subscript, because that is the one whose
	// result the expansion is. The leading ones only say what it is read
	// against. Keeping the final one where a single subscript has always
	// lived means every one of those readings answers a chain correctly
	// without being told chains exist; putting the first one there would
	// have needed each of them found and changed, and the one that was
	// missed would have answered a plausible value.
	Leading []LeadingIndex

	// TildeFlags is how many `~` characters were written between the `${`
	// and the rest of the expansion — `${~x}` carries 1 and `${~~x}` carries
	// 2. Zero is the ordinary expansion.
	//
	// A count and not a bool because parity is the meaning: an odd number
	// makes the result eligible for tilde expansion and filename generation
	// and an even one refuses it, both regardless of the option that would
	// otherwise decide, which is measured. Keeping the count also lets the
	// printer write the span back exactly as written.
	TildeFlags int

	// SplitFlags is how many `=` characters were written in the same slot —
	// `${=x}` carries 1 and `${==x}` carries 2. Zero is the ordinary
	// expansion.
	//
	// A count for the same reason TildeFlags is one: parity is the meaning.
	// An odd number splits the substituted value into words on `IFS` and an
	// even one refuses to, both regardless of the option that would
	// otherwise decide, which is measured.
	//
	// Separate from TildeFlags rather than one run of "flag characters",
	// because the two decide different questions and a script writes both:
	// `${=~g}` and `${~=g}` are the same expansion, and each half has to be
	// counted on its own for parity to mean anything.
	SplitFlags int

	// RcExpandFlags is how many `^` characters were written in the same
	// slot — `${^x}` carries 1 and `${^^x}` carries 2. Zero is the ordinary
	// expansion.
	//
	// A count for the reason TildeFlags and SplitFlags are counts: parity is
	// the meaning. An odd number distributes the word the expansion stands
	// in over the elements the expansion came to — `a=(1 2); x${^a}y` is
	// `x1y x2y` where `x${a}y` is `x1 2y` — and an even one refuses to, both
	// regardless of the `RC_EXPAND_PARAM` option that would otherwise
	// decide, which is measured.
	//
	// Its own field rather than a third `~`/`=` counter for the reason those
	// two are separate from each other: the three decide three different
	// questions and a script writes more than one of them, `${=^a}` and
	// `${^=a}` being the same expansion, so each has to be counted on its
	// own for parity to mean anything.
	RcExpandFlags int

	// SetTest marks a `+` written between the `${` and the parameter —
	// `${+name}` — which asks whether the parameter is set and substitutes
	// `1` or `0` rather than its value.
	//
	// A bool and not a count, where TildeFlags is a count: `${++x}` is a bad
	// substitution, measured, so there is no doubled spelling for a parity
	// to be about.
	//
	// It is only the answer when the expansion carries no operator.
	// Measured: `${+v#a}` on `abc` is `bc`, `${+v:-x}` on an unset `v` is
	// `x`, and `${+v=w}` assigns — with an operator written the `+` has no
	// effect at all rather than being refused.
	SetTest bool

	// Bad marks an expansion whose operator the grammar did not recognize,
	// in a dialect that diagnoses that when the expansion is reached rather
	// than when it is read. Src holds the inside of the braces for the
	// report; every other field on a Bad node is meaningless — except Name
	// and Index, which are read before the operator is and stay filled in.
	Bad bool
	// BadTransform narrows Bad to the `@` family in a grammar that *has* the
	// family: the operator was read as `@` and what followed it is not one
	// of the letters. Every other unreadable operator leaves this false,
	// including `@` in a grammar without the family, where the whole
	// construct is unknown rather than the letter.
	//
	// The distinction is not cosmetic. Where the family exists, a bad letter
	// is a failure of the *expansion* — checked against the value, at the
	// point the value is read — and it carries the status a failed expansion
	// carries, while every other unreadable operator is a failure to read
	// the word. Measured: the two exit differently from the same invocation.
	BadTransform bool
	// Src is the raw text between the braces, kept for the diagnostic.
	Src string

	// RefusedAtExpansion is a *parse* failure this dialect raises when the
	// word is expanded rather than while the word is read.
	//
	// Bad next door is the other deferral and a different one: that node is
	// an expansion the grammar could not read, reported by the runner in the
	// shell's own bad-substitution wording. This one is the failure the
	// parser already built — kind, token, tail and all — carried past the
	// parse so that the same sentence is written at the same shape, one
	// stage later.
	//
	// Asked only of a flag group in the one dialect that defers it; see
	// Dialect.FlagGroupRefusedAtExpansion for the measurement and for why
	// every *other* unreadable expansion in that shell is refused while
	// reading.
	RefusedAtExpansion *Error

	Start Pos
	Stop  Pos
}

ParamExpr is a parsed `${ … }`.

func (*ParamExpr) End

func (p *ParamExpr) End() Pos

func (*ParamExpr) Pos

func (p *ParamExpr) Pos() Pos

func (*ParamExpr) Subscript

func (p *ParamExpr) Subscript() *Word

Subscript is the word a subscript's *reading* uses: the operand behind a flag group where there is one, and the whole subscript where there is not.

One accessor rather than a test at each of the dozen places that read a subscript's text, because every one of them wants the same answer: the group is never part of the text being read, whatever it selects.

type ParamOp

type ParamOp uint8

ParamOp is the operator inside `${ … }`.

const (
	// ParamNone is a plain reference, `${x}`.
	ParamNone ParamOp = iota
	// ParamDefault is `-`: substitute the word when the test fires.
	ParamDefault
	// ParamAssign is `=`: assign the word, then substitute it. The only
	// expansion with a side effect.
	ParamAssign
	// ParamError is `?`: write the word as an error and exit.
	ParamError
	// ParamAlternate is `+`: substitute the word when the test does *not*
	// fire.
	ParamAlternate
	// ParamTrimPrefix is `#`, ParamTrimPrefixLong `##`. Doubling the
	// operator is what selects the longer match; there is no greediness
	// syntax inside the pattern.
	ParamTrimPrefix
	ParamTrimPrefixLong
	// ParamTrimSuffix is `%`, ParamTrimSuffixLong `%%`.
	ParamTrimSuffix
	ParamTrimSuffixLong
	// ParamReplace is `/`, with All and Anchor saying which spelling.
	ParamReplace
	// ParamSubstring is `${x:off:len}`.
	ParamSubstring
	// ParamUpperFirst is `^`, ParamLowerFirst `,` and ParamToggleFirst `~`:
	// the first character only, and only when it matches the operator's
	// pattern. `${x^b}` on `abc` is `abc`, because the first character is
	// not a `b`.
	ParamUpperFirst
	ParamLowerFirst
	ParamToggleFirst
	// ParamToggle is `~~`, which swaps the case of every character the
	// pattern matches, as ParamUpper and ParamLower do in one direction.
	ParamToggle
	// ParamUpper is `^^` and ParamLower `,,`. bash alone, so they are
	// rejected unless the dialect has them.
	ParamUpper
	ParamLower
	// ParamZip is `:^` and ParamZipCycle `:^^`: interleave this parameter
	// with the array the operand names, taking one element from each in
	// turn. `:^` stops when the shorter runs out and `:^^` cycles it
	// instead — `(1 2):^(x y z w)` is `1 x 2 y` and `:^^` is
	// `1 x 2 y 1 z 2 w`.
	//
	// Like the two set operators below, the operand is the **name** of
	// another array rather than a pattern. zsh alone, so it is rejected
	// unless the dialect has it.
	ParamZip
	ParamZipCycle
	// ParamExclude is `:#`, ParamKeepOnly its inverse under a flag, and
	// ParamSetDifference `:|` and ParamSetIntersection `:*`. All four choose
	// which *elements* survive, where ParamSubstring chooses which
	// characters do — and the `:` is shared, which is the whole difficulty:
	// `${a:#p}` is an exclusion in one grammar and an offset of `#p` in
	// another, so the character after the colon decides which construct was
	// written before any of it can be evaluated.
	//
	// ParamExclude takes a pattern, matched against a whole element.
	// ParamSetDifference and ParamSetIntersection take the *name* of another
	// array, and compare elements for equality rather than by pattern.
	ParamExclude
	ParamSetDifference
	ParamSetIntersection
	// ParamTransform is `@` followed by one letter naming a transformation
	// of the value — `${x@Q}` quotes it for reuse as input. The letter rides
	// on [ParamExpr.Transform]; the set is fixed, and a letter outside it is
	// a bad substitution rather than a shape of its own.
	ParamTransform
	// ParamAssignAlways is `::=`: assign the word and substitute it, with no
	// test at all.
	//
	// It is not ParamAssign with a second colon. ParamAssign *asks* — is the
	// parameter unset, or unset-or-empty when a colon was written — and
	// substitutes the parameter when the answer is no; this operator asks
	// nothing, so [ParamExpr.Colon] has no meaning on it and there is no
	// non-firing side for the parameter's own value to be returned on.
	//
	// One grammar in the panel has it. The rest read the same characters as
	// a substring whose offset is empty and whose length is `=word`, and
	// fail in arithmetic — `${v::=rst}` is `operand expected at \`=rst'` in
	// bash 5.3, bash 3.2 and ksh93, and a bad substitution in dash.
	ParamAssignAlways
	// ParamElementReplace is `:/`: the elements a pattern matches **whole**
	// are replaced by the word after the second slash and the rest are left
	// exactly as they were.
	//
	// The fourth member of ParamExclude's family and the only one that
	// writes rather than only choosing, so it shares that one's
	// disambiguation — the single character after the colon — and not
	// ParamReplace's reading, which substitutes a matching *span* inside a
	// value. `${x:/foo/Z}` on `foobar` is `foobar` where `${x/foo/Z}` is
	// `Zbar`.
	//
	// The operands are ParamReplace's: Arg is the pattern up to the first
	// unquoted slash and Arg2 the replacement, which is everything after it
	// however many slashes that holds. Omitting the slash is the empty
	// replacement rather than a shape of its own.
	ParamElementReplace
)

func (ParamOp) String

func (o ParamOp) String() string

type ParamPad

type ParamPad struct {
	// Width is the field width as written. It is an arithmetic expression
	// rather than a number — `${(l:COLUMNS-1:)x}` and `${(l:$n:)x}` are both
	// in reach on this machine — so the text is kept and evaluated when the
	// expansion runs.
	Width string
	// Fill is `string1`, repeated as often as needed to fill the space.
	Fill string
	// Insert is `string2`, laid once directly against the word before Fill
	// produces the rest.
	Insert string
	// FillSet and InsertSet report whether the group wrote that argument at
	// all, empty or not.
	FillSet   bool
	InsertSet bool
}

ParamPad is one padding flag's arguments: the width and the two optional fill strings of `${(l:expr::string1::string2:)x}` and of its `r` mirror.

The two Set fields are the reason this is a struct rather than three strings. Whether a fill was *written* is a distinction the shell keeps, measured on zsh 5.9.2 with `v=ab` and `IFS=.`:

${(l:5:)v}         `   ab`   no string1: the padding is spaces
${(l:5:::)v}       `...ab`   string1 written empty: IFS's first character
${(l:5::x:::)v}    `xx.ab`   string2 written empty: the same character,
                             inserted once against the value
${(l:5::x:)v}      `xxxab`   no string2: nothing is inserted

So an absent fill and an empty one are different answers, and only under a default IFS do they agree.

The slots survive a flag written twice, which is also measured: with `v=ab`, `${(l:3::x::y:l:5:)v}` is `xxyab` and `${(l:3::x::y:l:5::z:)v}` is `zzyab`. So a later `l` sets the width always and each fill only where it wrote one — the fields are three slots the group fills in, rather than one argument list the last flag replaces.

type Parser

type Parser struct {

	// Aliases answers what a word stands for, and is the caller's table
	// rather than one this package keeps — see alias.go. Nil expands
	// nothing.
	Aliases Aliases

	// GlobalAliases answers for the kind expanded wherever a word stands
	// rather than only where a command word does, and SuffixAliases for the
	// kind keyed on a command word's extension. Both are the caller's
	// tables, both are nil in a dialect without the kind, and one dialect in
	// the panel has either — see interp.Semantics.GlobalAliases.
	GlobalAliases Aliases
	SuffixAliases Aliases
	// contains filtered or unexported fields
}

Parser builds a syntax tree from tokens.

It is written from docs/spec/grammar/commands.md and carries the lexer's two requirements, for the same reason: a shell re-parses the current line on every keypress, so it never panics, and unfinished input is distinguishable from wrong input because `if x; then` mid-typing is normal.

func NewParser

func NewParser(src string, d Dialect) *Parser

NewParser returns a parser over src.

func NewParserAt

func NewParserAt(src string, d Dialect, first int) *Parser

NewParserAt returns a parser over src whose first line is numbered first.

Input does not always arrive whole. A shell reading its program from a descriptor takes a piece of it, runs that, and comes back for more — the piece is all the parser has, and a diagnostic still has to name the line of the *input* rather than the line of the piece. Nothing else changes: an offset stays relative to src, because it is only ever used to quote the text a node was written as, and that text is here.

first is the number to give src's first line, so 1 is NewParser.

func (*Parser) Err

func (p *Parser) Err() error

Err reports why parsing stopped, or nil.

func (*Parser) Incomplete

func (p *Parser) Incomplete() bool

Incomplete reports whether the input ended part-way through a construct that could still be finished. A prompt should ask for another line.

func (*Parser) InsideASubstitution added in v0.0.10

func (p *Parser) InsideASubstitution()

InsideASubstitution tells this parser that the text it is about to read is a substitution's body — the inside of a `$( )` or of a pair of backquotes, already cut out of the script it came from — so that the end of this input is that construct's closing delimiter rather than the end of a program.

The distinction is only visible where a closing context is *lenient*. Dialect.OpenEndedAndOr is the one that reaches it, and its own documentation has always named "a command substitution's `)`" among the places an and-or may end on its operator — which nothing could ever take, because the parenthesis is cut off before this text is handed over and [Parser.atListEnd] therefore sees the end of the input instead. Measured 2026-09-20 on zsh 5.9.2 from script files:

v=$(echo x &&); print -r -- "[$v]"       [x]
v=$(echo x ||); print -r -- "[$v]"       [x]
v=$( : || );    print -r -- "[$v]"       []
v=`echo x &&`;  print -r -- "[$v]"       [x]
v=$(echo x |);  print -r -- "[$v]"       refused — the pipeline never takes it
v=$(echo x ;;); print -r -- "[$v]"       refused — nor does a stray terminator

Deliberately narrower than "the input cannot be extended". Whether the end of a *script* or of a `-c` string ends such a list is a route question that Dialect.OpenEndedAndOr parks — zsh takes `echo x &&` from a file and still draws a continuation prompt for the same text typed at a terminal — and nothing here answers it. A substitution's body has no such second reading: its end is a delimiter the script wrote.

The counterpart of Parser.InsideProgramParentheses, and a separate fact from it: that one is about `$( )` alone, for a here-document rule the backquoted spelling measurably does not take, and this one is about both.

func (*Parser) InsideProgramParentheses added in v0.0.9

func (p *Parser) InsideProgramParentheses()

InsideProgramParentheses tells this parser that the text it is about to read is the inside of parentheses holding a program — the body of a `$( )`, already cut out of the script it came from.

The parser sets this for itself where it reads such a body in place (see [Lexer.parseToClose]); what it cannot know is that a *caller* has handed it a body it cut earlier, which is what an interpreter does when it parses a substitution's text at expansion time. Told nothing, that read has no parentheses around it and a here-document in it runs to the end of the text where the shell being modeled ends it at the delimiter (#785, #1021).

The counterpart of Parser.SetDialect: a fact about the text this parser was given, handed in by the only place that knows it, and read for what has not been tokenized yet.

func (*Parser) LineShift

func (p *Parser) LineShift() int

LineShift is how many lines the alias bodies this parser expanded added to the numbering, where Dialect.AliasBodyCountsLines says they count.

A caller that reads a program in pieces — one that arrives on a descriptor — retires a piece by counting its newlines and starting the next parser after them. That count is of the text, and an expanded alias contributed lines that were never in the text, so without this the numbering resets at the first refill.

func (*Parser) NextLine

func (p *Parser) NextLine() (*File, bool)

NextLine parses one logical line: the statements up to the newline that ends them, which is more than one line of text when a construct is still open.

It exists because a shell runs what it has read rather than reading everything first. `echo one` on line 1 runs before line 3 fails to parse, which is unanimous across the panel and is why a script that ends badly still does what its good lines said.

The *line* is the unit and not the statement, which is measured: with `echo one; { fi; }` on one line, nothing runs. So everything up to the newline is parsed before any of it is run, and a failure anywhere in it discards the whole line.

The second result is false at the end of the input, and when parsing has already failed.

func (*Parser) Open

func (p *Parser) Open() []Open

Open is what the parser was still inside when the input ran out, outermost first.

Empty when the input was complete, or when it was wrong in some way other than ending too soon. A caller drawing a continuation prompt wants this: it is the difference between "there is more to type" and "there is more to type and it is the `for` from three lines up".

The words are the shell's own keywords, which is all this package knows. What a dialect calls them at a prompt is the dialect's business — one of them says `for` where another would say the clause inside it.

func (*Parser) OpenHeredocExpands added in v0.0.11

func (p *Parser) OpenHeredocExpands() bool

OpenHeredocExpands reports whether the here-document Parser.OpenQuote names was opened with an **unquoted** delimiter, so that its body is shell text rather than literal lines.

The second half of OpenQuote's question, and false for every answer but a here-document's. Both delimiters are spelled `<<` there because what the next physical line *begins inside* is the same either way; what differs is what the reader does to that line before anything else sees it, and a reader recording what it read has to know. See driver's history gate and internal/histjoin, where a continuation the reader resolved is one line of a history entry and one it left alone is two.

func (*Parser) OpenQuote added in v0.0.5

func (p *Parser) OpenQuote() string

OpenQuote is the **lexer's** own unfinished thing, spelled as it is written: `'`, `"`, `$'`, `${`, `$((`, “ ` “ or `<<`. Empty when the input was whole, or when what ran out was the parser's rather than the lexer's.

Open above answers a continuation prompt, which wants the whole stack and the construct that opened it. This answers a different question and wants only the innermost: a shell reading a script one physical line at a time has to know what the *next* line begins inside, because a quote opened on one line is still open on the next and a here-document's body is not shell text at all. See driver's history gate, which is the caller.

A method rather than reading Open()[len-1], because that slice is allocated per call and its last entry is a construct keyword whenever the lexer had nothing open — `then` is not a quote, and a caller reading it as one would silence expansion inside every `if`.

func (*Parser) Parse

func (p *Parser) Parse() *File

Parse reads the whole input.

func (*Parser) ParseArithExpanded added in v0.0.4

func (p *Parser) ParseArithExpanded(src string, at Pos) ArithExpr

ParseArithExpanded is ParseArithFor for text that has **already** been expanded: a subscript and a substring's range, which are words their caller expands before anything arithmetic happens.

The difference is what a `$` means. ParseArithFor is handed a program's text and leaves an expression with an expansion in it alone — nil, for the interpreter to expand and read when it runs — because `$(( $x$y ))` is the *result* of substituting. Text that arrives here has been through that already, so a `$` still standing in it is an ordinary character, and it begins no operand: reading it a second time ran what the first round had only produced, and `k='$(cmd)'; a[$k]=V` executed cmd (#3047).

Measured 2026-09-15 from a script file, and unanimous in bash 5.3.20, bash 3.2.57, zsh 5.9.2 and ksh93u+ 2012-08-01: `$(cmd)`, a backquoted run, `$((1))` and a bare `$i` left in an expanded subscript are each refused for wanting an operand, and none of the four runs anything. A bare name is not one of them — `k='i'` and `k='1+1'` both name an element in all four — because resolving a name is the evaluator's job rather than a second round of expansion.

func (*Parser) ParseArithFor

func (p *Parser) ParseArithFor(src string, at Pos) ArithExpr

ParseArithFor is ParseParamExpFor for `$(( ))`.

func (*Parser) ParseArithSubscript added in v0.0.11

func (p *Parser) ParseArithSubscript(src string, at Pos) ArithExpr

ParseArithSubscript is ParseArithExpanded for the text between an *index's* brackets, which is a fresh arithmetic read and so takes the dialect's double-quote removal where ParseArithExpanded does not.

The two part because the quotes are asked about twice and the answers differ. Measured 2026-09-20, `env -i PATH=/usr/bin:/bin LC_ALL=C <shell> f.sh` over a script file with standard input on the null device, against `a=(9 8 7)`:

e='a["1"]'; printf '%s' "$(( $e ))"    bash 5.3.20 8    ksh93u+ 8
e="a['1']"; printf '%s' "$(( $e ))"    bash refuses `'1'` as an operand

The brackets came out of a value in both rows, so neither is text bash's removal is otherwise for — and yet the double quotation comes off and the apostrophes do not, which is the removal running over a subscript's text when it is read as an expression rather than word quote removal running over the subscript. A key is the other reading and takes neither: the same brackets on an associative name keep `"q'r'z"` whole, which is what Parser.ParseArithExpanded's own rows are about (#3941).

func (*Parser) ParseParamExpFor

func (p *Parser) ParseParamExpFor(src string, at Pos) *ParamExpr

ParseParamExpFor parses the inside of a `${ }` that was captured outside the normal word path — a here-document body, which is read as raw text and expanded only when the command runs.

The body is double-quoted context, which is what decides how the operand of a `${x:-word}` in it reads: `cat <<EOF` with `${u:-'$v'}` in the body prints `'VAL'` in every shell in the panel — the quotes two characters of the output and the `$v` between them still substituted, exactly as inside a pair of double quotes.

func (*Parser) ParseReference

func (p *Parser) ParseReference(src string, at Pos) *ParamExpr

ParseReference parses text that names a parameter and perhaps one of its elements — `x`, `x[@]`, `s[2]`, `a[(r)q]` — into the node every reading of a subscript already answers.

It is the shape a *value* takes where a construct reads one as a reference at run time rather than at the parse: a `(P)` group's resolved text, and `unset`'s operand. Those arrive as strings with the subscript unlexed, so the group has nowhere to hang its operand and a range has no two ends — which is why the answer was a name plus one arithmetic index and nothing else.

Unquoted, unlike ParseParamExpFor: a here-document body is inside quotes and a resolved value is not.

func (*Parser) Remarks

func (p *Parser) Remarks() []Remark

Remarks is what the parser has to say about input it accepted anyway.

Read it whether or not Err returned something: a remark can accompany a fatal error, and that is measured rather than hypothetical.

func (*Parser) SetDialect

func (p *Parser) SetDialect(d Dialect)

SetDialect replaces the dialect for input that has not been read yet.

A shell can change its own grammar while it runs: a builtin executed on one line decides whether a quantified group is a group on the next. The parser cannot know that — the builtin runs in an interpreter this package has never heard of — so the front end, which is the only place the two meet, reads the interpreter's answer between lines and hands it in here. The same joint as Aliases, for the same reason.

It applies to what has not been tokenized. The parser holds one token of lookahead, so the very first token of the next line may have been read under the old dialect; every construct a runtime toggle governs sits deeper in a line than its first token, which is what makes the boundary safe.

type Pipeline

type Pipeline struct {
	// Negated records a leading `!`, which applies to the whole pipeline
	// rather than to its first command: `! true | false` exits 0.
	//
	// False for an *even* run of them where [Dialect.RepeatedNegationToggles]
	// says each inverts the one before it, so it does not stand in for "a `!`
	// was written": `! !` is a bare negation that negates nothing.
	Negated bool
	Bang    Pos
	// Stop is the end of the last `!`, for a bare negation that has no
	// command to end at — the same fact [TimeClause.Stop] carries for a bare
	// `time`. Set whenever a `!` is read, and unset where none was.
	Stop Pos
	Cmds []Command
}

Pipeline is one or more commands joined by `|`.

Cmds is empty for a bare negation — a `!` with no pipeline after it, which Dialect.BareNegationReach says where a dialect takes. Nothing runs and the negation inverts a success, so the status is 1. That node is the reason Bang and Stop are both recorded: it is the one shape whose extent is the keyword run rather than a command.

func (*Pipeline) End

func (p *Pipeline) End() Pos

func (*Pipeline) Pos

func (p *Pipeline) Pos() Pos

type Pos

type Pos struct {
	// Offset is the byte offset from the start of the input, zero-based.
	Offset int32
	// Line is the one-based line number.
	Line int32
	// Col is the one-based column, counted in bytes rather than in runes or
	// grapheme clusters. Callers rendering a caret under a token want display
	// width, which depends on the terminal and on Unicode data this package
	// deliberately does not carry; Offset is the honest anchor for that.
	Col int32
}

Pos is a location in the input.

Every token carries one. That is not only for diagnostics: this package is meant to run on the keystroke path — a shell re-parses the current line on every keypress to highlight it — so a caller needs to map a token back to the characters the user is looking at, and it needs to do so on input that is incomplete or malformed.

The three fields are int32 rather than int, and that is a size decision with a measurement behind it. A Pos is not a thing the tree holds one of: a Word holds two and every Span inside it holds another, so a Pos is the single most repeated shape in a parsed program. At three ints it was 24 bytes and a Word was 72; at int32 it is 12 and a Word is 48, which is also a Go size class rather than three quarters of one. Over the scripts a real interactive startup reads, that plus the Span field order took the retained tree down by a fifth — and the retained tree is what the heap is sized by, which is what the collector's page mapping and zeroing are proportional to (#2073).

The bound it buys that with is 2GiB of input, per file. A shell script that large is not a thing, and the parser already holds the whole of its input as one string, so the limit that would be reached first is not this one.

func (Pos) After

func (p Pos) After(q Pos) bool

After reports whether p is later in the input than q.

func (Pos) IsValid

func (p Pos) IsValid() bool

IsValid reports whether p refers to a place in some input. The zero Pos does not: lines and columns are one-based, so a zero Line means unset rather than "the first line".

func (Pos) String

func (p Pos) String() string

type ProgramRoutes

type ProgramRoutes uint8

ProgramRoutes is a set of the ways a non-interactive program reaches a shell, for the grammar questions whose answer depends on which of them it was.

The routes are the front end's — a command string, a file, standard input — and this package names them anyway, because the questions have to be askable where they are answered. Two ask it:

[Dialect.ExpandAliases]      read by the front end, which passes the
                             table of aliases in or leaves it nil.
[Dialect.CloseQuotesAtEOF]   read by the lexer, which is told the route
                             through [Dialect.ProgramRoute].

`eval` is a command string. It is not an invocation route at all, but it is a program handed over as a *string* rather than read from a file or a descriptor, and that is the distinction ksh93 draws — measured 2026-09-07, `eval "echo 'abc"` inside a script file prints abc there where the same text as the script itself is refused.

const (
	// RouteFromCommandString is a program given as an argument: `-c`. Also
	// a string handed to `eval`, which is the same kind of program by the
	// one measurement that separates the kinds.
	RouteFromCommandString ProgramRoutes = 1 << iota
	// RouteFromScriptFile is a program read from a path named as an operand.
	// Also a file read by `.`, and a startup file.
	RouteFromScriptFile
	// RouteOnStandardInput is a program read from the descriptor, whether
	// by `-s` or by there being no operand and no terminal.
	RouteOnStandardInput
	// RouteOnEveryRoute is what a dialect that does not distinguish them
	// answers.
	RouteOnEveryRoute = RouteFromCommandString | RouteFromScriptFile | RouteOnStandardInput
	// RouteOnNoRoute is the empty set, spelled so a dialect can say it
	// deliberately rather than by leaving a field out.
	RouteOnNoRoute ProgramRoutes = 0
)

func (ProgramRoutes) Has

func (a ProgramRoutes) Has(route ProgramRoutes) bool

Has reports whether route is in the set. A caller asks with exactly one route, which is what it knows.

The empty route is in no set, which is what makes the strict answer the default: a parse that was never told how its program arrived — a function body being re-read, a prelude, a tree dump — gets the grammar every shell agrees on rather than one shell's leniency.

type Quoting

type Quoting uint8

Quoting says how a span of a word was written, which decides what happens to it later: only unquoted spans of an expansion result are subject to field splitting and pathname expansion.

const (
	// Unquoted text: expansions apply, and the result is split and globbed.
	Unquoted Quoting = iota
	// Single quotes protect everything, including backslash. No escape
	// exists inside them.
	SingleQuoted
	// Double quotes protect everything except $, `, \ and ". A backslash is
	// an escape only before those four and newline; before anything else it
	// is a literal backslash, so "a\nb" is backslash-then-n.
	DoubleQuoted
	// Dollar-single quotes, $'...', where backslash escapes are interpreted.
	// Absent from dash.
	DollarSingleQuoted
	// BackslashQuoted is a single character protected by an unquoted
	// backslash. It is its own value rather than folded into the literal
	// text around it, because the protection has to survive: `\*` and `'*'`
	// behave identically, and a field that forgot the backslash would glob.
	BackslashQuoted
)

type Redirect

type Redirect struct {
	// N is the file descriptor written immediately before the operator, or
	// nil. The adjacency is the rule: `echo 1>b` has one, `echo 1 >b` does
	// not and the 1 is an argument.
	N     *Word
	Op    Kind
	OpPos Pos
	// Text is the target as it was written, before any expansion.
	//
	// One dialect names it in a diagnostic — `$e: ambiguous redirect`, where
	// `$e` is what was typed and not what it came to — and by the time that
	// is known the word has been expanded and there is nothing left that
	// remembers how it was spelled. Same reason a background statement keeps
	// its text.
	Text string

	// Word is the target: a filename, a descriptor for `>&`, or a here-string
	// body. For a here-document it is the delimiter.
	Word *Word
	// Heredoc is the body, which is read from the lines after the command
	// rather than from the token stream. Nil for every redirection that is
	// not a here-document — including `<<<`, which shares a prefix with `<<`
	// and nothing else: a here-string's input is its Word, on the same line.
	Heredoc *Word
	// HeredocAtEOF says the body ran to the end of the input because the
	// delimiter never arrived. It is a fact about the text rather than a
	// dialect's choice — every shell in the panel takes the body as
	// everything to the end and runs the command — and it is recorded
	// because one thing about that body is *not* unanimous: whether it gains
	// the newline its last line never had. See
	// interp.Semantics.UnterminatedHeredocGainsATrailingNewline, which is the
	// only reader; a terminated here-document's body always ends in one, so
	// this is the only shape where the question exists (#1020).
	HeredocAtEOF bool

	// PipeBoth records a redirection nobody wrote: the `2>&1` that a `|&`
	// after this command stands for. The operator is the source text and
	// this is what it means, which is why the meaning is here rather than
	// on the pipeline — the redirection has to be the command's *last*, and
	// the command's own list is the only place that can say so.
	//
	// Kept marked so the printer writes `|&` back rather than the
	// redirection, and so anything reading the tree can tell what the script
	// said from what the grammar added.
	PipeBoth bool
}

Redirect is one redirection.

func Redirections added in v0.0.20

func Redirections(c Command) []*Redirect

Redirections are the redirections written on a command, or nil for a kind of command that cannot carry any.

Exported because a *definition's* redirections are part of what the function is, and a caller outside this package has to be able to ask: one shell's `$functions` value keeps the braces when there are any, because they follow the closing one and there would otherwise be nowhere for them to go. See interp.Runner.FunctionBodyText.

A definition delegates to its body, which is where the parser puts them — the same hand-off mergesStderr makes, for the same reason.

func (*Redirect) End

func (r *Redirect) End() Pos

func (*Redirect) Pos

func (r *Redirect) Pos() Pos

type RedirectDescriptorForm added in v0.0.20

type RedirectDescriptorForm uint8

RedirectDescriptorForm is what a written-back redirection does with a descriptor on its left that is the operator's own default. See Layout.RedirectDescriptor for the measurement.

const (
	// RedirectDescriptorAsWritten says back exactly what the script wrote,
	// which is ksh93's answer and the conservative one for an embedder that
	// has not chosen: a printer that neither adds nor removes cannot be
	// wrong about a descriptor nobody asked it to think about.
	RedirectDescriptorAsWritten RedirectDescriptorForm = iota
	// RedirectDescriptorWrittenOnDuplications is bash's pair of answers:
	// the default descriptor is written out in front of `>&` and `<&`, and
	// taken off a redirection that names a file. `echo x >&2` comes back
	// `echo x 1>&2` and `echo x 1>out` comes back `echo x > out`.
	RedirectDescriptorWrittenOnDuplications
	// RedirectDescriptorOmitted is zsh's single answer: a descriptor equal
	// to the operator's default is left off wherever it appears, whether
	// the script wrote it or not. `1>&2` comes back `>&2`.
	RedirectDescriptorOmitted
)

type RedirectionBeforeACompoundPolicy added in v0.0.9

type RedirectionBeforeACompoundPolicy uint8

reservesWord is Dialect.Reserves over the words the *lexer* classes as reserved, which is the narrower question an alias expansion asks.

The two part company on the three spellings above: they are constructs a dialect has, so a report about what is runnable must name them, and they are not words the lexer reserves, so the rule about which words an alias may shadow has never been about them. See [Parser.reservedInDialect]. RedirectionBeforeACompoundPolicy says whether a redirection may stand in **front** of a compound command, and before which of them.

POSIX puts a compound command's redirections after it and every shell takes them there; two of the panel take them in front as well, and they take them before different things. Measured 2026-09-18, script files under `env -i PATH=/usr/bin:/bin LC_ALL=C`:

written                                zsh 5.9.2  ksh93u+  bash, dash, ash
>/dev/null ( echo hi )                 runs       runs     refuses
>/dev/null (( 1 ))                     runs       runs     refuses
>/dev/null { echo hi; }                runs       refuses  refuses
>/dev/null while false; do :; done     runs       refuses  refuses
>/dev/null if true; then echo hi; fi   runs       refuses  refuses
>/dev/null for i in a; do echo hi; done  runs     refuses  refuses
>/dev/null case a in a) echo hi;; esac   runs     refuses  refuses
>/dev/null until true; do :; done      runs       refuses  refuses
>/dev/null select i in a; do break; done runs     refuses  refuses
>/dev/null repeat 2 do echo hi; done   runs       —        —
>/dev/null foreach i (a); echo hi; end runs       —        —
2>/dev/null [[ -n a ]]                 runs       refuses  refuses
>/dev/null ! false                     refuses    —        —
>/dev/null coproc cat                  refuses    —        —

So one column takes it before a **parenthesized** command alone and the other before every compound it has. The two rows at the bottom are that column's own boundary: `!` and `coproc` are not compound commands there and the redirection does not reach them.

The **assignment prefix** takes it away in both: `v=x >/dev/null ( echo hi )` and `>/dev/null v=x { echo hi; }` are refused in zsh and in ksh93 alike, which is why this is asked only where nothing but redirections has been read (#3560).

const (
	// RedirectionBeforeACompoundIsRefused is POSIX's own shape, and the
	// answer bash 5.3, bash 3.2, dash and BusyBox 1.37.0 ash give: a
	// compound command's redirections follow it.
	RedirectionBeforeACompoundIsRefused RedirectionBeforeACompoundPolicy = iota
	// RedirectionMayPrecedeAParenthesizedCommand takes one in front of `(
	// … )` and `(( … ))` and nowhere else: ksh93.
	RedirectionMayPrecedeAParenthesizedCommand
	// RedirectionMayPrecedeAnyCompoundCommand takes one in front of every
	// compound command the dialect has: zsh.
	RedirectionMayPrecedeAnyCompoundCommand
)

func (RedirectionBeforeACompoundPolicy) String added in v0.0.9

type Remark

type Remark struct {
	// Kind is what happened. There is one today, and the type exists so a
	// second does not change the shape of everything that reads these.
	Kind RemarkKind
	// Pos is where the input ran out, which is where the remark is located.
	Pos Pos
	// At is where the construct it is about began.
	At Pos
	// Token is what the remark is about: the here-document's delimiter that
	// was waited for, or the first of two operators written with no blank
	// between them.
	Token string
	// Count is how many constructs this remark is about, and it is filled in
	// for [RemarkHeredocCarriedOut] alone: the shell that says anything
	// about that one writes the number and makes its noun agree with it, and
	// one remark per document would have said it once per document.
	Count int
	// Next is the second of two operators written with no blank between
	// them, and is empty for every other kind. Two spellings are needed
	// because the one shell that says this names both of them in the
	// sentence, and neither can be derived from the other — the pair is
	// exactly what the remark is about.
	Next string
}

A Remark is something the parser has to say about input it nonetheless accepted — a third channel beside the error and the tree.

It exists because a parse can be *wrong enough to mention and right enough to run*: a here-document whose delimiter never arrives is taken as everything to the end of the input and the command runs, and one shell in the panel says so on the way past while three say nothing.

Two things about the shape were measured before it was built, and both rule out simpler designs:

  • A remark can accompany a *fatal* error. `f() { cat <<X` with neither the delimiter nor the `}` produces the warning and then the syntax error, so remarks are not "what the parser says when it succeeds" and a front end that reads them only in the else branch would drop this one.
  • A remark carries *two* positions. The warning is located at the line the input ran out on and names, inside its wording, the line the here-document began on.

Whether anything is said at all is the front end's to decide from its dialect: three of the four say nothing, so silence is an answer rather than a missing feature.

type RemarkKind

type RemarkKind int

RemarkKind is which remark this is.

const (
	// RemarkNone is the zero value and is never produced.
	RemarkNone RemarkKind = iota
	// RemarkHeredocAtEOF is a here-document whose delimiter never arrived.
	// The body is everything to the end of the input, and the command runs.
	RemarkHeredocAtEOF
	// RemarkBackquoteSubstitution is the older command substitution, read
	// and run exactly as it is written. One shell remarks on every one of
	// them; the others say nothing.
	//
	// It carries no Token and its two positions are the same one: the
	// backquote that opened the substitution, which is both where the
	// remark is located and what it is about.
	RemarkBackquoteSubstitution
	// RemarkOperatorsNotSeparated is two operators written with no blank
	// between them — `a&;b`, `(:);(:)`, `if |; then :; fi`. The text lexes
	// and means what it says; one shell remarks on the layout on the way
	// past and the others read it without a word.
	//
	// Token is the first operator and Next is the second. Its two positions
	// are the same one: the first operator, which is both where the remark
	// is located and what it is about.
	RemarkOperatorsNotSeparated
	// RemarkFunctionNameIsAnAlias is a function being defined under a name
	// the alias table holds, in the one column that refuses it. Token is
	// the name; its two positions are the same one, the word itself.
	//
	// A remark and not only an error because the shell writes *two* lines —
	// its own sentence about the alias, and then the ordinary parse failure
	// at the parentheses — which is the shape this channel exists for. See
	// Dialect.AliasAtAFunctionName.
	RemarkFunctionNameIsAnAlias
	// RemarkHeredocCarriedOut is a here-document a substitution's own text
	// opened and could not feed, whose body the lexer holding the enclosing
	// command went on to read from the lines after it. One shell says so and
	// names how many were carried; one does it silently.
	//
	// Count is how many, and its two positions are the same one: where the
	// substitution opened, which is the enclosing command's line. See
	// [Dialect.HeredocBodyFromAfterTheCommand].
	RemarkHeredocCarriedOut
)

func (RemarkKind) String

func (k RemarkKind) String() string

type RepeatClause

type RepeatClause struct {
	Count *Word
	Body  []*Stmt
	// Header is `repeat N` as written. See ForClause.Header.
	Header string
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

RepeatClause is `repeat N; do … done`, and the shorter spellings the same dialect gives every loop.

A count rather than a condition, which is what makes it a construct of its own rather than a `for` in disguise: the word is evaluated as an arithmetic expression once, before the first iteration, and a count that is not a number or is not positive runs the body no times at all.

func (*RepeatClause) End

func (c *RepeatClause) End() Pos

func (*RepeatClause) Pos

func (c *RepeatClause) Pos() Pos

type SelectClause

type SelectClause struct {
	Name string
	// RefusedName is ForClause.RefusedName for the menu loop, and the same
	// two shells check it at the same stage: measured, `select $n in a b`
	// and `select 1x in a b` parse in bash and ksh93 and are refused while
	// parsing by dash and zsh, exactly as the `for` spellings are.
	RefusedName string
	// Items and HasItems carry the same distinction as ForClause's: with `in`
	// omitted the menu is built from the positional parameters.
	Items    []*Word
	HasItems bool
	Body     []*Stmt
	// Header is `select x in a b` as written. See ForClause.Header.
	Header string
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

SelectClause is `select name [in words] do … done`.

The same shape as ForClause and not the same loop: the words are a menu rather than a sequence, the body runs once per *reply* rather than once per word, and the loop ends when the input does rather than when the list does. Sharing a node would have made every use of one ask which it was.

func (*SelectClause) End

func (c *SelectClause) End() Pos

func (*SelectClause) Pos

func (c *SelectClause) Pos() Pos

type SeparatorSkip

type SeparatorSkip uint8

grammar wants a command. See Dialect.SeparatorWhereACommandBelongs.

A count and a place rather than a bool, because the two shells that allow this draw it differently and a single yes/no could not be given a value for either of them without accepting lines the other refuses.

const (
	// NoSeparatorWhereACommandBelongs is the core answer: a `;` where a
	// command belongs is a syntax error. dash, bash 5.3, bash 3.2 and
	// bash-as-`sh`.
	NoSeparatorWhereACommandBelongs SeparatorSkip = iota

	// OneSeparatorExceptAfterABarOrBeforeACondition steps over a single `;`,
	// and not at all after a `|` or where a *condition* begins. ksh93, and
	// every part is measured rather than assumed: `a || ; ; b` is
	// “ `;' unexpected “ there where `a || ; b` runs, and `a | ; b` is
	// refused where `a || ; b` and `a |& ; b` are taken — the same asymmetry
	// #1115 found for that shell's `|&`, and the probe that says the bar is a
	// separate question from the and-or.
	//
	// The condition is the third exception and was missing, so `if; then`
	// parsed here and was blamed on the `then` where ksh93 blames the `;`.
	// Measured 2026-09-12 over a script file:
	//
	//	if; then :; fi                 `;' unexpected
	//	while; do :; done              `;' unexpected
	//	until; do :; done              `;' unexpected
	//	if :; then :; elif; then :; fi `;' unexpected
	//	if :; ; then :; fi             `;' unexpected — the whole list, not
	//	                               only the position after the keyword
	//	if : ; :; then :; fi           runs — a `;` *terminating* a statement
	//	                               of the condition is not this
	//	if false || ; then :; fi       runs — where an and-or's right-hand
	//	                               side belongs is still the and-or's
	//	if :; then : ; ; fi            runs — a body is not a condition
	//
	// So it is the position where a *statement of a condition list* begins,
	// and neither "anywhere in the header" nor "the token after the keyword".
	// The wider value takes all nine: zsh parses every line above (#2023).
	OneSeparatorExceptAfterABarOrBeforeACondition

	// AnySeparatorWhereACommandBelongs steps over as many as are written,
	// anywhere, the bar included. zsh: `echo one | ; ; cat -n` numbers the
	// line, and so does `echo one | ; ⏎ ; cat -n`.
	AnySeparatorWhereACommandBelongs

	// SeparatorOnlyWhereAnAndOrWantsOne steps over a single `;` where an
	// and-or's right-hand side belongs and nowhere else at all.
	//
	// No dialect holds this as its own answer: it is what
	// OneSeparatorExceptAfterABarOrBeforeACondition becomes inside the body
	// of a `$( … )` or a `${ …;}` in the shell that holds that value — see
	// [Dialect.SubstitutionBodyRefusesASteppedOverSeparator], which is where
	// the measurement is.
	SeparatorOnlyWhereAnAndOrWantsOne
)

type ShellSplit

type ShellSplit struct {
	// Comments is what a `#` where a word could begin means. See CommentMode.
	//
	// The zero value is the shell's ordinary rule, CommentsSkipped, because
	// this shares the lexer's type and the lexer's default has to be the
	// ordinary one. A caller wanting no comment rule at all — which is what
	// the flag this serves does with neither of its comment letters written
	// — has to say CommentsOrdinaryText.
	//
	// It is this and not [Dialect.Comments] that decides, whichever the
	// dialect carries: a value being re-read is not the line somebody typed,
	// and the flag's letters say what the caller wants of it outright.
	Comments CommentMode

	// NewlineIsBlank makes an unquoted newline ordinary whitespace. Without
	// it a newline is a word of its own, spelled `;` — which is what the
	// lexer already calls it, a command terminator, written the way the
	// terminator is written rather than the way this text happened to spell
	// it.
	NewlineIsBlank bool
}

ShellSplit says how ShellWords reads the text it is given.

type ShortForm

type ShortForm int

ShortForm is what a formatter does with a brace-spelled body.

const (
	// PreserveShortForm writes the body back with the spelling the author
	// used, read from the source rather than from the tree.
	PreserveShortForm ShortForm = iota
	// ExpandShortForm writes the keyword spelling however the body was
	// written. The portable form, and the only reachable answer in a
	// dialect that cannot parse the other one.
	ExpandShortForm
)

func (ShortForm) String

func (s ShortForm) String() string

type SimpleCmd

type SimpleCmd struct {
	Assigns []*Assign
	Args    []*Word
	Redirs  []*Redirect
	// Precommands are the reserved words taken away in front of this
	// command — see [Dialect.ReservedPrecommands]. They are kept rather than
	// dropped because printing a tree has to print the program that was
	// read: dropping the word would leave `nocorrect mv a b` printing as
	// `mv a b`, which is a different program in a shell that corrects
	// spelling.
	//
	// Nothing that *runs* the command reads them. That is the point of the
	// class: a word the grammar removes is a word the command cannot see.
	Precommands []*Word
	// DeclaresByReservedWord says the command word is one of
	// [Dialect.DeclarationReservedWords] and an operand is an assignment,
	// which a listing marks with a blank at the end. A fact read off the
	// words, so a tree printed and read again says it again.
	DeclaresByReservedWord bool
	Start                  Pos
	Stop                   Pos
}

SimpleCmd is assignments, arguments and redirections.

The three are interleaved in the source and separated here, because a redirection may appear before the command name or between its arguments — `>b echo hi` and `echo one >b two` both work — so they cannot be modeled as a suffix.

func (*SimpleCmd) End

func (c *SimpleCmd) End() Pos

func (*SimpleCmd) Pos

func (c *SimpleCmd) Pos() Pos

type Span

type Span struct {
	// Value is the span's text with its own delimiters removed — quotes for a
	// literal, `$(` and `)` for a substitution — and with no expansion
	// performed and no escapes resolved. Quote removal proper happens at the
	// end of expansion, not here.
	Value string

	// Arith is the parsed form of an ArithSubst span, for the same reason.
	Arith ArithExpr

	// Param is the parsed form of a ParamExp span, filled by the parser. The
	// lexer leaves it nil: finding the closing brace and understanding the
	// operators are different jobs, and only the second needs a dialect.
	Param *ParamExpr

	// Pos is where the span starts, including its opening delimiter.
	Pos Pos

	// Kind says whether this is literal text or a substitution.
	Kind SpanKind

	// Quoting is the quoting this span sits in. For a substitution it decides
	// only whether the result is split afterwards, not what the span is.
	Quoting Quoting

	// Translated says a double-quoted run was written `$"…"`, the mark one
	// shell has for a string a message catalog may translate.
	//
	// A flag rather than a [Quoting] value of its own, for the reason
	// CurrentShell below is one: with no catalog loaded the mark changes
	// nothing about the string — same escapes, same expansions, same
	// splitting — so everything that *expands* a word treats the two
	// spellings alike, and only the reading, the writing back and the one
	// invocation option that lists these strings can tell them apart. A
	// quoting value would have put the distinction in front of forty readers
	// that do not have a question for it.
	//
	// It is set on every span of the run, because a run is what carries one
	// pair of quotes: `$"a$xb"` is three spans inside one `$"…"`.
	//
	// See [TranslatedStrings], which is the reader, and note what recording
	// the mark does *not* promise — nothing here translates anything, and no
	// message catalog is consulted.
	Translated bool

	// CurrentShell says a command substitution was written `${ cmd;}`, which
	// runs in the shell that read it rather than in a subshell — so what it
	// assigns survives, which is the only reason the spelling exists.
	//
	// A flag rather than a kind of its own for the same reason Backquoted is
	// one: everything that *runs* a substitution treats the three alike, and
	// only the reading, the writing back and the choice of shell differ.
	CurrentShell bool

	// ReplyValue says a current-shell command substitution was written
	// `${| cmd;}`, whose value is whatever the body left in `$REPLY` rather
	// than what it printed — so its body's output goes where the shell's
	// output was going, and nothing is captured.
	//
	// A second flag on the same kind rather than a kind of its own, for the
	// reason CurrentShell is one: everything that *runs* a substitution
	// treats them alike, and only where the value comes from differs. It is
	// never set without CurrentShell, because the form is that form with a
	// different source for its value. See Dialect.ReplySubstitution.
	ReplyValue bool

	// Backquoted says a command substitution was written “ `like this` “
	// rather than as `$( … )`.
	//
	// The two are one node to everything that runs them, and they are not
	// the same syntax: a backquoted one ends at its closing backquote and
	// the other ends where its contents end. So a substitution holding a
	// here-document whose delimiter never matches — which happens, in a
	// script installed on this machine — is terminated by the backquote and
	// would not be terminated by a parenthesis. Anything writing one back
	// has to write the spelling that was read.
	Backquoted bool

	// Comments is the rule a `#` in this span's body was read under, for the
	// spans that hold a program and are read again by whatever runs them.
	// The zero value is [CommentsSkipped], the ordinary rule.
	//
	// It travels for the reason Backquoted does: a substitution's body is
	// kept as the text it was written as and parsed a second time when it
	// runs, and a second parse under a different rule is a second shell. The
	// front end is the one that can set the rule — see [Dialect.Comments] —
	// so without this the *typed* line reads a `#` as a character and the
	// body inside it reads the same `#` as a comment.
	//
	// Measured on zsh 5.9.2, 2026-09-12, `-f -i` on a pipe with the option
	// off: `echo M-$(echo a #b)` answers `M-a #b`, and so does the same line
	// with the substitution nested twice or written with backquotes. The
	// rule reaches the whole of the text that was typed, at every depth.
	Comments CommentMode

	// Bracketed says an arithmetic substitution was written `$[ … ]` rather
	// than `$(( … ))`.
	//
	// The same reasoning Backquoted carries, one construct along: the two are
	// one node to everything that evaluates them — the expression grammar is
	// the same and so is every diagnostic it produces — and they are not the
	// same syntax, because one ends at a `]` and the other at a `))`. So
	// anything writing one back has to write the spelling that was read, and
	// a printer that normalized `$[1+1]` into `$((1+1))` would be editing a
	// script rather than printing it.
	Bracketed bool

	// Braced says an arithmetic expansion was written `${(( … ))}` rather
	// than `$(( … ))`, which is the same reasoning again and the third
	// spelling of one construct. One shell in the panel has it; the rest
	// call it a bad substitution. See [Dialect.BracedArithmeticExpansion].
	Braced bool

	// PatternGroup says this literal span is the text of a parenthesised
	// group that belongs to the word — `(a|b)` in `echo (a|b)`, `@(a|b)`
	// or a `case` arm's pattern — so its `(`, `|` and `)` are the
	// pattern's rather than the shell's.
	//
	// The note has to be in the tree because a character cannot be read
	// off the value: an unquoted `(` in a word is a group's where the
	// group grammar accepted one and a literal parenthesis where a
	// backslash or a quote put it there, and both arrive as the same byte.
	// Nothing that *runs* a pattern needs this — the matcher reads the
	// group out of the text, as it reads a bracket expression — and
	// anything writing the word back does: printing `\(a\|b\)` for
	// `(a|b)` leaves the tree identical and turns a group into three
	// literal characters, which is a different program at exit 0 (#1221).
	//
	// Set on the unquoted spans of a group and on those alone. A quoted
	// span inside one carries its own quoting and is written back with it,
	// which is what says the protection was the script's rather than the
	// printer's.
	//
	// Setting it on the quoted spans as well is an equivalent mutant, and
	// is recorded here so the next reader does not go looking for the row
	// that would kill it: the printer is the only thing that reads this
	// field, and it reads it in the arm it takes for unquoted text alone —
	// a quoted span is written from its quoting before the question is
	// asked.
	PatternGroup bool

	// PidBrace says this literal span is one of the two braces of a `{ … }`
	// written immediately after `$$`, which the dialect that reads the
	// construct makes a character rather than syntax. See
	// [Dialect.PidBraceGroupIsText].
	//
	// The note has to be in the tree for the reason PatternGroup's does:
	// both braces arrive as the same byte a brace expansion opens with, and
	// which one this is was decided by what stood in front of it. Two
	// readers ask. Brace expansion refuses the pair a *list* — `$${a,b}` is
	// one word — and still reads a range out of it, because that is what the
	// shell does: `$${1..3}` is three. The flag is on the outer pair and on
	// nothing else, so the `{b,c}` *inside* `$${a{b,c}d}` is untouched and
	// expands as it always did.
	// And the printer writes the run back as it came: the blanks and
	// operators inside it were characters, so escaping them would leave the
	// tree identical and make the word a different program (#1221).
	PidBrace bool

	// Bare says a parameter expansion was written `$name` rather than
	// `${name}`.
	//
	// The two are the same node to everything that expands one, which is why
	// the flag is here and not a kind of its own — and they are not the same
	// *text*, because the short form has no closing brace to stop it. So a
	// `[` after the name belongs to the expansion only where a grammar flag
	// puts it there, and whether it is then read as a subscript is a
	// question the run answers: a shell can have the construct and still be
	// told, at run time, that the brackets after an unbraced name are
	// ordinary characters. Nothing can ask that once the two spellings are
	// indistinguishable, which is what this records.
	//
	// Only the parser reads it, and only to hand ParamExpr.BareIndexText the
	// subscript's text.
	Bare bool
}

Span is a run of a word written with uniform quoting.

A word is a sequence of these rather than a string, because quoting is recorded per span and not per word: in a"b c"d the three spans are unquoted, double-quoted and unquoted, and the whole is a single word. Later stages act only on the unquoted spans, so a lexer that flattened this would make field splitting and globbing unimplementable.

The field order is deliberate and is not the order to read them in. Wide fields first, then the one-byte ones together at the end: written in reading order — a kind, a value, a quoting — Go padded the struct out to 88 bytes, of which 15 were holes. There are about 185,000 of these in the tree a real interactive startup leaves behind, so the holes were 3MB of resident memory holding nothing (#2073). Reordering for legibility costs that again.

func ArithTextSpans added in v0.0.13

func ArithTextSpans(text string, d Dialect) ([]Span, error)

ArithTextSpans is HeredocSpans for the text of an arithmetic expression, which differs from a here-document's body in one place: **a backslash inside brackets keeps both of its bytes.**

A here-document body is finished text, so the escape has done its work by the time the body is written and the backslash goes. An arithmetic expression's text is read *twice* — once here, and again when a subscript found in it is read as a key or as an expression — and a backslash that quoted a `$` has to survive the first read or the second one expands what the first was told not to.

Measured 2026-09-23 on bash 5.3.20, `declare -A a; k='a b'; a[$k]=5; a['$k']=11`, each row from a script file:

$(( a[\$k] ))        11   the key is `$k`, the escaped `$` not expanded
$(( a['\$k'] ))       0   the key is `\$k`, the apostrophes keeping it
$(( a[$k] ))          5   the control: an unescaped `$` still expands
$(( \$n + 1 ))      error  outside brackets the backslash still goes

Reading the first row through a body's rules gave 5: the backslash came off here, the second read saw a bare `$k`, and the element the script had asked for by its literal name was answered by the one the expansion found. The last row is why the brackets are part of the rule rather than the whole text — bash removes the backslash where no subscript is open, and both shells refuse `$n` there with the same token.

func HeredocSpans

func HeredocSpans(body string, d Dialect) ([]Span, error)

HeredocSpans splits an unquoted here-document body into spans.

A body is not a word and not a double-quoted string, though it is much closer to the second: `$name`, `${ }`, `$( )`, `$(( ))` and backticks expand, a backslash escapes only those and itself and a newline, and everything else is literal — **quotes included**.

Running the word lexer over it instead is what this replaces, and the difference is not subtle: quoting rules applied, so `don't` came out as `dont` and `\n` as `n`. It printed no error and exited 0, which is the worst way to be wrong.

A quoted delimiter is not this. That body is literal throughout and never reaches here.

The error is the text running out *inside* one of those constructs — an unterminated `${`, `$(` or backquote — and it is returned rather than swallowed because the spans alone cannot say it happened: a `${` with no closing brace hands back a parameter-expansion span whose value is whatever followed, which reads as an ordinary expansion nobody wrote. A caller that drops it answers `x` for `x${` where every shell in the panel refuses the text (#1653).

Spans are still returned alongside it, as much of the body as was read, so a caller that has a use for a partial reading is not forced to re-lex.

func RereadWord added in v0.0.13

func RereadWord(text string, at Pos, d Dialect) ([]Span, error)

RereadWord reads text as the source of one word, for a run that has to read a word it *produced* rather than one the file held.

It is the second of the two re-readings a run can be asked for, and it is the wider one. RereadWordTail divides a word the file holds a second time, under the other reading of where a `}` ends an expansion; this reads text the file never held at all — what brace expansion produced, in the one shell that hands its output back to word expansion as ordinary shell text. So the `x` that `$var{x,y}` produced continues the parameter's name there, and the backslash a character range counted is an unquoted backslash like any other. See interp.Semantics.BraceOutputRereadAsText, which is the axis, and holds the measurements.

The whole of text is one word by construction — it is what one word of the program came to — so an operator in it is a character of the word rather than a token, and what a word scan stops at is put back as literal text. That is the rule an expansion's operand is read under too, for the same reason: everything in the text belongs to the one word it was cut from.

An error is the **run's** rather than the file's, exactly as RereadWordTail's is: the program read cleanly under the characters it actually holds, and it is what the produced text spells that will not read. `x{Z..a}y` is the worked example — the backtick the range counted opens a command substitution that the `y` behind it never closes, and bash answers it with a run-time `bad substitution` at status 1 with the next line still running.

func RereadWordTail added in v0.0.2

func RereadWordTail(tail string, at Pos, d Dialect, cutUnder BraceQuotePolicy, decode func(string) string) ([]Span, string, error)

RereadWordTail divides the tail of a word again, under d's reading of Dialect.QuoteProtectsTheClosingBrace.

tail is a ParamExpr.RawTail: the source from an expansion's `${` to the end of the word it stands in. The spans it returns replace the word's from that expansion onward. Everything in front of it is untouched, and is untouchable — the two readings scan identically until the first single quote inside the braces, which is the first place they can disagree.

It is read as double-quoted **content** rather than as the rest of a word, and that is measured rather than convenient. `printf "[%s]" "${v-'a}" x "'}"` is one argument in bash under both modes, and `"${v-'a}"; echo MIDDLE; :"'}"` is the one argument `V; echo MIDDLE; :'}` with `MIDDLE` never printing. So the leftover characters are neither re-tokenized nor field-split: the quotes around them are removed, a `'` among them is an ordinary character, and a `;` among them is text. A word's extent and a statement's are facts about the parse in bash too; only the division inside one already-cut word belongs to the run.

Which makes the `"` that closed the word part of this text, with nothing left to close — see [Lexer.inWordTail], which is what says that running out of a word's own tail is the word ending rather than an unmatched quote.

An error is the scan running out of an expansion — `"${v-'$('}"` never closes its `${` under the reading where the quote does not protect — and it belongs to the *run* rather than to the file, because the file read cleanly under the reading it was parsed with. bash answers it the same way, with a run-time `bad substitution` at the word rather than a syntax error at the line (#2604).

cutUnder is the reading the word was cut with — ParamExpr.RawTailRead — and decode is what a `$'…'` written in one of the tail's **word** operands stands for, and nil where the run has no such conversion to offer. It is the second thing the one column that reaches here does to an already-cut word, and it is a callback because the two halves live in two packages: the escape table belongs to the run — interp's expandDollarSingle applies it — and the scan for the closing brace belongs here. bash puts the escape's *value* back into the operand's text and reads the division off that, so a value that happens to be a quote character hides the `}` behind it and a value that holds a `}` ends the expansion early (#4207).

A pattern or a replacement operand is deliberately not converted, and that is measured rather than a simplification. Under `env -i PATH=/usr/bin:/bin LC_ALL=C` from a script file, bash 5.3.20 and bash 3.2 both answer `[x]` to `v="'"; printf '[%s]' "${v/$'\x27'/x}"` and `[']` to `v=Q; printf '[%s]' "${v/Q/$'\x27'}"` — the produced quote does not reach the scan there — while the word operand of the same expansion refuses. The split is the one Lexer.braceOperandIsAPattern makes, read off the parsed operator instead of off the text a scan has consumed; the two cannot be one function, because that one runs before there is an operator to read.

What comes back beside the spans is the text the division was read from, which is the tail itself when nothing was converted. The one column that reaches the failure quotes that text back in its diagnostic — with the escape already converted, which is the evidence the re-reading is happening at all — so the caller needs it rather than the word as written.

Positions inside a converted tail are the honest ones the text has and not the ones the file has: an escape is longer than its value, so everything behind the first conversion sits that much earlier than it was written. The failure this reaches names the word rather than a column, so nothing yet reads one; a reader of the returned spans' positions is reading offsets into the returned text.

type SpanKind

type SpanKind uint8

SpanKind says what a span is, as distinct from how it was quoted. A substitution inside double quotes is still a substitution, and the quoting only decides whether its result is split afterwards.

const (
	// Literal text.
	Literal SpanKind = iota
	// CommandSubst is $(...) or `...`. Value is the inner source, unparsed:
	// what is inside is a program, and parsing it is the parser's job.
	CommandSubst
	// ArithSubst is $((...)). Value is the inner expression, unparsed.
	ArithSubst
	// ParamExp is ${...}. Value is the inner text, unparsed — the operator
	// set inside is a separate specification.
	ParamExp
	// ProcSubstIn is <(...) and ProcSubstOut is >(...). Value is the inner
	// source, unparsed, exactly as for a command substitution: what is inside
	// is a program.
	//
	// Two kinds rather than one with a direction on it, because the direction
	// is not a property of the substitution so much as which end of the pipe
	// the word names. `<(cmd)` gives a path to read cmd's output from and
	// `>(cmd)` a path to write cmd's input to, and nothing that handles one
	// handles the other by changing a flag.
	ProcSubstIn
	ProcSubstOut
	// ProcSubstFile is `=(...)`: the same construct with a *file* where the
	// other two have a pipe. Value is the inner source, unparsed, as for the
	// other two.
	//
	// A third kind rather than a flag on the first, for the reason there are
	// two already: nothing that handles one handles another by changing a
	// field. `<(cmd)` hands over a path that is read *while* cmd writes, and
	// the shell carries on; `=(cmd)` runs cmd to completion, puts its output
	// in a regular file, and hands over that path — so the word is seekable
	// and reopenable, which is the whole reason the construct exists where a
	// pipe will not do. One dialect has it; see
	// Dialect.ProcessSubstitutionToFile.
	ProcSubstFile
)

func (SpanKind) String

func (k SpanKind) String() string

type Stmt

type Stmt struct {
	Expr Expr
	// Background is set when the statement ended with `&` rather than `;` or
	// a newline. It belongs to the statement rather than to the command,
	// because `a && b &` backgrounds the whole and-or.
	Background bool

	// Disown is set when the terminator was `&!` or `&|` rather than `&`,
	// where the dialect has them: the job is started in the background and
	// then let go of, so no listing shows it and nothing waits for it by
	// number.
	//
	// Always with Background, never instead of it — the two spellings are
	// `&` plus a disowning, which is why this is a second bool rather than a
	// third state of the first. A dialect without the operators never sets
	// it.
	Disown bool

	// Coprocess is set when the terminator was `|&` in the dialect that
	// spells a coprocess that way: the statement is started in the
	// background with a pipe on each of its named streams, and the shell
	// keeps the near ends.
	//
	// Always with Background, for the same reason Disown is and with the
	// same shape: `|&` is `&` plus two pipes, so this is a second bool
	// rather than a third state of the first. It sits on the *statement*
	// because the operator terminates the whole and-or — measured,
	// `echo A && cat |&` puts `echo A`'s output where a later `read -p`
	// finds it. A dialect without [Dialect.CoprocPipeOperator] never sets
	// it.
	Coprocess bool

	Semi Pos

	// Term is which token terminated the statement, where Semi is only
	// where it stood.
	//
	// Two facts, and a listing needs both. `a;` and a newline after `a` put
	// the terminator in different places but they are the same *position*
	// question, and Semi answers it for both — so a reader holding the tree
	// alone cannot say which of the two was written, and a printer following
	// the source has nothing to follow but the line numbers. Measured, that
	// is the wrong thing to follow: the engine that re-reads a `$( … )` body
	// and writes it back separates two statements by the terminator, so
	// `$(a;` newline `b)` comes back `$(a; b)` on one line and `$(a` newline
	// `b)` keeps its two. See [Terminator] for the rows.
	//
	// A `&` is not one of the values, because Background already says so and
	// a second field saying it again is a second thing to keep true.
	Term Terminator

	// Text is the source this statement was written as, and is recorded only
	// for a background one.
	//
	// Only there because that is the only place a shell has to show a
	// command back to someone long after reading it: a `jobs` listing names
	// what is running, and by then the words have been expanded, the
	// process has been started, and nothing else remembers how it was
	// spelled. Every other node can be re-read from the input it came from,
	// so paying for the text everywhere would be paying for one case.
	Text string
}

Stmt is one entry in a list: an and-or expression and how it was terminated.

func (*Stmt) End

func (s *Stmt) End() Pos

func (*Stmt) Pos

func (s *Stmt) Pos() Pos

type Style

type Style struct {
	// Indent is one level of indentation. Two spaces everywhere measured:
	// the zsh distribution and a third-party plugin tree agree at 92% and
	// 97%, bash leans 60/36 and the Google Shell Style Guide settles it,
	// and this tree's .editorconfig says the same.
	//
	// A `<<-` here-document body is unaffected however this reads, because
	// bodies are emitted verbatim — which is also what keeps the tabs that
	// make `<<-` strip anything at all.
	Indent string

	// ThenOnHeaderLine and DoOnHeaderLine put the keyword that opens a body
	// on the line of its header — `if cond; then`, `for x in …; do` — rather
	// than on a line of its own.
	//
	// Two fields and not one because a shell may answer them differently;
	// none measured does. The header's line wins by 13:1 or better in every
	// dialect with a sample worth counting.
	ThenOnHeaderLine bool
	DoOnHeaderLine   bool

	// BraceShortForm says what becomes of a body the author wrote with
	// braces where the dialect also has the keyword spelling.
	//
	// This is the one field the dialects disagree on. zsh alone spells
	// `if cond { … }`, `for f ( a b c ) { … }`, `while cond { … }` and
	// `repeat n { … }`, and those parse to the same tree as the keyword
	// form — no AST field separates them. A formatter printing from the
	// tree alone therefore rewrites one spelling into the other silently,
	// across some five hundred lines of a real zsh tree.
	//
	// Where no other dialect can parse a brace body at all, the answer
	// costs nothing; zsh's is the one that matters.
	BraceShortForm ShortForm

	// AlignTrailingComments lines up the `#` of consecutive trailing
	// comments at one indent, one space past the run's longest code line. A
	// run of one keeps a single space.
	//
	// Computed rather than inherited from the author's spaces, so the column
	// is consistent by construction — which is the difference between
	// aligning and preserving padding.
	AlignTrailingComments bool

	// MaxBlankLines is how many consecutive blank lines survive between
	// items. gofmt's rule, adopted: one, and none directly under an opener.
	MaxBlankLines int
}

Style is how a formatter lays a script out — the questions the grammar does not ask, answered per dialect by `dialect/<shell>.Style()`.

Nothing here names a shell, and nothing here decides what a token says. A field belongs in this struct only if a dialect could answer it differently or a named style guide put it up for decision; docs/spec/style.md records which fields currently have one answer across all four dialects, and on what evidence.

func CoreStyle

func CoreStyle() Style

CoreStyle is the arrangement for the core language — the common denominator, and what a file that declares no dialect is laid out with.

Every dialect preset starts here and overrides what it was measured to answer differently, which today is one field in one shell. Starting from the core rather than from a sibling is the same rule the other three vectors follow, and for the reason `dialect/doc.go` gives: a preset that inherits from a sibling inherits its future mistakes.

type SubscriptEnd

type SubscriptEnd struct {
	// Flags is the group this end opened with, nil where it opened with
	// none.
	Flags *SubscriptFlags
	// Text is the end behind that group, and the whole of the end where
	// there was no group — the same word as Flags.Arg when Flags is set, so
	// a reading needs only this one.
	Text *Word
}

SubscriptEnd is one end of a subscript written as a pair: the flag group that end opened with, and the text behind it.

type SubscriptFlags

type SubscriptFlags struct {
	// Src is the group as written, parentheses included: `(rn:2:)`.
	Src string
	// Flags is the letters in written order with their arguments stripped,
	// so `(rn:2:)` carries "rn".
	Flags string
	// Nth and Begin are the `n` and `b` arguments, and Sep the `s` one, as
	// written. They are *not* expanded: measured, `(rb:$one:)` reads `$one`
	// as arithmetic and fails there rather than substituting a value.
	Nth   string
	Begin string
	Sep   string
	// Arg is the subscript after the group.
	Arg *Word
	// PatternSpendsAnEscapedQuote says a `\"` in Arg is spent before it is
	// read as a pattern, which is so where the expansion this group belongs
	// to stands inside the subscript of one written in double quotes. See
	// ParamExpr.InsideASubscript.
	PatternSpendsAnEscapedQuote bool
}

SubscriptFlags is the flag group a subscript opened with.

Arg is the rest of the subscript — the operand the search flags match against, and the ordinary subscript when the group named no search flag at all: `${a[()2]}` and `${a[(e)2]}` are both the second element, measured. Keeping it here rather than replacing ParamExpr.Index leaves the written subscript recoverable, which a diagnostic that names `a[(re)x]` needs.

func SubscriptFromText added in v0.0.21

func SubscriptFromText(text string) (flags *SubscriptFlags, ok bool)

SubscriptFromText splits a subscript that arrives as **text** into the flag group it opens with and the operand behind it, for a caller outside this package that has the characters and not a parse.

Exported because the set-ness operators reach a subscript that way. `[[ -v a[(i)x] ]]`, `test -v 'a[(i)x]'` and `[ -v 'a[(i)x]' ]` all arrive at the element lookup holding `(i)x` as a string, and wrapping that in a literal word makes the arithmetic reader answer `bad math expression: operator expected` — where `${a[(i)x]}` reads the group and searches. The two routes asked the same question and only one of them could see the flags.

The operand is a literal word for the reason the expression reader's own call gives: the text has already been expanded once, so a second pass would perform a substitution twice.

ok is false where the text opens no group, and the caller should then use the text as it stands.

The operand carries no position: the text did not come from a parse, so there is no offset in any source that it stands at, and inventing one would put a wrong line number in any diagnostic that reads it.

type SubscriptQuoting added in v0.0.4

type SubscriptQuoting uint8

SubscriptQuoting is the set of quoting constructs that hold a `]` back from ending a `${name[ … ]}` subscript.

A set rather than an ordered policy, because the panel does not nest: bash takes the backslash and both quotes and leaves `$'…'` out, zsh takes the backslash alone, and ksh93 takes all four. See Dialect.SubscriptQuoteProtectsTheClosingBracket for the measurement.

const (
	// SubscriptBackslashQuotes reads `\]` as the character and not as the end
	// of the subscript. bash 5.3, ksh93 and zsh.
	SubscriptBackslashQuotes SubscriptQuoting = 1 << iota
	// SubscriptSingleQuotes reads `'…'` as a quoted run. bash 5.3 and ksh93;
	// not zsh, where `${a['0]'+1]}` stops at the quoted bracket and reports a
	// math error against `'0`.
	SubscriptSingleQuotes
	// SubscriptDoubleQuotes reads `"…"` as a quoted run, with a backslash
	// inside it quoting the next byte. bash 5.3 and ksh93.
	SubscriptDoubleQuotes
	// SubscriptDollarSingleQuotes reads `$'…'` as a quoted run. ksh93 alone —
	// bash 5.3 stops at the bracket inside it, which is the one cell where
	// bash and ksh93 part.
	SubscriptDollarSingleQuotes
)

func (SubscriptQuoting) Has added in v0.0.4

Has says q holds the given construct.

func (SubscriptQuoting) String added in v0.0.4

func (q SubscriptQuoting) String() string

type SubscriptRange

type SubscriptRange struct {
	// Lo and Hi are the two ends, in written order.
	Lo, Hi SubscriptEnd
}

SubscriptRange is a subscript written as a pair where at least one end opens with a flag group of its own: `${s[(r)l,(r)o]}` searches twice, and `${s[3,(r)o]}` searches once.

It exists because the two ends are not one operand. scanSubscriptFlags takes the group off the front and hands everything after it on, so the second group ended up inside the first one's operand and the search looked for a literal comma and a parenthesis — a miss, and an empty answer that reads as "nothing matched" (#1533).

**Where the comma is decides this, and the comma has to be written.** Measured on zsh 5.9.2: `i="1,2"; ${a[$i]}` is the *first* element there and not the range `1,2`, so a comma that arrives through a substitution never separates a pair, and a comma written inside an operand always does — `a=(p q,r s); ${a[(r)q,r]}` is empty, the search for `q` and the arithmetic `r` read as two ends, rather than the element whose value holds the comma. So the split is the parser's and not the run's.

Only where the grammar has flag groups at all, and **every** written pair there — a pair of plain arithmetic ends included. It was the flagged pairs alone, and the plain ones were separated out of the subscript's *expanded* text at the run instead, which is the same rule applied to the wrong text: `i="1,2"; ${a[$i]}` came back a two-element range where the shell answers the first element, because the comma the substitution brought in had never been written (#2160).

type Subshell

type Subshell struct {
	List  []*Stmt
	Start Pos
	Stop  Pos
	// contains filtered or unexported fields
}

Subshell is `( list )`, which runs in a child shell so its assignments do not escape.

func (*Subshell) End

func (c *Subshell) End() Pos

func (*Subshell) Pos

func (c *Subshell) Pos() Pos

type SubstitutionBodyRead added in v0.0.10

type SubstitutionBodyRead uint8

SeparatorSkip is how far a dialect will step over a `;` written where the SubstitutionBodyRead says **when** a command substitution's body is parsed: while the line that holds it is read, or when the substitution runs.

The body is a program written inside a word, so the two moments are both defensible and the panel is split three ways rather than two — which is why this is a value and not a bool. Measured 2026-09-19, script files under `env -i PATH=/usr/bin:/bin LC_ALL=C`, standard input on the null device, over `echo before; v=$(if); echo after` and the same line written with backquotes:

column                 `$( … )`   `` ` … ` ``
dash 0.5.12            with the line   with the line
BusyBox ash 1.37.0     with the line   with the line
bash 5.3.20            with the line   when it runs
bash 5.3.20 as `sh`    with the line   when it runs
bash 3.2.57            when it runs    when it runs
ksh93u+                when it runs    when it runs
zsh 5.9.2              when it runs    when it runs

`before` is what says which: a shell that read the body with the line refuses the whole line and prints nothing. The control that makes it a statement about *parsing* rather than about how far a failure unwinds is the same body in a branch nothing takes — `false && v=$(if); echo "after=$?"` — where dash, BusyBox ash and the two 5.3 columns still refuse and bash 3.2, ksh93 and zsh print `after=1` with nothing said.

The split runs *through* bash, which is the part a reading keyed on the shell's name gets wrong: 5.3 reads the newer spelling with the line and 3.2 does not.

const (
	// SubstitutionBodyReadWhenItRuns is the core answer: the body is kept as
	// source and parsed at the moment the word is expanded, so a body that
	// will not parse is not a fact about the line at all. bash 3.2, ksh93
	// and zsh.
	SubstitutionBodyReadWhenItRuns SubstitutionBodyRead = iota

	// NewerSubstitutionBodyReadWithItsLine parses a `$( … )` body while the
	// line is read and leaves the older spelling to be read when it runs.
	// bash 5.3 and the same build as `sh`.
	//
	// Measured on the nesting as well as on the plain shape: `v=$(echo
	// $(if))` is refused with the line there and “ v=`echo $(if)` “ and
	// “ v=$(echo `if`) “ are not, so it is the spelling of **each** body
	// and not of the outermost one.
	NewerSubstitutionBodyReadWithItsLine

	// EverySubstitutionBodyReadWithItsLine parses both spellings with the
	// line. dash and BusyBox ash.
	EverySubstitutionBodyReadWithItsLine
)

type Terminator added in v0.0.10

type Terminator uint8

Terminator is which token ended a statement — see Stmt.Term.

Three values and not four: a statement ended with `&`, `&!`, `&|` or `|&` is a TerminatedByNothing one carrying Background, since the operator is already recorded there and the terminator question has the same answer whichever of the four was written.

const (
	// TerminatedByNothing is a statement with no terminator of its own: the
	// last of a body, one closed by the `}` or `)` around it, one ended by a
	// `&`, and one whose own body took the separator — see the parser's
	// bodyTookTerm, which hands the inner statement's answer out with it.
	TerminatedByNothing Terminator = iota

	// TerminatedBySemicolon is a `;` written after the statement.
	TerminatedBySemicolon

	// TerminatedByNewline is a line break written after the statement, which
	// includes the one a comment line stands before: a comment is not in the
	// tree, so what terminated `a` in `a # c` and a newline is the newline.
	TerminatedByNewline
)

type TestClause

type TestClause struct {
	Expr  CondExpr
	Start Pos
	Stop  Pos
	// contains filtered or unexported fields
}

TestClause is `[[ … ]]`.

func (*TestClause) End

func (c *TestClause) End() Pos

func (*TestClause) Pos

func (c *TestClause) Pos() Pos

type TimeClause

type TimeClause struct {
	// Negated records a `!` written before `time`, which negates the timed
	// pipeline's status — `! time true` is 1 and still reports. A `!` after
	// `time` is the pipeline's own and lives on it.
	Negated bool
	Bang    Pos
	Time    Pos
	// Posix is the `-p` flag, which switches the report to the POSIX format.
	// Read only where [Dialect.TimePosixFlag] says so; elsewhere a `-p` is
	// the first word of the pipeline.
	Posix    bool
	PosixPos Pos
	// Pipeline is what is timed: a *Pipeline, another *TimeClause, or nil
	// for a bare `time`, which runs nothing and still reports.
	Pipeline Expr
	// Stop is the end of the last keyword token, for a bare `time` that has
	// no pipeline to end at.
	Stop Pos
}

TimeClause is `time [-p] [pipeline]`: the pipeline runs whole and the shell writes how long it took to its own standard error — which is why the report of `time true 2>&1 | wc -l` is not counted: the redirection belongs to an element inside the pipeline and the report lands outside it.

It is an expression rather than a command because that is where it binds: the whole pipeline and nothing past it. `time true && echo ok` times `true` alone.

func (*TimeClause) End

func (t *TimeClause) End() Pos

func (*TimeClause) Pos

func (t *TimeClause) Pos() Pos

type Token

type Token struct {
	Kind Kind
	Pos  Pos
	End  Pos

	// Text is the exact source of the token, quotes and all. It is what a
	// caller echoes back when reporting an error, and what a highlighter
	// measures.
	Text string

	// Spans is set for [TokWord] tokens and is empty otherwise.
	Spans []Span
	// contains filtered or unexported fields
}

Token is one lexical unit.

func (Token) IsQuoted

func (t Token) IsQuoted() bool

IsQuoted reports whether any part of the word was quoted. A word can be partly quoted, so this is not the same as "the word was written in quotes".

func (Token) Literal

func (t Token) Literal() string

Literal returns the word's spans joined, which is the word with its quote characters removed and nothing else done to it. It is meaningful only when the caller has already decided no expansion applies — a here-document delimiter, for instance.

type TokenClass

type TokenClass int

TokenClass is what sort of thing a token is, for the dialect that words an ordinary word differently from a reserved one.

const (
	// ClassOperator is punctuation: `&`, `)`, `}`.
	ClassOperator TokenClass = iota
	// ClassWord is an ordinary word, the one class dash does not quote.
	ClassWord
	// ClassReserved is a word the grammar reserves: `fi`, `do`, `esac`.
	ClassReserved
	// ClassNewline is the newline, which is a token here and is not
	// punctuation: the dialect that leaves a word unquoted leaves this one
	// unquoted too — `newline unexpected` beside `";;" unexpected` — and one
	// other spells it `\n` rather than by name.
	//
	// Its own class rather than an operator because both of those facts are
	// about the newline alone: every other member of ClassOperator is quoted
	// by that dialect and named by the characters it was written with.
	ClassNewline
)

type TopLevelAlternation added in v0.0.1

type TopLevelAlternation uint8

TopLevelAlternation is how a dialect reads a `|` standing outside every group and bracket — see Dialect.PatternTopLevelAlternation.

Two shells have the reading and they do not have the same one, which is why this is not a bool. Measured 2026-09-13 on ksh93u+ 2012-08-01 and zsh 5.9.2, `env -i PATH=/usr/bin:/bin` over a script file, with `v=abc` and `L='a|ab'`:

probe                        ksh93        zsh
${v#a|ab}   written bar      bc           abc
${v#$L}     value bar        bc           abc  (bc under ${~L})
case via a value             matches      does not
$P with P='a|b', files a b   one field    two fields under globsubst
[[ ab == $L ]]               no           no

Two rows carry the argument. The **fourth** is what keeps the readings apart in the same direction they differ everywhere else, and it is measured against a control: in that same run ksh93 expands `a*` and `a?` out of a value to two fields each, so it is the *bar* that does not reach pathname expansion there and not the value failing to be a pattern. And `w='a|b'; ${w#a|b}` answers `|b` in ksh93 where zsh answers empty — the value stops matching its own text, which is what says the bar is syntax there rather than one more character.

const (
	// NoTopLevelAlternation reads the bar as an ordinary character. The
	// zero value, and what four of the panel's seven columns do.
	NoTopLevelAlternation TopLevelAlternation = iota

	// TopLevelAlternationFromAValue is zsh's: the bar is an alternation
	// only where a **value** supplied it, so a written one is an ordinary
	// character and the provenance has to be arranged — see
	// interp's markWrittenBars (#2168). It reaches pathname expansion,
	// where a value under `globsubst` becomes a pattern in its own right.
	TopLevelAlternationFromAValue

	// TopLevelAlternationWhereverWritten is ksh93's, and is wider in one
	// direction and narrower in another. Wider: provenance is not asked, so
	// a bar the script wrote is an alternation exactly as one out of a value
	// is. Narrower: it does **not** reach pathname expansion, and it does
	// not reach `[[ ]]`.
	//
	// So neither reading contains the other, which is the shape this whole
	// vector exists for — turning the old bool on for ksh would have given
	// it zsh's provenance rule, which is the wrong one (#2528).
	TopLevelAlternationWhereverWritten
)

func (TopLevelAlternation) ReachesPathnameExpansion added in v0.0.1

func (t TopLevelAlternation) ReachesPathnameExpansion() bool

ReachesPathnameExpansion reports whether a top-level bar makes a field a pattern against the filesystem.

Only zsh's reading does. Stated as a method rather than derived at the call site because the two facts are independent — a dialect could have ksh's provenance rule and zsh's reach — and because the one caller is a hundred lines from the flag.

func (TopLevelAlternation) ReadsATopLevelBar added in v0.0.1

func (t TopLevelAlternation) ReadsATopLevelBar(condition bool) bool

ReadsATopLevelBar reports whether a bar outside every group is an alternation, in the contexts that ask about pattern text rather than about file names. condition says the pattern stands inside `[[ ]]`.

The two readings part company there, and **the simpler rule is wrong**: both shells answer `n` to `L='a|ab'; [[ ab == $L ]]`, which reads as "no dialect reads a bar in a condition" — but turning it off for both takes zsh's `setopt globsubst; [[ ab == $L ]]` from matching to not, because there the value has become a pattern in its own right and the bar inside it is live. ksh93 has no such option and answers `n` either way. Measured 2026-09-13; the globsubst row is what distinguishes the two rules, and a table without it would have justified the wrong one.

func (TopLevelAlternation) ReadsAWrittenBar added in v0.0.1

func (t TopLevelAlternation) ReadsAWrittenBar() bool

ReadsAWrittenBar reports whether the bar is an alternation however it arrived, which is the question markWrittenBars asks: where it is false and the reading is on, a written bar has to be escaped so only a value's survives.

type TranslatedString added in v0.0.10

type TranslatedString struct {
	Text string
	Line int32
}

TranslatedString is one `$"…"` the program was written with: the text between its quotes, and the line it stands on.

The text is what the printer would write back inside the quotes, which is the same thing a reader of the script sees — escapes as written, expansions as written, nothing performed. Nothing here translates it and no message catalog is consulted; see Span.Translated.

func TranslatedStrings added in v0.0.10

func TranslatedStrings(f *File) []TranslatedString

TranslatedStrings is every `$"…"` in a file, in the order it was written.

It is the printer walking the tree rather than a walk of its own, and that is the whole reason it is here instead of in the caller. A second traversal would have to know every node kind, and the one that is missed is the one nobody notices — where a printer that failed to reach a node would be writing a program back with a piece of it missing, which nothing in this tree could keep quiet about.

What it does *not* reach is a substitution's interior: a `$(…)` span holds its script as unparsed text, so a `$"…"` written inside one is text here rather than a run. That is the same recursion a listing of the span would need; see the printer's own note about it.

type TryClause

type TryClause struct {
	// Try is the first half, run first and whatever happens.
	Try []*Stmt
	// Always is the second half, run however the first half ended.
	Always []*Stmt
	Start  Pos
	Stop   Pos
	// contains filtered or unexported fields
}

TryClause is `{ … } always { … }`, the try-always block.

The two halves are lists rather than Group nodes, the way an IfClause's branches are: the braces are mandatory punctuation of this production, so a nested group node would carry no fact the construct does not already state — and a list is what lets `break` inside the try half reach the loop around the whole construct rather than a group standing in the way.

Redirs belongs to the construct and not to either half, which is measured: `{ echo t; } always { echo a; } > /dev/null` sends *both* halves there, and a redirection written after the try half instead ends it, so that spelling is a parse error. See Dialect.TryAlways for the grammar.

func (*TryClause) End

func (c *TryClause) End() Pos

func (*TryClause) Pos

func (c *TryClause) Pos() Pos

type Word

type Word struct {
	Spans []Span
	Start Pos
	Stop  Pos
}

Word is one word: a sequence of spans, because quoting is recorded per span and not per word.

func ElementSubscript added in v0.0.1

func ElementSubscript(w *Word) (sub, value *Word, appends, ok bool)

ElementSubscript splits one element of a compound literal into the subscript it names and the value it gives — `[sub]=value`, or `[sub]+=value` for the spelling that joins what the element already holds.

The split is on the word **as written**, before any expansion, and that is the whole reason it lives here rather than in the interpreter: expanding first would hand `[k]=v` to the pattern matcher, where `[k]` is a character class. Both halves come back as words for the caller to expand as an assignment's value, which is what keeps `[k]=$x` whole and `[k]=*` a star.

ok is false for a plain word, which is every element that does not open with an unquoted `[` and close it with a `]=` or a `]+=`. A quoted head is text: `a=("[1]=A" p)` is two ordinary words in every shell that reads the bracketed spelling at all.

One scan serves two callers on purpose. The parser asks only whether the element is subscripted — one dialect decides a literal's whole shape on that answer for the first element, see Dialect.ArrayLiteralShapeFollowsTheFirstElement — and the interpreter asks for the halves. A second scan beside this one is how the two would come to disagree about what a subscripted element *is*, and then a literal would parse under one reading and store under the other.

func ProcSubstInSubscriptAsText added in v0.0.13

func ProcSubstInSubscriptAsText(w *Word, isSubscript bool) *Word

ProcSubstInSubscriptAsText returns w with every process substitution that stands inside an **array subscript** read as the characters it was written with, and w itself where there is none.

Inside a subscript a script wrote, `<(` and `>(` are the arithmetic `<` and a grouping rather than a substitution. Measured 2026-09-22 from a script file under `env -i PATH=/usr/bin:/bin LC_ALL=C bash f.sh`, standard input on the null device, bash 5.3.20, with `a=(1 2 3)` and `declare -A A`:

${a[0<(1)]}          2 — the subscript is `0<1`, which is 1
${a[1>(2)]}          1 — and `1>2`, which is 0
A[1<(2)]=v           the key is the five characters `1<(2)`
[[ a[1<(2)] -lt 9 ]] holds, and nothing is run
$(( a[1<(2)] ))      2

and the two controls that say it is the *subscript* rather than the construct or the brackets:

[[ -e <(echo x) ]]   a substitution, at the front of the same operand
echo a[1<(2)]        `a[1/dev/fd/63]` — brackets in an ordinary command
                     word are no subscript, so the substitution is made

Performing it instead answered a script from a descriptor number divided into a subscript: `${a[1<(2)]}` was `1/dev/fd/63: division by 0` and handed back nothing where the element is `2`, with a diagnostic naming a path no line of the script mentions (#4253).

isSubscript says the word **is** a subscript, so every span in it stands inside one. Where it is false the word is scanned for the brackets it writes itself, which is what a condition's arithmetic operand needs: one word may hold a subscript and the next may open with a substitution, and only the brackets part them.

The substitution is replaced rather than refused, because the text is still an expression — `0<(1)` reads as `0<1`, parentheses and all — and a reader that saw nothing there would take the subscript for `0`.

func (*Word) End

func (w *Word) End() Pos

func (*Word) IsQuoted

func (w *Word) IsQuoted() bool

IsQuoted reports whether any span of the word was quoted. A word can be partly quoted, so this is not "the word was written in quotes" — and for a condition's right operand it is what separates a pattern from a literal.

func (*Word) Literal

func (w *Word) Literal() string

Literal joins the spans, which is the word with its quote characters removed and nothing else done. Meaningful only where no expansion applies.

func (*Word) Pos

func (w *Word) Pos() Pos

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL