You almost got it right! :-)
But you were "bitten" by two subtleties related to the way in which TeX parses table-cells.
You didn't say whether you can explain how the incomplete ! Incomplete \ifx; all text was ignored after line...
-error comes into being, so let's look at your definition of \letinscan
:
\newcommand{\letinscan}[1]{% --> this command processes the "let" arguments
\ifx\relax#1\empty
\else
\ifletinsep
, \\
\else
\letinseptrue
\fi
 % ----> when I remove this alignment character, the command executes without error
\expandafter\letinscan
\fi
}
This command is carried out inside an aligned
-environment which internally is something like a table where single cells are separated from each other by &
.
So there are two problems:
Problem 1:
The content of a table-cell forms a local scope. Thus the assignment \letinseptrue
is restricted to the local scope formed by the table-cell. It needs to be preceded with \global
.
I suggest preceding the assignment \letinsepfalse
within the definition of \letin
with \global
, too.
Problem 2:
In case #1
's first token's meaning equals the current meaning of the control-word-token \relax
, the \ifx\relax#1
-condition is true, and the \else
-branch inclusive the \fi
belonging to \ifx
is to be skipped. But there is &
which is not nested in braces in that branch. Thus at the time of skipping the \ifx
's \else
-branch TeX "assumes" that the current table-cell ends at that &
. This interferes with the \ifx
's \else
-branch's \fi
being somewhere behind that &
: As the table-cell ends before encountering the matching \fi
, TeX "thinks" that \fi
is missing.
You can resolve this by putting &
in a set of curly braces that get discarded in case things don't get skipped but get carried out, e.g., as argument of a macro \iden
which just spits out its argument.
In the following code four modifications are done to your code so that things work out as expected by you:
\newcommand\iden[1]{#1} %%%%%%%%% modification 1: added macro \iden
\newif\ifletinsep % --> this condition is used to prevent a comma for the last "let" argument
\newcommand*{\letin}[2]{% --> this is the command we want
\begin{aligned}[t]%
\textbf{let }%
\global\letinsepfalse %%%%%%%%% modification 2: added \global
\letinscan#1\relax\\%
\textbf{in }
\end{aligned}%
}
\newcommand{\letinscan}[1]{% --> this command processes the "let" arguments
\ifx\relax#1\empty
\else
\ifletinsep
,\\%
\else
\global\letinseptrue %%%%%%%%% modification 3: added \global
\fi
\iden{&}#1% %%%%%%%%% modification 4: hide "&" in the argument-braces of \iden
\expandafter\letinscan
\fi
}
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
\[\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}\]
\noindent\hrule
\[\letin{{x = 1}{y = 2}}{x*y}\]
\noindent\hrule
\[\letin{{x = 1}}{x}\]
\noindent\hrule
\[\letin{}{\varnothing}\]
\end{document}

By now the idea is to iterate until encountering some "sentinel-token" (\relax
with your code, \letinstop
with my code) denoting the end of the list of arguments.
To be honest, all this \newif\ifletinsep
- and \global\letinsepfalse
/\global\letinseptrue
-assignment-trickery seems cumbersome to me.
Probably expansion-based tail-recursion without any intermediate/temporary assignment for setting an \if..
-switch but with another macro-argument, holding emptiness in the first iteration where no separator is to be inserted and holding the ,\\
-separator in subsequent iterations, does the trick as well:
\newcommand\letin[2]{%
% #1 - List with a variable number of undelimited "let-arguments".
% The single "let-arguments" must not contain unbalanced
% \if../\else/\fi !!!
% The single "let-arguments" must not have a leading token
% whose meaning equals the meaning of \letinstop !!!
% #2 - "in-argument"=stuff to append to bold phrase "in ".
\begin{aligned}[t]\textbf{let }%
\letinloop{}#1\letinstop
\\\textbf{in }\end{aligned}%
}%
\newcommand\letinstop{\letinstop}%
\newcommand\secondfirst[2]{#2#1}%
\newcommand\letinloop[2]{%
% #1 - Separator/Tokens to prepend to & and this iteration's
% "let-argument".
% In the first iteration this argument is empty.
% In subsequent iterations this argument holds the tokens: ,\\
% #2 - Either this iteration's "let-argument" or \letinstop-quark.
\ifx\letinstop#2\empty\else\secondfirst{#1\letinloop{,\\}}\fi
}%
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
\[\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}\]
\noindent\hrule
\[\letin{{x = 1}{y = 2}}{x*y}\]
\noindent\hrule
\[\letin{{x = 1}}{x}\]
\noindent\hrule
\[\letin{}{\varnothing}\]
\end{document}

Instead of using \ifx
for checking if a token with the meaning of the sentinel-token denoting that the end of the list of let-arguments is reached, you can use a mechanism based on delimited arguments for checking the presence of the sentinel-token.
This way—at the cost of defining more macros—you reduce the restrictions from
"the single 'let-arguments' not being allowed to contain unbalanced
\if..
/\else
/\fi
and the single 'let-arguments' not being allowed to have a leading token whose meaning equals the meaning of \letinstop
"
to
"the single 'let-arguments' not being allowed to consist of the single token \letinstop
":
As with this approach the presence of the sentinel-token is checked instead of checking meanings via \ifx
, you don't need to define the sentinel-token. (Instead of a single sentinel-token with this approach you could as well use a sentinel-phrase consisting of a set of tokens suitable as delimiter of a delimited macro argument.)
\makeatletter
% \CheckWhetherletinstop{<tokens to check>}%
% {<Tokens in case <tokens to check> consists
% only of the single token \letinstop>}%
% {<Tokens in case <tokens to check> does not
% consist only of the single token \letinstop>}%
% Test if the argument consists of the single token \letinstop.
% Internally an argument delimited by !\letinstop! is used.
% So first crank out the case of the argument containing ! as
% a case of the argument not being the single token \letinstop.
% Then you are safe that the argument is not something like
% !\letinstop!whatsoever
% which would also match the delimiter and thus fool the check.
\newcommand\CheckWhetherletinstop[1]{%
% #1 - <tokens to check>.
% \ifcat$\detokenize{<arg to check>}$<true>\else<false>\fi
% checks whether the set of tokens formed by <arg to check>
% is empty. Thus the following checks if #1 contains an
% exclamation-mark which is not nested in curly braces.
\ifcat$\detokenize\expandafter{\gobbletoexclam#1!}$%
\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
{\letinstopFork!#1!{\@firstoftwo}!\letinstop!{\@secondoftwo}!!!!}%
{\@secondoftwo}%
}%
\@ifdefinable\gobbletoexclam{\long\def\gobbletoexclam#1!{}}%
\@ifdefinable\letinstopFork{%
\long\def\letinstopFork#1!\letinstop!#2#3!!!!{#2}%
}%
\makeatother
\newcommand\letin[2]{%
% #1 - List with a variable number of undelimited "let-arguments".
% The single "let-arguments" must not consist of the single
% token \letinstop !!!
% #2 - "in-argument"=stuff to append to bold phrase "in ".
\begin{aligned}[t]\textbf{let }%
\letinloop{}#1\letinstop
\\\textbf{in }\end{aligned}%
}%
\newcommand\letinloop[2]{%
% #1 - Separator/Tokens to prepend to & and this iteration's
% "let-argument".
% In the first iteration this argument is empty.
% In subsequent iterations this argument holds the tokens: ,\\
% #2 - Either this iteration's "let-argument" or \letinstop-token.
\CheckWhetherletinstop{#2}{}{#1\letinloop{,\\}}%
}%
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
\[\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}\]
\noindent\hrule
\[\letin{{x = 1}{y = 2}}{x*y}\]
\noindent\hrule
\[\letin{{x = 1}}{x}\]
\noindent\hrule
\[\letin{}{\varnothing}\]
\end{document}

Remark:
In subsequent coding-examples you find \romannumeral
.
This may seem confusing at first glimpse.
In the subsequent coding-examples \romannumeral
is not used for obtaining lowercase-roman-representation of whatsoever number but is (ab?)used for triggering a lot of expansion-work and flipping-macro-arguments-around-work so that in case of needing to be in control of expansion you don't need so many/long \expandafter
-chains.
\romannumeral
usually is used for "gobbling" tokens that form a TeX-⟨number⟩-quantity and in return for them delivering character-tokens which form the representation of the value of that TeX-⟨number⟩-quantity in lowercase roman numerals.
\romannumeral
in any case triggers "gobbling" those tokens that form the TeX-⟨number⟩-quantity. But in case that TeX-⟨number⟩-quantity does have a value which is not positive, silently, i.e., without error-message or the like, no tokens at all are delivered in return.
Besides this while searching for tokens belonging to the TeX-⟨number⟩-quantity, expansion of expandable tokens is not suppressed.
Thus you can (ab?)use \romannumeral
's searching for tokens belonging to the TeX-⟨number⟩-quantity for having (La)TeX doing a lot of expansion-work and flipping-macro-arguments-around-work as long as it is ensured that in the end a TeX-⟨number⟩-quantity is found whose value is not positive.
In the subsequent coding-examples as "TeX-⟨number⟩-quantity whose value is not positive" the \chardef
-token \stopromannumeral
/\UD@stopromannumeral
is used.
You specified that \letin{{x = 1}{y = 2}{z = 3}}{x*y + z}
should expand to
\begin{aligned}[t]
\textbf{let }&x = 1,\\
&y = 2,\\
&z = 3\\
\textbf{in }&x*y + z
\end{aligned}
, and strictly spoken with the above code-examples that set of tokens is not delivered as a whole. Instead, by and by things are delivered and also processed(!) immediately that are further ahead in that set of tokens before delivering and processing the things that are further back in that set of tokens.
If you need the entire set of tokens available as a whole, e.g., for defining a macro from it, or for passing it on as argument of another macro, the approach above can be modified to collect tokens within a macro-argument.
This way the entire set of tokens that forms the result is available as a whole after triggering two expansion-steps on \letin
—the first one delivers \letin
's toplevel-expansion whose first token is \romannumeral
. The second one triggers \romannumeral
which in turn triggers subsequent expansion-steps while searching for tokens belonging to the TeX-⟨number⟩-quantity until TeX finds the token \stopromannumeral
which denotes a non-positive TeX-⟨number⟩-quantity causing TeX to gobble it and to end the \romannumeral
-routine without delivering tokens in return for that ⟨number⟩-quantity-token:
\newcommand\letin[2]{%
% #1 - List with a variable number of undelimited "let-arguments".
% The single "let-arguments" must not contain unbalanced
% \if../\else/\fi !!!
% The single "let-arguments" must not have a leading token
% whose meaning equals the meaning of \letinstop !!!
% #2 - "in-argument"=stuff to append to bold phrase "in ".
\romannumeral\letinloop{\begin{aligned}[t]\textbf{let }}{}{\\\textbf{in }\end{aligned}}#1\letinstop
}%
\csname @ifdefinable\endcsname\stopromannumeral{\chardef\stopromannumeral=`\^^00}%
\newcommand\letinstop{\letinstop}%
\newcommand\fot[2]{#1}%
\newcommand\sot[2]{#2}%
\newcommand\letinloop[4]{%
% #1 - Tokens of result gathered so far.
% Initially this argument holds the tokens:
% \begin{aligned}[t]\textbf{let }
% #2 - Separator/Tokens to prepend to & and this iteration's
% "let-argument".
% In the first iteration this argument is empty.
% In subsequent iterations this argument holds the tokens: ,\\
% #3 - Tokens to append to the <tokens of the result gathered so
% far> when the loop is done; this argument holds:
% \\\textbf{in }&<in-argument>\end{aligned}
% #4 - Either this iteration's "let-argument" or \letinstop-quark.
\ifx\letinstop#4\empty\expandafter\fot\else\expandafter\sot\fi
{\stopromannumeral#1#3}%
{\letinloop{#1#2}{,\\}{#3}}%
}%
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}}{x*y}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}}{x}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{}{\varnothing}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\end{document}

If you wish to make sure that things work out when being carried out inside a tabular-environment or inside an alignment or the like, where unbraced &
might erroneously be associated with the surrounding table-cell, you can do this by nesting the loop between the curly braces of a macro-argument—\fot
(=first of two) in this example:
\newcommand\letin[2]{%
% #1 - List with a variable number of undelimited "let-arguments".
% The single "let-arguments" must not contain unbalanced
% \if../\else/\fi !!!
% The single "let-arguments" must not have a leading token
% whose meaning equals the meaning of \letinstop !!!
% #2 - "in-argument"=stuff to append to bold phrase "in ".
\romannumeral\expandafter\fot\expandafter{%
\expandafter\stopromannumeral
\romannumeral\letinloop{\begin{aligned}[t]\textbf{let }}{}#1\letinstop
\\\textbf{in }\end{aligned}%
}{}%
}%
\csname @ifdefinable\endcsname\stopromannumeral{\chardef\stopromannumeral=`\^^00}%
\newcommand\letinstop{\letinstop}%
\newcommand\fot[2]{#1}%
\newcommand\sot[2]{#2}%
\newcommand\letinloop[3]{%
% #1 - Tokens of result gathered so far.
% Initially this argument holds the tokens:
% \begin{aligned}[t]\textbf{let }
% #2 - Separator/Tokens to prepend to & and this iteration's
% "let-argument".
% In the first iteration this argument is empty.
% In subsequent iterations this argument holds the tokens: ,\\
% #3 - Either this iteration's "let-argument" or \letinstop-quark.
\ifx\letinstop#3\empty\expandafter\fot\else\expandafter\sot\fi
{\stopromannumeral#1}{\letinloop{#1#2}{,\\}}%
}%
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}}{x*y}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}}{x}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{}{\varnothing}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\end{document}

By now the idea was to iterate until encountering some "sentinel-token" (\relax
with your code, \letinstop
with my code) denoting the end of the list of arguments.
Another approach is keeping the list of arguments as an argument itself and iterating and extracting/removing the first argument "inside" that argument until that argument is blank (=is empty or contains space-tokens only).
Advantages:
- When iterating until reaching a sentinel-token, you may not use the sentinel-token within the list of arguments. When iterating until having "blankness" there is no forbidden token.
- Iterating until having blankness can be implemented expandably without using any
\if..\else..\fi
-expression at all. Thus there is no danger of user's arguments containing unbalanced \if
or \else
or \fi
fooling the loop. Thus elements of the list of arguments can contain unbalanced \if
or \else
or \fi
. This won't disturb the algorithm. (But if you don't know what you are doing the result may not work out as expected by you.)
Disadvantages:
- You need a (sub-)routine for checking the "blankness" of a macro argument and you need a (sub-)routine for extracting an undelimited macro-argument's first undelimited macro-argument. Thus there is a lot of code for (sub-)routines. For private purposes I would probably not do this. I would probably only do this for a package where the code must be user-proof/must handle all kinds of weird user-input.
\makeatletter
%%=============================================================================
%% PARAPHERNALIA:
%% \UD@firstoftwo, \UD@secondoftwo, \UD@PassFirstToSecond, \UD@Exchange,
%% \UD@stopromannumeral, \UD@CheckWhetherNull, \UD@CheckWhetherBlank,
%% \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral\expandafter\UD@secondoftwo\string{\expandafter
\UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
\expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%% "fetching" _un_delimited arguments: --
%% \UD@CheckWhetherBlank{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that
%% argument which is to be checked is blank>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not blank>}%
\newcommand\UD@CheckWhetherBlank[1]{%
\romannumeral\expandafter\expandafter\expandafter\UD@secondoftwo
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo#1{}{}}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%.............................................................................
%% \UD@ExtractFirstArg{ABCDE} yields A
%%
%% \UD@ExtractFirstArg{{AB}CDE} yields AB
%%
%% Due to \romannumeral-expansion the result is delivered after two
%% expansion-steps/after "hitting" \UD@ExtractFirstArg with \expandafter
%% twice.
%%
%% \UD@ExtractFirstArg's argument must not be blank.
%% This case can be cranked out via \UD@CheckWhetherBlank before calling
%% \UD@ExtractFirstArg.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% Frozen-\relax is chosen because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \UD@ExtractFirstArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%.............................................................................
\@ifdefinable\UD@RemoveTillFrozenrelax{%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}%
{\long\def\UD@RemoveTillFrozenrelax#1#2}{{#1}}%
}%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral\expandafter
\UD@PassFirstToSecond\expandafter{\romannumeral
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral#1}%
}{%
\UD@stopromannumeral\romannumeral\UD@ExtractFirstArgLoop
}%
}{%
\newcommand\UD@ExtractFirstArg[1]%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{\expandafter\UD@stopromannumeral\UD@secondoftwo{}#1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillFrozenrelax#1}}%
}%
%%=============================================================================
%% \letin based on paraphernalia
%%=============================================================================
\newcommand\letin[2]{%
% #1 - List with a variable number of undelimited "let-arguments".
% #2 - "in-argument"=stuff to append to bold phrase "in ".
\romannumeral\letinloop{\begin{aligned}[t]\textbf{let }}{#1}{\\\textbf{in }\end{aligned}}{}%
}%
\newcommand\letinloop[4]{%
% #1 - Tokens of result gathered so far.
% Initially this argument holds the tokens:
% \begin{aligned}[t]\textbf{let }
% #2 - List with a variable number of undelimited "let-arguments".
% #3 - Tokens to append to the <tokens of the result gathered so
% far> when the loop is done; this argument holds:
% \\\textbf{in }&<in-argument>\end{aligned}
% #4 - Separator/Tokens to prepend to & and this iteration's
% "let-argument".
% In the first iteration this argument is empty.
% In subsequent iterations this argument holds the tokens: ,\\
\UD@CheckWhetherBlank{#2}{\UD@stopromannumeral#1#3}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#2}{%
\expandafter\letinloop\expandafter{%
\romannumeral
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\UD@ExtractFirstArg{#2}%
}{\UD@stopromannumeral#1#4&}%
}%
}%
{#3}{,\\}%
}%
}%
\makeatother
\documentclass{article}
\usepackage{amsmath, amssymb}
\begin{document}
This is an obscure thing where the fact that when starting a table cell TeX keeps
expanding until a non-expandable token is found is used for removing the first
list-item via \verb|\iffalse|---the items of the argument-list contain unbalanced
\verb|\if..| and \verb|\fi|:
\bigskip
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{ {\iffalse x = 1} {\fi y = 2} {z = 3} }{x*y + z}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}{z = 3}}{x*y + z}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}{y = 2}}{x*y}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{{x = 1}}{x}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\noindent\hrule
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\letin{}{\varnothing}}
\noindent\texttt{\string\temp:\\\meaning\temp}
\[\temp\]
\end{document}
