I think an expandable test entirely in TeX, not using Lua, that is reliable to 100% is not feasible—at least not feasible with unicode-TeX-engines like XeTeX or LuaTeX where 1114112 different character-codes are possible.
(I think handling explicit character-tokens of category 1/2 is feasible if you are willing to have a bunch of code just for doing some brace-hacking.)
The hard problems are:
I introduced the restriction of not using macros that process delimited arguments.
For each possible character-code you could write a macro for detecting by means of delimited arguments whether a token of that character-code is an active character of that character-code/is a one-letter-control-sequence whose name equals the character with the character-code in question/is something else.
But on a unicode-machine like xetex or luatex 1114112 character-codes from 0 to 1114111 are possible, thus 1114112 such macros would be needed. (Defining such macros "on the fly" is not an option as defining contradicts expandability.)
On a traditional 8-bit-engine where only 256 character-codes are possible, one could nowadays probably define 256 macros.
Outline of a mechanism for cranking out active-character and—in case of \escapechar being negative—one-letter-control-sequence via many macros that process delimited arguments—only applicable with 8-bit-engines if applicable at all:
Assuming that you wish to examine a macro-argument where you already know that it is
- either an explicit character token not of category 1 or 2
- or an active character token let equal to a pendant not of category 1 or 2
- or a single-letter-control-sequence let equal to a character-token not of category 1 or 2 whose character-code corresponds to the character forming the name of the single-letter-control-sequence while
\esapechar is negative,
i.e., in any case something not of category 1/2 whose stringification yields a single character-token of category 10 or 12
, you can expandably crank out active characters and (in the edge case of \escapechar being negative) single-letter-control-sequences expandably by means of something like
\long\def\tokenfork#1<token1><token2>#2#3\SEP{#2}%
\long\def\forktoken#1{%
\tokenfork
#1<token2>{tokens in case #1 = <token1>}%
<token1>#1{tokens in case #1 = <token2>}%
<token1><token2>{<tokens in case #1 is s.th. else}%
\SEP
}
where <token 1> is the active character and <token 2> is the single-letter-control-sequence.
Instead of \tokenfork and \forktoken you choose macro-names wherein the character occurs, e.g. \Afork and \forkA for cranking out active-A and \A via \csname fork\string#1\endcsname{#1} whereby #1 is something whose character-code is A and whose stringification yields the single letter A:
\long\def\firstofthree#1#2#3{#1}%
\long\def\secondofthree#1#2#3{#2}%
\long\def\thirdofthree#1#2#3{#3}%
\long\def\forkdefiner#1{\begingroup\lccode`\X=#1 \lccode`\~=#1 \lowercase{\let~\relax\definefork{X}{~}}}%
\long\def\definefork#1#2{%
\expandafter\let\csname #1fork\endcsname\relax
\expandafter\let\csname fork#1\endcsname\relax
\expandafter\let\csname#1\endcsname\relax
\expandafter\defineforkB\csname #1fork\expandafter\endcsname
\csname fork#1\expandafter\endcsname
\csname#1\endcsname{#2}%
}%
\long\def\defineforkB#1#2#3#4{%
\long\gdef#1##1#3#4##2##3\SEP{##2}%
\long\gdef#2##1{#1##1#4{\thirdofthree}#3##1{\secondofthree}#3#4{\firstofthree}\SEP}%
\endgroup
}%
% In a loop define two macros forming forking mechanism for the character-codes 0..255
% that are possible in traditional 8-bit engines:
\newcount\scratchy
\scratchy=-1
\loop
\ifnum\scratchy<255 %
\advance\scratchy by 1 %
\forkdefiner{\scratchy}%
\repeat
% Now test with letter-A, one-letter-\A, active-A
\message{%
^^J%
\csname fork\string A\endcsname{A}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}%
{\escapechar=-1 \message{%
^^J%
\csname fork\string\A\endcsname{\A}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}}%
{\catcode`\A=13 \message{%
^^J%
\csname fork\string A\endcsname{A}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}}%
% Now test with other-!, one-letter-\!, active-!
\message{%
^^J%
\csname fork\string!\endcsname{!}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}%
{\escapechar=-1 \message{%
^^J%
\csname fork\string\!\endcsname{\!}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}}%
{\catcode`\!=13 \message{%
^^J%
\csname fork\string!\endcsname{!}{neither active char nor one-letter-cs}%
{active char}%
{one-letter-cs}%
}}%
\bye
Output:
$ pdftex test.tex
This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdftex)
restricted \write18 enabled.
entering extended mode
(./test.tex
neither active char nor one-letter-cs
one-letter-cs
active char
neither active char nor one-letter-cs
one-letter-cs
active char )
No pages of output.
Transcript written on test.log.
When examining tokens, the nameless control-sequence, producible via \csname\endcsname or via ending a line of .tex-input with an escape-char (backslash) while \endlinechar is negative, might also need special treatment via delimited arguments because applying \string to it yields <escapechar>csname<escapechar>endcsname. You get the same stringification for a control-sequence-token whose name is csname<escapechar>endcsname, producible via \csname csname\string\endcsname\endcsname. There is the very very edge case of these two different tokens having assigned the same meaning so that examining meaning and \string-representation alone is not sufficient for distinguishing them.
There are more weird things, e.g., frozen-\relax (which cannot be redefined) versus normal \relax, ...
\str_if_eq_x:nn(TF). – Joseph Wright Feb 11 '15 at 09:55\ifxis an expanable primitive for comparing a token lists in TeX. – wipet Feb 11 '15 at 10:02\mytestneed itself to be expandable?' – Joseph Wright Feb 11 '15 at 11:59\ifxis expandable if the lists to be compared are replacement texts of macros, but the case here is to compare{abc}with{xyz}and to do that (easily) with\ifxyou have to define two temporary macros first, the trick is to avoid those assignments. – David Carlisle Feb 11 '15 at 13:40\ifxan use a delimited macro, perhaps with a string test first (to screen out input that is 'badly out'). – Joseph Wright Feb 11 '15 at 13:52