% \iffalse meta-comment
%
%% File: l3fp-parse.dtx
%
% Copyright (C) 2011-2025 The LaTeX Project
%
% It may be distributed and/or modified under the conditions of the
% LaTeX Project Public License (LPPL), either version 1.3c of this
% license or (at your option) any later version.  The latest version
% of this license is in the file
%
%    https://www.latex-project.org/lppl.txt
%
% This file is part of the "l3kernel bundle" (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% -----------------------------------------------------------------------
%
% The development version of the bundle can be found at
%
%    https://github.com/latex3/latex3
%
% for those people who are interested.
%
%<*driver>
\documentclass[full,kernel]{l3doc}
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \title{^^A
%   The \pkg{l3fp-parse} module\\
%   Floating point expression parsing^^A
% }
% \author{^^A
%  The \LaTeX{} Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
% \date{Released 2025-10-24}
%
% \maketitle
%
% \begin{documentation}
%
% \end{documentation}
%
% \begin{implementation}
%
% \section{\pkg{l3fp-parse} implementation}
%
%    \begin{macrocode}
%<*code>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=fp>
%    \end{macrocode}
%
% \subsection{Work plan}
%
% The task at hand is non-trivial, and some previous failed attempts
% show that the code leads to unreadable logs, so we had better get it
% (almost) right the first time.  Let us first describe our goal, then
% discuss the design precisely before writing any code.
%
% In this file at least, a \meta{floating point object} is a floating
% point number or tuple.  This can be extended to anything that starts
% with \cs{s_@@} or \cs{s_@@_\meta{type}} and ends with \cs{@@_sep:} with some
% internal structure that depends on the \meta{type}.
%
% \begin{macro}[EXP]{\@@_parse:n}
%   \begin{syntax}
%     \cs{@@_parse:n} \Arg{fp expr}
%   \end{syntax}
%   Evaluates the \meta{fp expr} and leaves the result
%   in the input stream as a floating point object.  This
%   function forms the basis of almost all public \pkg{l3fp} functions.
%   During evaluation, each token is fully \texttt{f}-expanded.
%
%   \cs{@@_parse_o:n} does the same but expands once after its result.
%   \begin{texnote}
%     Registers (integers, toks, etc.) are automatically unpacked,
%     without requiring a function such as \cs{int_use:N}.  Invalid
%     tokens remaining after \texttt{f}-expansion lead to
%     unrecoverable low-level \TeX{} errors.
%   \end{texnote}
% \end{macro}
%
% \begin{variable}
%   {
%     \c_@@_prec_func_int,
%     \c_@@_prec_hatii_int,
%     \c_@@_prec_hat_int,
%     \c_@@_prec_not_int,
%     \c_@@_prec_juxt_int,
%     \c_@@_prec_times_int,
%     \c_@@_prec_plus_int,
%     \c_@@_prec_comp_int,
%     \c_@@_prec_and_int,
%     \c_@@_prec_or_int,
%     \c_@@_prec_quest_int,
%     \c_@@_prec_colon_int,
%     \c_@@_prec_comma_int,
%     \c_@@_prec_tuple_int,
%     \c_@@_prec_end_int,
%   }
%   Floating point expressions are composed of numbers, given in various
%   forms, infix operators, such as |+|, |**|, or~|,| (which joins two
%   numbers into a list), and prefix operators, such as the unary~|-|,
%   functions, or opening parentheses.  Here is a list of precedences
%   which control the order of evaluation (some distinctions are
%   irrelevant for the order of evaluation, but serve as signals), from
%   the tightest binding to the loosest binding.
%   \begin{itemize}
%     \item[16] Function calls.
%     \item[13/14] Binary |**| and~|^| (right to left).
%     \item[12] Unary |+|, |-|, |!| (right to left).
%     \item[11] Juxtaposition (implicit~|*|) with no parenthesis.
%     \item[10] Binary |*| and~|/|.
%     \item[9] Binary |+| and~|-|.
%     \item[7] Comparisons.
%     \item[6] Logical \texttt{and}, denoted by~|&&|.
%     \item[5] Logical \texttt{or}, denoted by~\verb*+||+.
%     \item[4] Ternary operator |?:|, piece~|?|.
%     \item[3] Ternary operator |?:|, piece~|:|.
%     \item[2] Commas.
%     \item[1] Place where a comma is allowed and generates a tuple.
%     \item[0] Start and end of the expression.
%   \end{itemize}
%    \begin{macrocode}
\int_const:Nn \c_@@_prec_func_int   { 16 }
\int_const:Nn \c_@@_prec_hatii_int  { 14 }
\int_const:Nn \c_@@_prec_hat_int    { 13 }
\int_const:Nn \c_@@_prec_not_int    { 12 }
\int_const:Nn \c_@@_prec_juxt_int   { 11 }
\int_const:Nn \c_@@_prec_times_int  { 10 }
\int_const:Nn \c_@@_prec_plus_int   { 9 }
\int_const:Nn \c_@@_prec_comp_int   { 7 }
\int_const:Nn \c_@@_prec_and_int    { 6 }
\int_const:Nn \c_@@_prec_or_int     { 5 }
\int_const:Nn \c_@@_prec_quest_int  { 4 }
\int_const:Nn \c_@@_prec_colon_int  { 3 }
\int_const:Nn \c_@@_prec_comma_int  { 2 }
\int_const:Nn \c_@@_prec_tuple_int  { 1 }
\int_const:Nn \c_@@_prec_end_int    { 0 }
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Storing results}
%
% The main question in parsing expressions expandably is to decide where
% to put the intermediate results computed for various subexpressions.
%
% One option is to store the values at the start of the expression, and
% carry them together as the first argument of each macro.  However, we
% want to \texttt{f}-expand tokens one by one in the expression (as
% \cs{int_eval:n} does), and with this approach, expanding the next
% unread token forces us to jump with \cs{exp_after:wN} over every value
% computed earlier in the expression.  With this approach, the run-time
% grows at least quadratically in the length of the expression, if
% not as its cube (inserting the \cs{exp_after:wN} is tricky and slow).
%
% A second option is to place those values at the end of the expression.
% Then expanding the next unread token is straightforward, but this
% still hits a performance issue: for long expressions we would be
% reaching all the way to the end of the expression at every step of the
% calculation.  The run-time is again quadratic.
%
% A variation of the above attempts to place the intermediate results
% which appear when computing a parenthesized expression near the
% closing parenthesis.  This still lets us expand tokens as we go, and
% avoids performance problems as long as there are enough parentheses.
% However, it would be better to avoid requiring the closing
% parenthesis to be present as soon as the corresponding opening
% parenthesis is read: the closing parenthesis may still be hidden in a
% macro yet to be expanded.
%
% Hence, we need to go for some fine expansion control: the result is
% stored \emph{before} the start!
%
% Let us illustrate this idea in a simple model: adding positive
% integers which may be resulting from the expansion of macros, or may
% be values of registers.  Assume that one number, say, $12345$, has
% already been found, and that we want to parse the next number. The
% current status of the code may look as follows.
% \begin{syntax}
%   \cs{exp_after:wN} |\add:ww| \cs{int_value:w} 12345 \cs{exp_after:wN} ;
%   \cs{exp:w} |\operand:w| \meta{stuff}
% \end{syntax}
% One step of expansion expands \cs{exp_after:wN}, which triggers the
% primitive \cs{int_value:w}, which reads the five digits we have
% already found, |12345|.  This integer is unfinished, causing the
% second \cs{exp_after:wN} to expand, and to trigger the construction
% \cs{exp:w}, which expands |\operand:w|, defined to read
% what follows and make a number out of it, then leave \cs{exp_end:}, the
% number, and a \cs{@@_sep:} in the input stream.  Once |\operand:w| is
% done expanding, we obtain essentially
% \begin{syntax}
%   \cs{exp_after:wN} |\add:ww| \cs{int_value:w} 12345 \cs{@@_sep:}
%   \cs{exp:w} \cs{exp_end:} 333444 \cs{@@_sep:}
% \end{syntax}
% where in fact \cs{exp_after:wN} has already been expanded,
% \cs{int_value:w} has already seen |12345|, and
% \cs{exp:w} is still looking for a number.  It finds
% \cs{exp_end:}, hence expands to nothing.  Now, \cs{int_value:w} sees
% the \cs{@@_sep:}, which cannot be part of a number.  The expansion
% stops, and we are left with
% \begin{syntax}
%   |\add:ww| 12345 \cs{@@_sep:} 333444 \cs{@@_sep:}
% \end{syntax}
% which can safely perform the addition by grabbing two arguments
% delimited by~\cs{@@_sep:}.
%
% If we were to continue parsing the expression, then the following
% number should also be cleaned up before the next use of a binary
% operation such as |\add:ww|.  Just like \cs{int_value:w} |12345|
% \cs{exp_after:wN}~\cs{@@_sep:} expanded what follows once, we need |\add:ww|
% to do the calculation, and in the process to expand the following
% once.  This is also true in our real application: all the functions of
% the form \cs[no-index]{@@_\ldots_o:ww} expand what follows once.  This comes at the
% cost of leaving tokens in the input stack, and we need to be
% careful not to waste this memory.  All of our discussion above is nice
% but simplistic, as operations should not simply be performed in the
% order they appear.
%
% \subsubsection{Precedence and infix operators}
%
% The various operators we will encounter have different precedences,
% which influence the order of calculations: $1+2\times 3 = 1+(2\times
% 3)$ because $\times$~has a higher precedence than~$+$.  The true
% analog of our macro |\operand:w| must thus take care of that.  When
% looking for an operand, it needs to perform calculations until
% reaching an operator which has lower precedence than the one which
% called |\operand:w|.  This means that |\operand:w| must know what the
% previous binary operator is, or rather, its precedence: we thus rename
% it |\operand:Nw|.  Let us describe as an example how we plan to do
% the calculation |41-2^3*4+5|.  More precisely we describe how to
% perform the first operation in this expression.  Here, we abuse
% notations: the first argument of |\operand:Nw| should be an integer
% constant (\cs{c_@@_prec_plus_int}, \ldots{}) equal to the precedence
% of the given operator, not directly the operator itself.
% \begin{itemize}
%   \item Clean up~|41| and find~|-|.  We call |\operand:Nw|~|-| to find
%     the second operand.
%   \item Clean up~|2| and find~|^|.
%   \item Compare the precedences of |-| and~|^|.  Since the latter is
%     higher, we need to compute the exponentiation.  For this, find the
%     second operand with a nested call to |\operand:Nw|~|^|.
%   \item Clean up~|3| and find~|*|.
%   \item Compare the precedences of |^| and~|*|.  Since the former is
%     higher, |\operand:Nw|~|^| has found the second operand of the
%     exponentiation, which is computed: $2^{3} = 8$.
%   \item We now have |41-8*4+5|, and |\operand:Nw|~|-| is still
%     looking for a second operand for the subtraction.  Is it~$8$?
%   \item Compare the precedences of |-| and~|*|.  Since the latter is
%     higher, we are not done with~$8$.  Call |\operand:Nw|~|*| to find
%     the second operand of the multiplication.
%   \item Clean up~|4|, and find~|+|.
%   \item Compare the precedences of |*| and~|+|.  Since the former is
%     higher, |\operand:Nw|~|*| has found the second operand of the
%     multiplication, which is computed: $8*4 = 32$.
%   \item We now have |41-32+5|, and |\operand:Nw|~|-| is still looking
%     for a second operand for the subtraction.  Is it~$32$?
%   \item Compare the precedences of |-| and~|+|.  Since they are equal,
%     |\operand:Nw|~|-| has found the second operand for the
%     subtraction, which is computed: $41-32=9$.
%   \item We now have |9+5|.
% \end{itemize}
% The procedure above stops short of performing all computations, but
% adding a surrounding call to |\operand:Nw| with a very low precedence
% ensures that all computations are performed before |\operand:Nw|
% is done.  Adding a trailing marker with the same very low precedence
% prevents the surrounding |\operand:Nw| from going beyond the marker.
%
% The pattern above to find an operand for a given operator, is to find
% one number and the next operator, then compare precedences to know if
% the next computation should be done.  If it should, then perform it
% after finding its second operand, and look at the next operator, then
% compare precedences to know if the next computation should be done.
% This continues until we find that the next computation should not be
% done.  Then, we stop.
%
% We are now ready to get a bit more technical and describe which of the
% \pkg{l3fp-parse} functions correspond to each step above.
%
% First, \cs{@@_parse_operand:Nw} is the |\operand:Nw| function above,
% with small modifications due to expansion issues discussed later.  We
% denote by \meta{precedence} the argument of \cs{@@_parse_operand:Nw},
% that is, the precedence of the binary operator whose operand we are
% trying to find.  The basic action is to read numbers from the input
% stream.  This is done by \cs{@@_parse_one:Nw}.  A first approximation
% of this function is that it reads one \meta{number}, performing no
% computation, and finds the following binary \meta{operator}.  Then it
% expands to
% \begin{quote}
%   \meta{number}\\
%   |  \__fp_parse_infix_|\meta{operator}|:N| \meta{precedence}
% \end{quote}
% expanding the \texttt{infix} auxiliary before leaving the above in the
% input stream.
%
% We now explain the \texttt{infix} auxiliaries.  We need some
% flexibility in how we treat the case of equal precedences: most often,
% the first operation encountered should be performed, such as |1-2-3|
% being computed as |(1-2)-3|, but |2^3^4| should be evaluated as
% |2^(3^4)| instead.  For this reason, and to support the equivalence
% between |**| and~|^| more easily, each binary operator is converted to
% a control sequence |\__fp_parse_infix_|\meta{operator}|:N| when it is
% encountered for the first time.  Instead of passing both precedences
% to a test function to do the comparison steps above, we pass the
% \meta{precedence} (of the earlier operator) to the \texttt{infix}
% auxiliary for the following \meta{operator}, to know whether to
% perform the computation of the \meta{operator}.  If it should not be
% performed, the \texttt{infix} auxiliary expands to
% \begin{syntax}
%   |@| \cs{use_none:n} |\__fp_parse_infix_|\meta{operator}|:N|
% \end{syntax}
% and otherwise it calls \cs{@@_parse_operand:Nw} with the precedence of
% the \meta{operator} to find its second operand \meta{number_2} and the
% next \meta{operator_2}, and expands to
% \begin{syntax}
%   |@| \cs{@@_parse_apply_binary:NwNwN}
%   ~~~~\meta{operator} \meta{number_2}
%   |@| |\__fp_parse_infix_|\meta{operator_2}|:N|
% \end{syntax}
% The \texttt{infix} function is responsible for comparing precedences,
% but cannot directly call the computation functions, because the first
% operand \meta{number} is before the \texttt{infix} function in the
% input stream.  This is why we stop the expansion here and give control
% to another function to close the loop.
%
% A definition of \cs{@@_parse_operand:Nw} \meta{precedence} with some
% of the expansion control removed is
% \begin{syntax}
%   \cs{exp_after:wN} \cs{@@_parse_continue:NwN}
%   \cs{exp_after:wN} \meta{precedence}
%   \cs{exp:w} \cs{exp_end_continue_f:w}
%   ~~\cs{@@_parse_one:Nw} \meta{precedence}
% \end{syntax}
% This expands \cs{@@_parse_one:Nw} \meta{precedence} completely, which
% finds a number, wraps the next \meta{operator} into an \texttt{infix}
% function, feeds this function the \meta{precedence}, and expands it,
% yielding either
% \begin{syntax}
%   \cs{@@_parse_continue:NwN} \meta{precedence}
%   \meta{number} |@|
%   \cs{use_none:n} |\__fp_parse_infix_|\meta{operator}|:N|
% \end{syntax}
% or
% \begin{syntax}
%   \cs{@@_parse_continue:NwN} \meta{precedence}
%   \meta{number} |@|
%   \cs{@@_parse_apply_binary:NwNwN}
%   ~~\meta{operator} \meta{number_2}
%   |@| |\__fp_parse_infix_|\meta{operator_2}|:N|
% \end{syntax}
% The definition of \cs{@@_parse_continue:NwN} is then very simple:
% \begin{syntax}
%   |\cs_new:Npn \__fp_parse_continue:NwN #1#2@#3 { #3 #1 #2 @ }|
% \end{syntax}
% In the first case, |#3|~is \cs{use_none:n}, yielding
% \begin{syntax}
%   \cs{use_none:n} \meta{precedence} \meta{number} |@|
%   |\__fp_parse_infix_|\meta{operator}|:N|
% \end{syntax}
% then \meta{number} |@| |\__fp_parse_infix_|\meta{operator}|:N|.  In
% the second case, |#3|~is \cs{@@_parse_apply_binary:NwNwN}, whose role
% is to compute \meta{number} \meta{operator} \meta{number_2} and to
% prepare for the next comparison of precedences: first we get
% \begin{syntax}
%   \cs{@@_parse_apply_binary:NwNwN}
%   ~~\meta{precedence} \meta{number} |@|
%   ~~\meta{operator} \meta{number_2}
%   |@| |\__fp_parse_infix_|\meta{operator_2}|:N|
% \end{syntax}
% then
% \begin{syntax}
%   \cs{exp_after:wN} \cs{@@_parse_continue:NwN}
%   \cs{exp_after:wN} \meta{precedence}
%   \cs{exp:w} \cs{exp_end_continue_f:w}
%   |\__fp_|\meta{operator}|_o:ww| \meta{number} \meta{number_2}
%   \cs{exp:w} \cs{exp_end_continue_f:w}
%   |\__fp_parse_infix_|\meta{operator_2}|:N| \meta{precedence}
% \end{syntax}
% where |\__fp_|\meta{operator}|_o:ww| computes \meta{number}
% \meta{operator} \meta{number_2} and expands after the result, thus
% triggers the comparison of the precedence of the \meta{operator_2} and
% the \meta{precedence}, continuing the loop.
%
% We have introduced the most important functions here, and the next few
% paragraphs we describe various subtleties.
%
% \subsubsection{Prefix operators, parentheses, and functions}
%
% Prefix operators (unary |-|, |+|,~|!|) and parentheses are taken care
% of by the same mechanism, and functions (\texttt{sin}, \texttt{exp},
% etc.) as well.  Finding the argument of the unary~|-|, for instance,
% is very similar to grabbing the second operand of a binary infix
% operator, with a subtle precedence explained below.  Once that operand
% is found, the operator can be applied to it (for the unary~|-|, this
% simply flips the sign).  A left parenthesis is just a prefix operator
% with a very low precedence equal to that of the closing parenthesis
% (which is treated as an infix operator, since it normally appears just
% after numbers), so that all computations are performed until the
% closing parenthesis.  The prefix operator associated to the left
% parenthesis does not alter its argument, but it removes the closing
% parenthesis (with some checks).
%
% Prefix operators are the reason why we only summarily described the
% function \cs{@@_parse_one:Nw} earlier.  This function is responsible
% for reading in the input stream the first possible \meta{number} and
% the next infix \meta{operator}.  If what follows \cs{@@_parse_one:Nw}
% \meta{precedence} is a prefix operator, then we must find the operand
% of this prefix operator through a nested call to
% \cs{@@_parse_operand:Nw} with the appropriate precedence, then apply
% the operator to the operand found to yield the result of
% \cs{@@_parse_one:Nw}.  So far, all is simple.
%
% The unary operators |+|, |-|,~|!| complicate things a little bit:
% |-3**2| should be $-(3^2)=-9$, and not $(-3)^2=9$.  This would easily
% be done by giving~|-| a lower precedence, equal to that of the infix
% |+| and~|-|.  Unfortunately, this fails in cases such as |3**-2*4|,
% yielding $3^{-2\times 4}$ instead of the correct $3^{-2}\times 4$.  A
% second attempt would be to call \cs{@@_parse_operand:Nw} with the
% \meta{precedence} of the previous operator, but |0>-2+3| is then
% parsed as |0>-(2+3)|: the addition is performed because it binds more
% tightly than the comparison which precedes~|-|.  The correct approach
% is for a unary~|-| to perform operations whose precedence is greater
% than both that of the previous operation, and that of the unary~|-|
% itself.  The unary~|-| is given a precedence higher than
% multiplication and division.  This does not lead to any surprising
% result, since $-(x/y) = (-x)/y$ and similarly for multiplication, and
% it reduces the number of nested calls to \cs{@@_parse_operand:Nw}.
%
% Functions are implemented as prefix operators with very high
% precedence, so that their argument is the first number that can
% possibly be built.
%
% Note that contrarily to the \texttt{infix} functions discussed
% earlier, the \texttt{prefix} functions do perform tests on the
% previous \meta{precedence} to decide whether to find an argument or
% not, since we know that we need a number, and must never stop there.
%
% \subsubsection{Numbers and reading tokens one by one}
%
% So far, we have glossed over one important point: what is a
% \enquote{number}?  A number is typically given in the form
% \meta{significand}|e|\meta{exponent}, where the \meta{significand} is
% any non-empty string composed of decimal digits and at most one
% decimal separator (a period), the exponent
% \enquote{\texttt{e}\meta{exponent}} is optional and is composed of an
% exponent mark~|e| followed by a possibly empty string of signs
% |+| or~|-| and a non-empty string of decimal digits.  The
% \meta{significand} can also be an integer, dimension, skip, or muskip
% variable, in which case dimensions are converted from points (or mu
% units) to floating points, and the \meta{exponent} can also be an
% integer variable.  Numbers can also be given as floating point
% variables, or as named constants such as |nan|, |inf| or~|pi|.  We may
% add more types in the future.
%
% When \cs{@@_parse_one:Nw} is looking for a \enquote{number}, here is
% what happens.
% \begin{itemize}
%   \item If the next token is a control sequence with the meaning of
%     \cs{scan_stop:}, it can be: \cs{s_@@}, in which case our job is
%     done, as what follows is an internal floating point number, or
%     \cs{s_@@_expr_mark}, in which case the expression has come to an early
%     end, as we are still looking for a number here, or something else,
%     in which case we consider the control sequence to be a bad
%     variable resulting from \texttt{c}-expansion.
%   \item If the next token is a control sequence with a different
%     meaning, we assume that it is a register, unpack it with
%     \cs{tex_the:D}, and use its value (in \texttt{pt} for dimensions
%     and skips, \texttt{mu} for muskips) as the \meta{significand} of a
%     number: we look for an exponent.
%   \item If the next token is a digit, we remove any leading zeros,
%     then read a significand larger than~$1$ if the next character is a
%     digit, read a significand smaller than~$1$ if the next character
%     is a period, or we have found a significand equal to~$0$
%     otherwise, and look for an exponent.
%   \item If the next token is a letter, we collect more letters until
%     the first non-letter: the resulting word may denote a function
%     such as |asin|, a constant such as |pi| or be unknown.  In the
%     first case, we call \cs{@@_parse_operand:Nw} to find the argument
%     of the function, then apply the function, before declaring that we
%     are done.  Otherwise, we are done, either with the value of the
%     constant, or with the value |nan| for unknown words.
%   \item If the next token is anything else, we check whether it is a
%     known prefix operator, in which case \cs{@@_parse_operand:Nw}
%     finds its operand.  If it is not known, then either a number is
%     missing (if the token is a known infix operator) or the token is
%     simply invalid in floating point expressions.
% \end{itemize}
% Once a number is found, \cs{@@_parse_one:Nw} also finds an infix
% operator.  This goes as follows.
% \begin{itemize}
%   \item If the next token is a control sequence, it could be the
%     special marker \cs{s_@@_expr_mark}, and
%     otherwise it is a case of juxtaposing numbers, such as
%     |2\c_zero_int|, with an implied multiplication.
%   \item If the next token is a letter, it is also a case of
%     juxtaposition, as letters cannot be proper infix operators.
%   \item Otherwise (including in the case of digits), if the token is a
%     known infix operator, the appropriate
%     |\__fp_infix_|\meta{operator}|:N| function is built, and if it
%     does not exist, we complain.  In particular, the juxtaposition
%     |\c_zero_int 2| is disallowed.
% \end{itemize}
%
% In the above, we need to test whether a character token~|#1| is a
% digit:
% \begin{verbatim}
% \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
%   is a digit
% \else:
%   not a digit
% \fi:
% \end{verbatim}
% To exclude |0|, replace |9| by |10|. The use of
% \cs{token_to_str:N} ensures that a digit with any catcode is detected.
% To test if a character token is a letter, we need to work with its
% character code, testing if |`#1| lies in $[65,90]$ (uppercase letters)
% or $[97,112]$ (lowercase letters)
% \begin{verbatim}
% \if_int_compare:w \__fp_int_eval:w
%     ( `#1 \if_int_compare:w `#1 > `Z - 32 \fi: ) / 26 = 3 \exp_stop_f:
%   is a letter
% \else:
%   not a letter
% \fi:
% \end{verbatim}
% At all steps, we try to accept all category codes: when |#1|~is kept
% to be used later, it is almost always converted to category code other
% through \cs{token_to_str:N}.  More precisely, catcodes $\{3, 6, 7, 8,
% 11, 12\}$ should work without trouble, but not $\{1, 2, 4, 10, 13\}$,
% and of course $\{0, 5, 9\}$ cannot become tokens.
%
% Floating point expressions should behave as much as possible like
% \eTeX{}-based integer expressions and dimension expressions.  In
% particular, \texttt{f}-expansion should be performed as the expression
% is read, token by token, forcing the expansion of protected macros,
% and ignoring spaces.  One advantage of expanding at every step is that
% restricted expandable functions can then be used in floating point
% expressions just as they can be in other kinds of expressions.
% Problematically, spaces stop \texttt{f}-expansion: for instance, the
% macro~|\X| below would not be expanded if we simply performed
% \texttt{f}-expansion.
% \begin{verbatim}
%   \DeclareDocumentCommand {\test} {m} { \fp_eval:n {#1} }
%   \ExplSyntaxOff
%   \test { 1 + \X }
% \end{verbatim}
% Of course, spaces typically do not appear in a code setting, but may very
% easily come in document-level input, from which some expressions may
% come.  To avoid this problem, at every step, we do essentially what
% \cs{use:f} would do: take an argument, put it back in the input
% stream, then \texttt{f}-expand it.  This is not a complete solution,
% since a macro's expansion could contain leading spaces which would stop
% the \texttt{f}-expansion before further macro calls are performed.
% However, in practice it should be enough: in particular, floating
% point numbers are correctly expanded to the underlying \cs{s_@@}
% \ldots{} structure.  The \texttt{f}-expansion is performed by
% \cs{@@_parse_expand:w}.
%
% ^^A begin[todo]
%
% \subsection{Main auxiliary functions}
%
% \begin{macro}[rEXP]{\@@_parse_operand:Nw}
%   \begin{syntax}
%     \cs{exp:w} \cs{@@_parse_operand:Nw} \meta{precedence} \cs{@@_parse_expand:w}
%   \end{syntax}
%   Reads the \enquote{\ttfamily\ldots{}}, performing every computation
%   with a precedence higher than \meta{precedence}, then expands to
%   \begin{syntax}
%     \meta{result} |@| |\__fp_parse_infix_|\meta{operation}|:N| \ldots{}
%   \end{syntax}
%   where the \meta{operation} is the first operation with a lower
%   precedence, possibly \texttt{end}, and the
%   \enquote{\ttfamily\ldots{}} start just after the \meta{operation}.
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_infix_+:N}
%   \begin{syntax}
%     \cs{@@_parse_infix_+:N} \meta{precedence} \ldots{}
%   \end{syntax}
%   If |+|~has a precedence higher than the \meta{precedence}, cleans up
%   a second \meta{operand} and finds the \meta{operation_2} which
%   follows, and expands to
%   \begin{syntax}
%     |@| \cs{@@_parse_apply_binary:NwNwN} |+| \meta{operand} |@| \cs{@@_parse_infix_\meta{operation_2}:N} \ldots{}
%   \end{syntax}
%   Otherwise expands to
%   \begin{syntax}
%     |@| \cs{use_none:n} \cs{@@_parse_infix_+:N} \ldots{}
%   \end{syntax}
%   A similar function exists for each infix operator.
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_one:Nw}
%   \begin{syntax}
%     \cs{@@_parse_one:Nw} \meta{precedence} \ldots{}
%   \end{syntax}
%   Cleans up one or two operands depending on how the precedence of the
%   next operation compares to the \meta{precedence}.  If the following
%   \meta{operation} has a precedence higher than \meta{precedence},
%   expands to
%   \begin{syntax}
%     \meta{operand_1} |@| \cs{@@_parse_apply_binary:NwNwN} \meta{operation} \meta{operand_2} |@| |\__fp_parse_infix_|\meta{operation_2}|:N| \ldots{}
%   \end{syntax}
%   and otherwise expands to
%   \begin{syntax}
%     \meta{operand} |@| \cs{use_none:n} |\__fp_parse_infix_|\meta{operation}|:N| \ldots{}
%   \end{syntax}
% \end{macro}
%
% ^^A end[todo]
%
% \subsection{Helpers}
%
% \begin{macro}[rEXP]{\@@_parse_expand:w}
%   \begin{syntax}
%     \cs{exp:w} \cs{@@_parse_expand:w} \meta{tokens}
%   \end{syntax}
%   This function must always come within a \cs{exp:w} expansion.
%   The \meta{tokens} should be the part of the expression that we have
%   not yet read.  This requires in particular closing all conditionals
%   properly before expanding.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_expand:w #1 { \exp_end_continue_f:w #1 }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_return_sep:w}
%   This very odd function swaps its position with the following
%   \cs{fi:} and removes \cs{@@_parse_expand:w} normally responsible for
%   expansion.  That turns out to be useful.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_return_sep:w
    #1 \fi: \@@_parse_expand:w { \fi: \@@_sep: #1 }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]
%   {
%     \@@_parse_digits_vii:N  ,
%     \@@_parse_digits_vi:N   ,
%     \@@_parse_digits_v:N    ,
%     \@@_parse_digits_iv:N   ,
%     \@@_parse_digits_iii:N  ,
%     \@@_parse_digits_ii:N   ,
%     \@@_parse_digits_i:N    ,
%     \@@_parse_digits_:N
%   }
%   These functions must be called within an \cs{int_value:w} or
%   \cs{@@_int_eval:w} construction.  The first token which follows must
%   be \texttt{f}-expanded prior to calling those functions.  The
%   functions read tokens one by one, and output digits into the input
%   stream, until meeting a non-digit, or up to a number of digits equal
%   to their index.  The full expansion is
%   \begin{syntax}
%     \meta{digits} \cs{@@_sep:} \meta{filling 0} \cs{@@_sep:} \meta{length}
%   \end{syntax}
%   where \meta{filling 0} is a string of zeros such that \meta{digits}
%   \meta{filling 0} has the length given by the index of the function,
%   and \meta{length} is the number of zeros in the \meta{filling 0}
%   string.  Each function puts a digit into the input stream and calls
%   the next function, until we find a non-digit.  We are careful to
%   pass the tested tokens through \cs{token_to_str:N} to normalize
%   their category code.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1 #2 #3
  {
    \cs_new:cpn { @@_parse_digits_ #1 :N } ##1
      {
        \if_int_compare:w 9 < 1 \token_to_str:N ##1 \exp_stop_f:
          \token_to_str:N ##1 \exp_after:wN #2 \exp:w
        \else:
          \@@_parse_return_sep:w #3 ##1
        \fi:
        \@@_parse_expand:w
      }
  }
\@@_tmp:w {vii}  \@@_parse_digits_vi:N   { 0000000 \@@_sep: 7 }
\@@_tmp:w {vi}   \@@_parse_digits_v:N    { 000000 \@@_sep: 6 }
\@@_tmp:w {v}    \@@_parse_digits_iv:N   { 00000 \@@_sep: 5 }
\@@_tmp:w {iv}   \@@_parse_digits_iii:N  { 0000 \@@_sep: 4 }
\@@_tmp:w {iii}  \@@_parse_digits_ii:N   { 000 \@@_sep: 3 }
\@@_tmp:w {ii}   \@@_parse_digits_i:N    { 00 \@@_sep: 2 }
\@@_tmp:w {i}    \@@_parse_digits_:N     { 0 \@@_sep: 1 }
\cs_new:Npn \@@_parse_digits_:N { \@@_sep: \@@_sep: 0 }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Parsing one number}
%
% \begin{macro}[EXP]{\@@_parse_one:Nw}
%   This function finds one number, and packs the symbol which follows
%   in an \cs[no-index]{@@_parse_infix_\ldots{}} csname.
%   |#1|~is the previous \meta{precedence},
%   and |#2|~the first token of the operand.  We distinguish four cases:
%   |#2|~is equal to \cs{scan_stop:} in meaning, |#2|~is a different
%   control sequence, |#2|~is a digit, and |#2|~is something else (this
%   last case is split further later).  Despite the earlier
%   \texttt{f}-expansion, |#2|~may still be expandable if it was
%   protected by \cs{exp_not:N}, as may happen with the \LaTeXe{} command
%   \tn{protect}.  Using a well placed \cs{reverse_if:N}, this case is
%   sent to \cs{@@_parse_one_fp:NN} which deals with it robustly.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one:Nw #1 #2
  {
    \if_catcode:w \scan_stop: \exp_not:N #2
      \exp_after:wN \if_meaning:w \exp_not:N #2 #2 \else:
        \exp_after:wN \reverse_if:N
      \fi:
      \if_meaning:w \scan_stop: #2
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_one_fp:NN
      \else:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_one_register:NN
      \fi:
    \else:
      \if_int_compare:w 9 < 1 \token_to_str:N #2 \exp_stop_f:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_one_digit:NN
      \else:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_one_other:NN
      \fi:
    \fi:
    #1 #2
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_one_fp:NN,
%     \@@_exp_after_expr_mark_f:nw,
%     \@@_exp_after_?_f:nw
%   }
%   This function receives a \meta{precedence} and a control sequence
%   equal to \cs{scan_stop:} in meaning.  There are three cases.
%   \begin{itemize}
%     \item \cs{s_@@} starts a floating point number, and we call
%       \cs{@@_exp_after_f:nw}, which |f|-expands after the floating
%       point.
%     \item \cs{s_@@_expr_mark} is a premature end, we call
%       \cs{@@_exp_after_expr_mark_f:nw}, which triggers an |fp-early-end|
%       error.
%     \item For a control sequence not containing \cs[no-index]{s_@@}, we call
%       \cs{@@_exp_after_?_f:nw}, causing a |bad-variable| error.
%   \end{itemize}
%   This scheme is extensible: additional types can be added by starting
%   the variables with a scan mark of the form \cs[no-index]{s_@@_\meta{type}} and
%   defining |\__fp_exp_after_|\meta{type}|_f:nw|.  In all cases, we
%   make sure that the second argument of \cs{@@_parse_infix:NN} is
%   correctly expanded.
%   A special case only enabled in \LaTeXe{} is that if \tn{protect} is
%   encountered then the error message mentions the control sequence
%   which follows it rather than \tn{protect} itself.  The test for
%   \LaTeXe{} uses \tn{@unexpandable@protect} rather than \tn{protect}
%   because \tn{protect} is often \cs{scan_stop:} hence \enquote{does
%   not exist}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one_fp:NN #1
  {
    \@@_exp_after_any_f:nw
      {
        \exp_after:wN \@@_parse_infix:NN
        \exp_after:wN #1 \exp:w \@@_parse_expand:w
      }
  }
\cs_new:Npn \@@_exp_after_expr_mark_f:nw #1
  {
    \int_case:nnF { \exp_after:wN \use_i:nnn \use_none:nnn #1 }
      {
        \c_@@_prec_comma_int { }
        \c_@@_prec_tuple_int { }
        \c_@@_prec_end_int
          {
            \exp_after:wN \c_@@_empty_tuple_fp
            \exp:w \exp_end_continue_f:w
          }
      }
      {
        \msg_expandable_error:nn { fp } { early-end }
        \exp_after:wN \c_nan_fp \exp:w \exp_end_continue_f:w
      }
    #1
  }
\cs_new:cpn { @@_exp_after_?_f:nw } #1#2
  {
    \msg_expandable_error:nnn { kernel } { bad-variable }
      {#2}
    \exp_after:wN \c_nan_fp \exp:w \exp_end_continue_f:w #1
  }
\cs_set_protected:Npn \@@_tmp:w #1
  {
    \cs_if_exist:NT #1
      {
        \cs_gset:cpn { @@_exp_after_?_f:nw } ##1##2
          {
            \exp_after:wN \c_nan_fp \exp:w \exp_end_continue_f:w ##1
            \str_if_eq:nnTF {##2} { \protect }
              {
                \cs_if_eq:NNTF ##2 #1 { \use_i:nn } { \use:n }
                {
                  \msg_expandable_error:nnn { fp }
                    { robust-cmd }
                }
              }
              {
                \msg_expandable_error:nnn { kernel }
                  { bad-variable } {##2}
              }
          }
      }
  }
\exp_args:Nc \@@_tmp:w { @unexpandable@protect }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_one_register:NN,
%     \@@_parse_one_register_aux:Nw,
%     \@@_parse_one_register_auxii:wwwNw,
%     \@@_parse_one_register_int:www,
%     \@@_parse_one_register_mu:www,
%     \@@_parse_one_register_dim:ww,
%   }
%   This is called whenever~|#2| is a control sequence other than
%   \cs{scan_stop:} in meaning.  We special-case \tn{wd}, \tn{ht}, \tn{dp}
%   (see later) and otherwise assume that it is a register, but
%   carefully unpack it with \cs{tex_the:D} within braces.  First, we
%   find the exponent following~|#2|.  Then we unpack~|#2| with
%   \cs{tex_the:D}, and the \texttt{auxii} auxiliary distinguishes
%   integer registers from dimensions/skips from muskips, according to
%   the presence of a period and/or of |pt|.  For integers, simply
%   convert \meta{value}|e|\meta{exponent} to a floating point number
%   with \cs{@@_parse:n} (this is somewhat wasteful).  For other
%   registers, the decimal rounding provided by \TeX{} does not
%   accurately represent the binary value that it manipulates, so we
%   extract this binary value as a number of scaled points with
%   \cs{int_value:w} \cs{dim_to_decimal_in_sp:n} |{| \meta{decimal value} |pt| |}|, and
%   use an auxiliary of \cs{dim_to_fp:n}, which performs the
%   multiplication by $2^{-16}$, correctly rounded.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one_register:NN #1#2
  {
    \exp_after:wN \@@_parse_infix_after_operand:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
      \@@_parse_one_register_special:N #2
      \exp_after:wN \@@_parse_one_register_aux:Nw
      \exp_after:wN #2
      \int_value:w
        \exp_after:wN \@@_parse_exponent:N
        \exp:w \@@_parse_expand:w
  }
\cs_new:Npe \@@_parse_one_register_aux:Nw #1
  {
    \exp_not:n
      {
        \exp_after:wN \use:nn
        \exp_after:wN \@@_parse_one_register_auxii:wwwNw
      }
    \exp_not:N \exp_after:wN { \exp_not:N \tex_the:D #1 }
      \@@_sep: \exp_not:N \@@_parse_one_register_dim:ww
      \tl_to_str:n { pt } \@@_sep: \exp_not:N \@@_parse_one_register_mu:www
      . \tl_to_str:n { pt } \@@_sep: \exp_not:N \@@_parse_one_register_int:www
      \s_@@_stop
  }
\exp_args:Nno \use:nn
  { \cs_new:Npn \@@_parse_one_register_auxii:wwwNw #1 . #2 }
    { \tl_to_str:n { pt } #3 \@@_sep: #4#5 \s_@@_stop }
    { #4 #1.#2\@@_sep: }
\exp_args:Nno \use:nn
  { \cs_new:Npn \@@_parse_one_register_mu:www #1 }
    { \tl_to_str:n { mu } \@@_sep: #2 \@@_sep: }
    { \@@_parse_one_register_dim:ww #1 \@@_sep: }
\cs_new:Npn \@@_parse_one_register_int:www #1\@@_sep: #2.\@@_sep: #3\@@_sep:
  { \@@_parse:n { #1 e #3 } }
\cs_new:Npn \@@_parse_one_register_dim:ww #1\@@_sep: #2\@@_sep:
  {
    \exp_after:wN \@@_from_dim_test:ww
    \int_value:w #2 \exp_after:wN ,
    \int_value:w \dim_to_decimal_in_sp:n { #1 pt } \@@_sep:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_parse_one_register_special:N,
%     \@@_parse_one_register_math:NNw,
%     \@@_parse_one_register_wd:w,
%     \@@_parse_one_register_wd:Nw
%   }
%   The \tn{wd}, \tn{dp}, \tn{ht} primitives expect an integer argument.
%   We abuse the exponent parser to find the integer argument: simply
%   include the exponent marker~|e|.  Once that \enquote{exponent} is
%   found, use \cs{tex_the:D} to find the box dimension and then copy
%   what we did for dimensions.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one_register_special:N #1
  {
    \if_meaning:w \box_wd:N #1 \@@_parse_one_register_wd:w \fi:
    \if_meaning:w \box_ht:N #1 \@@_parse_one_register_wd:w \fi:
    \if_meaning:w \box_dp:N #1 \@@_parse_one_register_wd:w \fi:
    \if_meaning:w \infty #1
      \@@_parse_one_register_math:NNw \infty #1
    \fi:
    \if_meaning:w \pi #1
      \@@_parse_one_register_math:NNw \pi #1
    \fi:
  }
\cs_new:Npn \@@_parse_one_register_math:NNw
    #1#2#3#4 \@@_parse_expand:w
  {
    #3
    \str_if_eq:nnTF {#1} {#2}
      {
        \msg_expandable_error:nnn
          { fp } { infty-pi } {#1}
        \c_nan_fp
      }
      { #4 \@@_parse_expand:w }
  }
\cs_new:Npn \@@_parse_one_register_wd:w
    #1#2 \exp_after:wN #3#4 \@@_parse_expand:w
  {
    #1
    \exp_after:wN \@@_parse_one_register_wd:Nw
    #4 \@@_parse_expand:w e
  }
\cs_new:Npn \@@_parse_one_register_wd:Nw #1#2 \@@_sep:
  {
    \exp_after:wN \@@_from_dim_test:ww
    \exp_after:wN 0 \exp_after:wN ,
    \int_value:w \dim_to_decimal_in_sp:n { #1 #2 } \@@_sep:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_one_digit:NN}
%   A digit marks the beginning of an explicit floating point number.
%   Once the number is found, we catch the case of overflow and
%   underflow with \cs{@@_sanitize:wN}, then
%   \cs{@@_parse_infix_after_operand:NwN} expands \cs{@@_parse_infix:NN}
%   after the number we find, to wrap the following infix operator as
%   required.  Finding the number itself begins by removing leading
%   zeros: further steps are described later.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one_digit:NN #1
  {
    \exp_after:wN \@@_parse_infix_after_operand:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
      \exp_after:wN \@@_sanitize:wN
      \int_value:w \@@_int_eval:w 0 \@@_parse_trim_zeros:N
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_one_other:NN}
%   For this function, |#2|~is a character token which is not a digit.
%   If it is an \textsc{ascii} letter, \cs{@@_parse_letters:N} beyond this one and give
%   the result to \cs{@@_parse_word:Nw}.  Otherwise, the character is
%   assumed to be a prefix operator, and we build
%   |\__fp_parse_prefix_|\meta{operator}|:Nw|.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_one_other:NN #1 #2
  {
    \if_int_compare:w
        \@@_int_eval:w
          ( `#2 \if_int_compare:w `#2 > `Z - 32 \fi: ) / 26
        = 3 \exp_stop_f:
      \exp_after:wN \@@_parse_word:Nw
      \exp_after:wN #1
      \exp_after:wN #2
      \exp:w \exp_after:wN \@@_parse_letters:N
      \exp:w
    \else:
      \exp_after:wN \@@_parse_prefix:NNN
      \exp_after:wN #1
      \exp_after:wN #2
      \cs:w
        @@_parse_prefix_ \token_to_str:N #2 :Nw
        \exp_after:wN
      \cs_end:
      \exp:w
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_word:Nw}
% \begin{macro}[rEXP]{\@@_parse_letters:N}
%   Finding letters is a simple recursion.  Once \cs{@@_parse_letters:N}
%   has done its job, we try to build a control sequence from the
%   word~|#2|.  If it is a known word, then the corresponding action is
%   taken, and otherwise, we complain about an unknown word, yield
%   \cs{c_nan_fp}, and look for the following infix operator.  Note that
%   the unknown word could be a mistyped function as well as a mistyped
%   constant, so there is no way to tell whether to look for arguments;
%   we do not.
%   The standard requires \enquote{inf} and \enquote{infinity} and
%   \enquote{nan} to be recognized regardless of case, but we probably
%   don't want to allow every \pkg{l3fp} word to have an arbitrary
%   mixture of lower and upper case, so we test and use a
%   differently-named control sequence.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_word:Nw #1#2\@@_sep:
  {
    \cs_if_exist_use:cF { @@_parse_word_#2:N }
      {
        \cs_if_exist_use:cF
          { @@_parse_caseless_ \str_casefold:n {#2} :N }
          {
            \msg_expandable_error:nnn
              { fp } { unknown-fp-word } {#2}
            \exp_after:wN \c_nan_fp \exp:w \exp_end_continue_f:w
            \@@_parse_infix:NN
          }
      }
      #1
  }
\cs_new:Npn \@@_parse_letters:N #1
  {
    \exp_end_continue_f:w
    \if_int_compare:w
        \if_catcode:w \scan_stop: \exp_not:N #1
          0
        \else:
          \@@_int_eval:w
            ( `#1 \if_int_compare:w `#1 > `Z - 32 \fi: ) / 26
        \fi:
        = 3 \exp_stop_f:
      \exp_after:wN #1
      \exp:w \exp_after:wN \@@_parse_letters:N
      \exp:w
    \else:
      \@@_parse_return_sep:w #1
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]
%   {\@@_parse_prefix:NNN, \@@_parse_prefix_unknown:NNN}
%   For this function, |#1|~is the previous \meta{precedence}, |#2|~is
%   the operator just seen, and |#3|~is a control sequence which
%   implements the operator if it is a known operator.  If this control
%   sequence is \cs{scan_stop:}, then the operator is in fact unknown.
%   Either the expression is missing a number there (if the operator is
%   valid as an infix operator), and we put \texttt{nan}, wrapping the
%   infix operator in a csname as appropriate, or the character is
%   simply invalid in floating point expressions, and we continue
%   looking for a number, starting again from \cs{@@_parse_one:Nw}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_prefix:NNN #1#2#3
  {
    \if_meaning:w \scan_stop: #3
      \exp_after:wN \@@_parse_prefix_unknown:NNN
      \exp_after:wN #2
    \fi:
    #3 #1
  }
\cs_new:Npn \@@_parse_prefix_unknown:NNN #1#2#3
  {
    \cs_if_exist:cTF { @@_parse_infix_ \token_to_str:N #1 :N }
      {
        \msg_expandable_error:nnn
          { fp } { missing-number } {#1}
        \exp_after:wN \c_nan_fp \exp:w \exp_end_continue_f:w
        \@@_parse_infix:NN #3 #1
      }
      {
        \msg_expandable_error:nnn
          { fp } { unknown-symbol } {#1}
        \@@_parse_one:Nw #3
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Numbers: trimming leading zeros}
%
% Numbers are parsed as follows: first we trim leading zeros, then
% if the next character is a digit, start reading a significand $\geq 1$
% with the set of functions |\__fp_parse_large|\ldots{}; if it is a
% period, the significand is~$<1$; and otherwise it is zero.  In the
% second case, trim additional zeros after the period, counting them for
% an exponent shift $\meta{exp_1}<0$, then read the significand with the
% set of functions |\__fp_parse_small|\ldots{} Once the significand is
% read, read the exponent if |e|~is present.
%
% \begin{macro}[rEXP]{\@@_parse_trim_zeros:N, \@@_parse_trim_end:w}
%   This function expects an already expanded token.  It removes any
%   leading zero, then distinguishes three cases: if the first non-zero
%   token is a digit, then call \cs{@@_parse_large:N} (the significand
%   is $\geq 1$); if it is |.|, then continue trimming zeros with
%   \cs{@@_parse_strim_zeros:N}; otherwise, our number is exactly zero,
%   and we call \cs{@@_parse_zero:} to take care of that case.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_trim_zeros:N #1
  {
    \if:w 0 \exp_not:N #1
      \exp_after:wN \@@_parse_trim_zeros:N
      \exp:w
    \else:
      \if:w . \exp_not:N #1
        \exp_after:wN \@@_parse_strim_zeros:N
        \exp:w
      \else:
        \@@_parse_trim_end:w #1
      \fi:
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_trim_end:w #1 \fi: \fi: \@@_parse_expand:w
  {
      \fi:
    \fi:
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      \exp_after:wN \@@_parse_large:N
    \else:
      \exp_after:wN \@@_parse_zero:
    \fi:
    #1
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]
%   {\@@_parse_strim_zeros:N, \@@_parse_strim_end:w}
%   If we have removed all digits until a period (or if the body started
%   with a period), then enter the \enquote{\texttt{small_trim}} loop
%   which outputs $-1$ for each removed~$0$.  Those $-1$ are added to an
%   integer expression waiting for the exponent.  If the first non-zero
%   token is a digit, call \cs{@@_parse_small:N} (our significand is
%   smaller than~$1$), and otherwise, the number is an exact zero.  The
%   name \texttt{strim} stands for \enquote{small trim}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_strim_zeros:N #1
  {
    \if:w 0 \exp_not:N #1
      - 1
      \exp_after:wN \@@_parse_strim_zeros:N \exp:w
    \else:
      \@@_parse_strim_end:w #1
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_strim_end:w #1 \fi: \@@_parse_expand:w
  {
    \fi:
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      \exp_after:wN \@@_parse_small:N
    \else:
      \exp_after:wN \@@_parse_zero:
    \fi:
    #1
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_zero:}
%   After reading a significand of~$0$, find any exponent, then put a
%   sign of~|1| for \cs{@@_sanitize:wN}, which removes everything
%   and leaves an exact zero.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_zero:
  {
    \exp_after:wN \@@_sep: \exp_after:wN 1
    \int_value:w \@@_parse_exponent:N
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Number: small significand}
%
% \begin{macro}[rEXP]{\@@_parse_small:N}
%   This function is called after we have passed the decimal separator
%   and removed all leading zeros from the significand.  It is followed
%   by a non-zero digit (with any catcode).  The goal is to read up to
%   $16$ digits.  But we can't do that all at once, because
%   \cs{int_value:w} (which allows us to collect digits and continue
%   expanding) can only go up to $9$ digits.  Hence we grab digits in
%   two steps of $8$ digits.  Since |#1| is a digit, read seven more
%   digits using \cs{@@_parse_digits_vii:N}.  The \texttt{small_leading}
%   auxiliary leaves those digits in the \cs{int_value:w}, and
%   grabs some more, or stops if there are no more digits.  Then the
%   \texttt{pack_leading} auxiliary puts the various parts in the
%   appropriate order for the processing further up.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_small:N #1
  {
    \exp_after:wN \@@_parse_pack_leading:NNNNNww
    \int_value:w \@@_int_eval:w 1 \token_to_str:N #1
      \exp_after:wN \@@_parse_small_leading:wwNN
      \int_value:w 1
        \exp_after:wN \@@_parse_digits_vii:N
        \exp:w \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_small_leading:wwNN}
%   \begin{syntax}
%     \cs{@@_parse_small_leading:wwNN} |1| \meta{digits} \cs{@@_sep:} \meta{zeros} \cs{@@_sep:} \meta{number~of~zeros}
%   \end{syntax}
%   We leave \meta{digits} \meta{zeros} in the input stream: the
%   functions used to grab digits are such that this constitutes digits
%   $1$ through~$8$ of the significand.  Then prepare to pack $8$~more
%   digits, with an exponent shift of zero (this shift is used in
%   the case of a large significand).  If |#4|~is a digit, leave it
%   behind for the packing function, and read $6$~more digits to reach a
%   total of $15$~digits: further digits are involved in the rounding.
%   Otherwise put $8$~zeros in to complete the significand, then look
%   for an exponent.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_small_leading:wwNN 1 #1 \@@_sep: #2\@@_sep: #3 #4
  {
    #1 #2
    \exp_after:wN \@@_parse_pack_trailing:NNNNNNww
    \exp_after:wN 0
    \int_value:w \@@_int_eval:w 1
      \if_int_compare:w 9 < 1 \token_to_str:N #4 \exp_stop_f:
        \token_to_str:N #4
        \exp_after:wN \@@_parse_small_trailing:wwNN
        \int_value:w 1
          \exp_after:wN \@@_parse_digits_vi:N
          \exp:w
      \else:
        0000 0000 \@@_parse_exponent:Nw #4
      \fi:
      \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_small_trailing:wwNN}
%   \begin{syntax}
%     \cs{@@_parse_small_trailing:wwNN} |1| \meta{digits} \cs{@@_sep:} \meta{zeros} \cs{@@_sep:} \meta{number~of~zeros} \meta{next~token}
%   \end{syntax}
%   Leave digits $10$ to~$15$ (arguments |#1| and |#2|) in the input
%   stream.  If the \meta{next~token} is a digit, it is the $16$th
%   digit, we keep it, then the \texttt{small_round} auxiliary considers
%   this digit and all further digits to perform the rounding: the
%   function expands to nothing, to |+0| or to |+1|.
%   Otherwise, there is no $16$-th digit, so we put a~$0$, and look for
%   an exponent.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_small_trailing:wwNN 1 #1 \@@_sep: #2\@@_sep: #3 #4
  {
    #1 #2
    \if_int_compare:w 9 < 1 \token_to_str:N #4 \exp_stop_f:
      \token_to_str:N #4
      \exp_after:wN \@@_parse_small_round:NN
      \exp_after:wN #4
      \exp:w
    \else:
      0 \@@_parse_exponent:Nw #4
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]
%   {
%     \@@_parse_pack_trailing:NNNNNNww ,
%     \@@_parse_pack_leading:NNNNNww   ,
%     \@@_parse_pack_carry:w
%   }
%   Those functions are expanded after all the digits are found, we took
%   care of the rounding, as well as the exponent.  The last argument is
%   the exponent.  The previous five arguments are $8$~digits which we
%   pack in groups of~$4$, and the argument before that is~$1$, except
%   in the rare case where rounding lead to a carry, in which case the
%   argument is~$2$.  The \texttt{trailing} function has an exponent
%   shift as its first argument, which we add to the exponent found in
%   the |e...| syntax.  If the trailing digits cause a carry, the
%   integer expression for the leading digits is incremented (|+1|
%   in the code below).  If the leading digits propagate this carry all
%   the way up, the function \cs{@@_parse_pack_carry:w} increments the
%   exponent, and changes the significand from |0000...| to |1000...|:
%   this is simple because such a carry can only occur to give rise to a
%   power of~$10$.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_pack_trailing:NNNNNNww
    #1 #2 #3#4#5#6 #7\@@_sep: #8 \@@_sep:
  {
    \if_meaning:w 2 #2 + 1 \fi:
    \@@_sep: #8 + #1 \@@_sep: {#3#4#5#6} {#7}\@@_sep:
  }
\cs_new:Npn \@@_parse_pack_leading:NNNNNww #1 #2#3#4#5 #6\@@_sep: #7\@@_sep:
  {
    + #7
    \if_meaning:w 2 #1 \@@_parse_pack_carry:w \fi:
    \@@_sep: 0 {#2#3#4#5} {#6}
  }
\cs_new:Npn \@@_parse_pack_carry:w \fi: \@@_sep: 0 #1
  { \fi: + 1 \@@_sep: 0 {1000} }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Number: large significand}
%
% Parsing a significand larger than~$1$ is a little bit more difficult
% than parsing small significands.  We need to count the number of
% digits before the decimal separator, and add that to the final
% exponent.  We also need to test for the presence of a dot each time we
% run out of digits, and branch to the appropriate \texttt{parse_small}
% function in those cases.
%
% \begin{macro}[EXP]{\@@_parse_large:N}
%   This function is followed by the first non-zero digit of a
%   \enquote{large} significand ($\geq 1$).  It is called within an
%   integer expression for the exponent.  Grab up to $7$~more digits,
%   for a total of $8$~digits.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_large:N #1
  {
    \exp_after:wN \@@_parse_large_leading:wwNN
    \int_value:w 1 \token_to_str:N #1
      \exp_after:wN \@@_parse_digits_vii:N
      \exp:w \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_large_leading:wwNN}
%   \begin{syntax}
%     \cs{@@_parse_large_leading:wwNN} |1| \meta{digits} \cs{@@_sep:} \meta{zeros} \cs{@@_sep:} \meta{number~of~zeros} \meta{next~token}
%   \end{syntax}
%   We shift the exponent by the number of digits in~|#1|, namely the
%   target number, $8$, minus the \meta{number of zeros} (number of
%   digits missing).  Then prepare to pack the $8$~first digits.  If the
%   \meta{next token} is a digit, read up to $6$~more digits (digits
%   $10$ to~$15$).  If it is a period, try to grab the end of our
%   $8$~first digits, branching to the \texttt{small} functions since
%   the number of digit does not affect the exponent anymore.  Finally,
%   if this is the end of the significand, insert the \meta{zeros} to
%   complete the $8$~first digits, insert $8$~more, and look for an
%   exponent.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_large_leading:wwNN 1 #1 \@@_sep: #2\@@_sep: #3 #4
  {
    + \c_@@_half_prec_int - #3
    \exp_after:wN \@@_parse_pack_leading:NNNNNww
    \int_value:w \@@_int_eval:w 1 #1
      \if_int_compare:w 9 < 1 \token_to_str:N #4 \exp_stop_f:
        \exp_after:wN \@@_parse_large_trailing:wwNN
        \int_value:w 1 \token_to_str:N #4
          \exp_after:wN \@@_parse_digits_vi:N
          \exp:w
      \else:
        \if:w . \exp_not:N #4
          \exp_after:wN \@@_parse_small_leading:wwNN
          \int_value:w 1
            \cs:w
              @@_parse_digits_
              \@@_int_to_roman:w #3
              :N \exp_after:wN
            \cs_end:
            \exp:w
        \else:
          #2
          \exp_after:wN \@@_parse_pack_trailing:NNNNNNww
          \exp_after:wN 0
          \int_value:w 1 0000 0000
          \@@_parse_exponent:Nw #4
        \fi:
      \fi:
      \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_large_trailing:wwNN}
%   \begin{syntax}
%     \cs{@@_parse_large_trailing:wwNN} |1| \meta{digits} \cs{@@_sep:} \meta{zeros} \cs{@@_sep:} \meta{number~of~zeros} \meta{next~token}
%   \end{syntax}
%   We have just read $15$~digits.  If the \meta{next token} is a digit,
%   then the exponent shift caused by this block of $8$~digits is~$8$,
%   first argument to the \texttt{pack_trailing} function.  We keep the
%   \meta{digits} and this $16$-th digit, and find how this should be
%   rounded using \cs{@@_parse_large_round:NN}.  Otherwise, the exponent
%   shift is the number of \meta{digits}, $7$~minus the \meta{number of
%     zeros}, and we test for a decimal point.  This case happens in
%   |123451234512345.67| with exactly $15$ digits before the decimal
%   separator.  Then branch to the appropriate \texttt{small} auxiliary,
%   grabbing a few more digits to complement the digits we already
%   grabbed.  Finally, if this is truly the end of the significand, look
%   for an exponent after using the \meta{zeros} and providing a $16$-th
%   digit of~$0$.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_large_trailing:wwNN 1 #1 \@@_sep: #2\@@_sep: #3 #4
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #4 \exp_stop_f:
      \exp_after:wN \@@_parse_pack_trailing:NNNNNNww
      \exp_after:wN \c_@@_half_prec_int
      \int_value:w \@@_int_eval:w 1 #1 \token_to_str:N #4
        \exp_after:wN \@@_parse_large_round:NN
        \exp_after:wN #4
        \exp:w
    \else:
      \exp_after:wN \@@_parse_pack_trailing:NNNNNNww
      \int_value:w \@@_int_eval:w 7 - #3 \exp_stop_f:
      \int_value:w \@@_int_eval:w 1 #1
        \if:w . \exp_not:N #4
          \exp_after:wN \@@_parse_small_trailing:wwNN
          \int_value:w 1
            \cs:w
              @@_parse_digits_
              \@@_int_to_roman:w #3
              :N \exp_after:wN
            \cs_end:
            \exp:w
        \else:
          #2 0 \@@_parse_exponent:Nw #4
        \fi:
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Number: beyond 16 digits, rounding}
%
% \begin{macro}[rEXP]{\@@_parse_round_loop:N, \@@_parse_round_up:N}
%   This loop is called when rounding a number (whether the mantissa is
%   small or large).  It should appear in an integer expression.  This
%   function reads digits one by one, until reaching a non-digit, and
%   adds~$1$ to the integer expression for each digit.  If all digits
%   found are~$0$, the function ends the expression by \cs{@@_sep:}|0|,
%   otherwise by \cs{@@_sep:}|1|.  This is done by switching the loop to
%   |round_up| at the first non-zero digit, thus we avoid to test
%   whether digits are~$0$ or not once we see a first non-zero digit.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_round_loop:N #1
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      + 1
      \if:w 0 \token_to_str:N #1
        \exp_after:wN \@@_parse_round_loop:N
        \exp:w
      \else:
        \exp_after:wN \@@_parse_round_up:N
        \exp:w
      \fi:
    \else:
      \@@_parse_return_sep:w 0 #1
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_round_up:N #1
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      + 1
      \exp_after:wN \@@_parse_round_up:N
      \exp:w
    \else:
      \@@_parse_return_sep:w 1 #1
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_round_after:wN}
%   After the loop \cs{@@_parse_round_loop:N}, this function fetches an
%   exponent with \cs{@@_parse_exponent:N}, and combines it with the
%   number of digits counted by \cs{@@_parse_round_loop:N}.  At the same
%   time, the result |0| or |1| is added to the
%   surrounding integer expression.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_round_after:wN #1\@@_sep: #2
  {
    + #2 \exp_after:wN \@@_sep:
    \int_value:w \@@_int_eval:w #1 + \@@_parse_exponent:N
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]
%   {\@@_parse_small_round:NN, \@@_parse_round_after:wN}
%   Here, |#1|~is the digit that we are currently rounding (we only care
%   whether it is even or odd).  If |#2|~is not a digit, then fetch an
%   exponent and expand to \cs{@@_sep:}\meta{exponent} only.  Otherwise, we
%   expand to |+0| or |+1|, then \cs{@@_sep:}\meta{exponent}.  To
%   decide which, call \cs{@@_round_s:NNNw} to know whether to round up,
%   giving it as arguments a sign~$0$ (all explicit numbers are
%   positive), the digit |#1|~to round, the first following digit~|#2|,
%   and either |+0| or |+1| depending on whether the
%   following digits are all zero or not.  This last argument is
%   obtained by \cs{@@_parse_round_loop:N}, whose number of digits we
%   discard by multiplying it by~$0$.  The exponent which follows the
%   number is also fetched by \cs{@@_parse_round_after:wN}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_small_round:NN #1#2
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #2 \exp_stop_f:
      +
      \exp_after:wN \@@_round_s:NNNw
      \exp_after:wN 0
      \exp_after:wN #1
      \exp_after:wN #2
      \int_value:w \@@_int_eval:w
        \exp_after:wN \@@_parse_round_after:wN
        \int_value:w \@@_int_eval:w 0 * \@@_int_eval:w 0
          \exp_after:wN \@@_parse_round_loop:N
          \exp:w
    \else:
      \@@_parse_exponent:Nw #2
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
%
% \begin{macro}[rEXP]
%   {
%     \@@_parse_large_round:NN,
%     \@@_parse_large_round_test:NN,
%     \@@_parse_large_round_aux:wNN,
%   }
%   Large numbers are harder to round, as there may be a period in the
%   way.  Again, |#1|~is the digit that we are currently rounding (we
%   only care whether it is even or odd).  If there are no more digits
%   (|#2|~is not a digit), then we must test for a period: if there is
%   one, then switch to the rounding function for small significands,
%   otherwise fetch an exponent.  If there are more digits (|#2|~is a
%   digit), then round, checking with \cs{@@_parse_round_loop:N} if all
%   further digits vanish, or some are non-zero.  This loop is not
%   enough, as it is stopped by a period.  After the loop, the
%   \texttt{aux} function tests for a period: if it is present, then we
%   must continue looking for digits, this time discarding the number of
%   digits we find.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_large_round:NN #1#2
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #2 \exp_stop_f:
      +
      \exp_after:wN \@@_round_s:NNNw
      \exp_after:wN 0
      \exp_after:wN #1
      \exp_after:wN #2
      \int_value:w \@@_int_eval:w
        \exp_after:wN \@@_parse_large_round_aux:wNN
        \int_value:w \@@_int_eval:w 1
          \exp_after:wN \@@_parse_round_loop:N
    \else: %^^A could be dot, or e, or other
      \exp_after:wN \@@_parse_large_round_test:NN
      \exp_after:wN #1
      \exp_after:wN #2
    \fi:
  }
\cs_new:Npn \@@_parse_large_round_test:NN #1#2
  {
    \if:w . \exp_not:N #2
      \exp_after:wN \@@_parse_small_round:NN
      \exp_after:wN #1
      \exp:w
    \else:
      \@@_parse_exponent:Nw #2
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_large_round_aux:wNN #1 \@@_sep: #2 #3
  {
    + #2
    \exp_after:wN \@@_parse_round_after:wN
    \int_value:w \@@_int_eval:w #1
      \if:w . \exp_not:N #3
        + 0 * \@@_int_eval:w 0
          \exp_after:wN \@@_parse_round_loop:N
          \exp:w \exp_after:wN \@@_parse_expand:w
      \else:
        \exp_after:wN \@@_sep:
        \exp_after:wN 0
        \exp_after:wN #3
      \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Number: finding the exponent}
%
% Expansion is a little bit tricky here, in part because we accept input
% where multiplication is implicit.
% \begin{syntax}
%   \cs{@@_parse:n} |{ 3.2 erf(0.1) }|
%   \cs{@@_parse:n} |{ 3.2 e\l_my_int }|
%   \cs{@@_parse:n} |{ 3.2 \c_pi_fp }|
% \end{syntax}
% The first case indicates that just looking one character ahead for an
% \enquote{\texttt{e}} is not enough, since we would mistake the
% function \texttt{erf} for an exponent of \enquote{\texttt{rf}}.  An
% alternative would be to look two tokens ahead and check if what
% follows is a sign or a digit, considering in that case that we must be
% finding an exponent.  But taking care of the second case requires that
% we unpack registers after \texttt{e}.  However, blindly expanding the
% two tokens ahead completely would break the third example (unpacking
% is even worse).  Indeed, in the course of reading $3.2$, \cs{c_pi_fp}
% is expanded to \cs{s_@@} \cs{@@_chk:w} |1| |0| |{-1}| |{3141}|
% $\cdots$ \cs{@@_sep:} and \cs{s_@@} stops the expansion.  Expanding two tokens
% ahead would then force the expansion of \cs{@@_chk:w} (despite it
% being protected), and that function tries to produce an error.
%
% What can we do?  Really, the reason why this last case breaks is that
% just as \TeX{} does, we should read ahead as little as possible.
% Here, the only case where there may be an exponent is if the first
% token ahead is |e|.  Then we expand (and possibly unpack) the second
% token.
%
% \begin{macro}[rEXP]{\@@_parse_exponent:Nw}
%   This auxiliary is convenient to smuggle some material through
%   \cs{fi:} ending conditional processing.  We place those \cs{fi:}
%   (argument~|#2|) at a very odd place because this allows us to insert
%   \cs{@@_int_eval:w} \ldots{} there if needed.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_exponent:Nw #1 #2 \@@_parse_expand:w
  {
    \exp_after:wN \@@_sep:
    \int_value:w #2 \@@_parse_exponent:N #1
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]
%   {\@@_parse_exponent:N, \@@_parse_exponent_aux:NN}
%   This function should be called within an \cs{int_value:w}
%   expansion (or within an integer expression).  It leaves digits of the
%   exponent behind it in the input stream, and terminates the expansion
%   with a \cs{@@_sep:}.  If there is no~|e| (or~|E|), leave an exponent of~$0$.  If
%   there is an~|e| or~|E|, expand the next token to run some tests on it.  The
%   first rough test is that if the character code of~|#1| is greater
%   than that of~|9| (largest code valid for an exponent, less than any
%   code valid for an identifier), there was in fact no exponent;
%   otherwise, we search for the sign of the exponent.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_exponent:N #1
  {
    \if:w e \if:w E \exp_not:N #1 e \else: \exp_not:N #1 \fi:
      \exp_after:wN \@@_parse_exponent_aux:NN
      \exp_after:wN #1
      \exp:w
    \else:
      0 \@@_parse_return_sep:w #1
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_exponent_aux:NN #1#2
  {
    \if_int_compare:w \if_catcode:w \scan_stop: \exp_not:N #2
                0 \else: `#2 \fi: > `9 \exp_stop_f:
      0 \exp_after:wN \@@_sep: \exp_after:wN #1
    \else:
      \exp_after:wN \@@_parse_exponent_sign:N
    \fi:
    #2
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_exponent_sign:N}
%   Read signs one by one (if there is any).
%    \begin{macrocode}
\cs_new:Npn \@@_parse_exponent_sign:N #1
  {
    \if:w + \if:w - \exp_not:N #1 + \fi: \token_to_str:N #1
      \exp_after:wN \@@_parse_exponent_sign:N
      \exp:w \exp_after:wN \@@_parse_expand:w
    \else:
      \exp_after:wN \@@_parse_exponent_body:N
      \exp_after:wN #1
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_exponent_body:N}
%   An exponent can be an explicit integer (most common case), or
%   various other things (most of which are invalid).
%    \begin{macrocode}
\cs_new:Npn \@@_parse_exponent_body:N #1
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      \token_to_str:N #1
      \exp_after:wN \@@_parse_exponent_digits:N
      \exp:w
    \else:
      \@@_parse_exponent_keep:NTF #1
        { \@@_parse_return_sep:w #1 }
        {
          \exp_after:wN \@@_sep:
          \exp:w
        }
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_exponent_digits:N}
%   Read digits one by one, and leave them behind in the input stream.
%   When finding a non-digit, stop, and insert a \cs{@@_sep:}.  Note that
%   we do not check for overflow of the exponent, hence there can be a
%   \TeX{} error.  It is mostly harmless, except when parsing
%   |0e9876543210|, which should be a valid representation of $0$, but
%   is not.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_exponent_digits:N #1
  {
    \if_int_compare:w 9 < 1 \token_to_str:N #1 \exp_stop_f:
      \token_to_str:N #1
      \exp_after:wN \@@_parse_exponent_digits:N
      \exp:w
    \else:
      \@@_parse_return_sep:w #1
    \fi:
    \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[rEXP]{\@@_parse_exponent_keep:NTF}
%   This is the last building block for parsing exponents.  The
%   argument~|#1| is already fully expanded, and neither |+| nor~|-| nor
%   a digit.  It can be:
%   \begin{itemize}
%   \item \cs{s_@@}, marking the start of an internal floating point,
%     invalid here;
%   \item another control sequence equal to \tn{relax}, probably a bad
%     variable;
%   \item a register: in this case we make sure that it is an integer
%     register, not a dimension;
%   \item a character other than |+|, |-| or digits, again, an error.
%   \end{itemize}
%    \begin{macrocode}
\prg_new_conditional:Npnn \@@_parse_exponent_keep:N #1 { TF }
  {
    \if_catcode:w \scan_stop: \exp_not:N #1
      \if_meaning:w \scan_stop: #1
        \if:w 0 \@@_str_if_eq:nn { \s_@@ } { \exp_not:N #1 }
          0
          \msg_expandable_error:nnn
            { fp } { after-e } { floating~point~ }
          \prg_return_true:
        \else:
          0
          \msg_expandable_error:nnn
            { kernel } { bad-variable } {#1}
          \prg_return_false:
        \fi:
      \else:
        \if:w 0 \@@_str_if_eq:nn { \int_value:w #1 } { \tex_the:D #1 }
          \int_value:w #1
        \else:
          0
          \msg_expandable_error:nnn
            { fp } { after-e } { dimension~#1 }
        \fi:
        \prg_return_false:
      \fi:
    \else:
      0
      \msg_expandable_error:nnn
        { fp } { missing } { exponent }
      \prg_return_true:
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Constants, functions and prefix operators}
%
% \subsubsection{Prefix operators}
%
% \begin{macro}[EXP]{\@@_parse_prefix_+:Nw}
%   A unary~|+| does nothing: we should continue looking for a number.
%    \begin{macrocode}
\cs_new_eq:cN { @@_parse_prefix_+:Nw } \@@_parse_one:Nw
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_apply_function:NNNwN}
%   Here, |#1| is a precedence, |#2| is some extra data used by some
%   functions, |#3| is \emph{e.g.}, \cs{@@_sin_o:w}, and expands once
%   after the calculation, |#4| is the operand, and |#5| is a
%   \cs[no-index]{@@_parse_infix_\ldots{}:N} function.  We feed the data~|#2|, and the
%   argument~|#4|, to the function~|#3|, which expands
%   \cs{exp:w} thus the \texttt{infix} function~|#5|.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_apply_function:NNNwN #1#2#3#4@#5
  {
    #3 #2 #4 @
    \exp:w \exp_end_continue_f:w #5 #1
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_apply_unary:NNNwN}
% \begin{macro}[EXP]{\@@_parse_apply_unary_chk:NwNw, \@@_parse_apply_unary_chk:nNNNw}
% \begin{macro}[EXP]{\@@_parse_apply_unary_type:NNN, \@@_parse_apply_unary_error:NNw}
%   In contrast to \cs{@@_parse_apply_function:NNNwN}, this checks that
%   the operand |#4| is a single argument (namely there is a single
%   \cs{@@_sep:}).  We use the fact that any floating point starts with a
%   \enquote{safe} token like \cs{s_@@}.  If there is no argument
%   produce the |fp-no-arg| error; if there are at least two produce
%   |fp-multi-arg|.  For the error message extract the mathematical
%   function name (such as |sin|) from the \pkg{expl3} function that
%   computes it, such as \cs{@@_sin_o:w}.
%
%   In addition, since there is a single argument we can dispatch on
%   type and check that the resulting function exists.  This catches
%   things like |sin((1,2))| where it does not make sense to take the
%   sine of a tuple.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_apply_unary:NNNwN #1#2#3#4@#5
  {
    \@@_parse_apply_unary_chk:NwNw #4 @ \@@_sep: . \s_@@_stop
    \@@_parse_apply_unary_type:NNN
    #3 #2 #4 @
    \exp:w \exp_end_continue_f:w #5 #1
  }
\cs_new:Npn \@@_parse_apply_unary_chk:NwNw #1#2 \@@_sep: #3#4 \s_@@_stop
  {
    \if_meaning:w @ #3 \else:
      \token_if_eq_meaning:NNTF . #3
        { \@@_parse_apply_unary_chk:nNNNNw { no } }
        { \@@_parse_apply_unary_chk:nNNNNw { multi } }
    \fi:
  }
\cs_new:Npn \@@_parse_apply_unary_chk:nNNNNw #1#2#3#4#5#6 @
  {
    #2
    \@@_error:nffn { #1-arg } { \@@_func_to_name:N #4 } { } { }
    \exp_after:wN #4 \exp_after:wN #5 \c_nan_fp @
  }
\cs_new:Npn \@@_parse_apply_unary_type:NNN #1#2#3
  {
    \@@_change_func_type:NNN #3 #1 \@@_parse_apply_unary_error:NNw
    #2 #3
  }
\cs_new:Npn \@@_parse_apply_unary_error:NNw #1#2#3 @
  { \@@_invalid_operation_o:fw { \@@_func_to_name:N #1 } #3 }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_prefix_-:Nw, \@@_parse_prefix_!:Nw}
%   The unary~|-| and boolean not are harder: we parse the operand using
%   a precedence equal to the maximum of the previous precedence~|##1|
%   and the precedence \cs{c_@@_prec_not_int} of the unary operator, then call
%   the appropriate |\__fp_|\meta{operation}|_o:w| function,
%   where the \meta{operation} is |set_sign| or |not|.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1#2#3#4
  {
    \cs_new:cpn { @@_parse_prefix_ #1 :Nw } ##1
      {
        \exp_after:wN \@@_parse_apply_unary:NNNwN
        \exp_after:wN ##1
        \exp_after:wN #4
        \exp_after:wN #3
        \exp:w
        \if_int_compare:w #2 < ##1
          \@@_parse_operand:Nw ##1
        \else:
          \@@_parse_operand:Nw #2
        \fi:
        \@@_parse_expand:w
      }
  }
\@@_tmp:w - \c_@@_prec_not_int \@@_set_sign_o:w 2
\@@_tmp:w ! \c_@@_prec_not_int \@@_not_o:w ?
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_prefix_.:Nw}
%   Numbers which start with a decimal separator (a~period) end up here.
%   Of course, we do not look for an operand, but for the rest of the
%   number.  This function is very similar to \cs{@@_parse_one_digit:NN}
%   but calls \cs{@@_parse_strim_zeros:N} to trim zeros after the
%   decimal point, rather than the \texttt{trim_zeros} function for
%   zeros before the decimal point.
%    \begin{macrocode}
\cs_new:cpn { @@_parse_prefix_.:Nw } #1
  {
    \exp_after:wN \@@_parse_infix_after_operand:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
      \exp_after:wN \@@_sanitize:wN
      \int_value:w \@@_int_eval:w 0 \@@_parse_strim_zeros:N
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {\@@_parse_prefix_(:Nw, \@@_parse_lparen_after:NwN}
%   The left parenthesis is treated as a unary prefix operator because
%   it appears in exactly the same settings.  If the previous precedence
%   is \cs{c_@@_prec_func_int} we are parsing arguments of a function
%   and commas should not build tuples; otherwise commas should build
%   tuples.  We distinguish these cases by precedence:
%   \cs{c_@@_prec_comma_int} for the case of arguments,
%   \cs{c_@@_prec_tuple_int} for the case of tuples.
%   Once the operand is found, the \texttt{lparen_after} auxiliary makes
%   sure that there was a closing parenthesis (otherwise it complains),
%   and leaves in the input stream an operand,
%   fetching the following infix operator.
%    \begin{macrocode}
\cs_new:cpn { @@_parse_prefix_(:Nw } #1
  {
    \exp_after:wN \@@_parse_lparen_after:NwN
    \exp_after:wN #1
    \exp:w
    \if_int_compare:w #1 = \c_@@_prec_func_int
      \@@_parse_operand:Nw \c_@@_prec_comma_int
    \else:
      \@@_parse_operand:Nw \c_@@_prec_tuple_int
    \fi:
    \@@_parse_expand:w
  }
\cs_new:Npe \@@_parse_lparen_after:NwN #1#2 @ #3
  {
    \exp_not:N \token_if_eq_meaning:NNTF #3
      \exp_not:c { @@_parse_infix_):N }
      {
        \exp_not:N \@@_exp_after_array_f:w #2 \s_@@_expr_stop
        \exp_not:N \exp_after:wN
        \exp_not:N \@@_parse_infix_after_paren:NN
        \exp_not:N \exp_after:wN #1
        \exp_not:N \exp:w
        \exp_not:N \@@_parse_expand:w
      }
      {
        \exp_not:N \msg_expandable_error:nnn
          { fp } { missing } { ) }
        \exp_not:N \tl_if_empty:nT {#2} \exp_not:N \c_@@_empty_tuple_fp
        #2 @
        \exp_not:N \use_none:n #3
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_prefix_):Nw}
%   The right parenthesis can appear as a prefix in two similar cases:
%   in an empty tuple or tuple ending with a comma, or in an empty
%   argument list or argument list ending with a comma, such as in
%   |max(1,2,)| or in |rand()|.
%    \begin{macrocode}
\cs_new:cpn { @@_parse_prefix_):Nw } #1
  {
    \if_int_compare:w #1 = \c_@@_prec_comma_int
    \else:
      \if_int_compare:w #1 = \c_@@_prec_tuple_int
        \exp_after:wN \c_@@_empty_tuple_fp \exp:w
      \else:
        \msg_expandable_error:nnn
          { fp } { missing-number } { ) }
        \exp_after:wN \c_nan_fp \exp:w
      \fi:
      \exp_end_continue_f:w
    \fi:
    \@@_parse_infix_after_paren:NN #1 )
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Constants}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_word_inf:N  , \@@_parse_word_nan:N   ,
%     \@@_parse_word_pi:N   , \@@_parse_word_deg:N   ,
%     \@@_parse_word_true:N , \@@_parse_word_false:N ,
%   }
%   Some words correspond to constant floating points.  The floating
%   point constant is left as a result of \cs{@@_parse_one:Nw} after
%   expanding \cs{@@_parse_infix:NN}.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1 #2
  {
    \cs_new:cpn { @@_parse_word_#1:N }
      { \exp_after:wN #2 \exp:w \exp_end_continue_f:w \@@_parse_infix:NN }
  }
\@@_tmp:w { inf } \c_inf_fp
\@@_tmp:w { nan } \c_nan_fp
\@@_tmp:w { pi  } \c_pi_fp
\@@_tmp:w { deg } \c_one_degree_fp
\@@_tmp:w { true } \c_one_fp
\@@_tmp:w { false } \c_zero_fp
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_caseless_inf:N,
%     \@@_parse_caseless_infinity:N,
%     \@@_parse_caseless_nan:N
%   }
%   Copies of \cs[no-index]{@@_parse_word_\ldots{}:N} commands, to allow
%   arbitrary case as mandated by the standard.
%    \begin{macrocode}
\cs_new_eq:NN \@@_parse_caseless_inf:N \@@_parse_word_inf:N
\cs_new_eq:NN \@@_parse_caseless_infinity:N \@@_parse_word_inf:N
\cs_new_eq:NN \@@_parse_caseless_nan:N \@@_parse_word_nan:N
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_word_pt:N , \@@_parse_word_in:N ,
%     \@@_parse_word_pc:N , \@@_parse_word_cm:N , \@@_parse_word_mm:N ,
%     \@@_parse_word_dd:N , \@@_parse_word_cc:N , \@@_parse_word_nd:N ,
%     \@@_parse_word_nc:N , \@@_parse_word_bp:N , \@@_parse_word_sp:N ,
%   }
%   Dimension units are also floating point constants but their value is
%   not stored as a floating point constant.  We give the values
%   explicitly here.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1 #2
  {
    \cs_new:cpn { @@_parse_word_#1:N }
      {
        \@@_exp_after_f:nw { \@@_parse_infix:NN }
        \s_@@ \@@_chk:w 10 #2 \@@_sep:
      }
  }
\@@_tmp:w {pt} { {1} {1000} {0000} {0000} {0000} }
\@@_tmp:w {in} { {2} {7227} {0000} {0000} {0000} }
\@@_tmp:w {pc} { {2} {1200} {0000} {0000} {0000} }
\@@_tmp:w {cm} { {2} {2845} {2755} {9055} {1181} }
\@@_tmp:w {mm} { {1} {2845} {2755} {9055} {1181} }
\@@_tmp:w {dd} { {1} {1070} {0085} {6496} {0630} }
\@@_tmp:w {cc} { {2} {1284} {0102} {7795} {2756} }
\@@_tmp:w {nd} { {1} {1066} {9783} {4645} {6693} }
\@@_tmp:w {nc} { {2} {1280} {3740} {1574} {8031} }
\@@_tmp:w {bp} { {1} {1003} {7500} {0000} {0000} }
\@@_tmp:w {sp} { {-4} {1525} {8789} {0625} {0000} }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_word_em:N, \@@_parse_word_ex:N}
%   The font-dependent units |em| and |ex| must be evaluated on the fly.
%   We reuse an auxiliary of \cs{dim_to_fp:n}.
%    \begin{macrocode}
\tl_map_inline:nn { {em} {ex} }
  {
    \cs_new:cpn { @@_parse_word_#1:N }
      {
        \exp_after:wN \@@_from_dim_test:ww
        \exp_after:wN 0 \exp_after:wN ,
        \int_value:w \dim_to_decimal_in_sp:n { 1 #1 } \exp_after:wN \@@_sep:
        \exp:w \exp_end_continue_f:w \@@_parse_infix:NN
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Functions}
%
% ^^A begin[todo]
%
% \begin{macro}[EXP]
%   {\@@_parse_unary_function:NNN, \@@_parse_function:NNN}
%    \begin{macrocode}
\cs_new:Npn \@@_parse_unary_function:NNN #1#2#3
  {
    \exp_after:wN \@@_parse_apply_unary:NNNwN
    \exp_after:wN #3
    \exp_after:wN #2
    \exp_after:wN #1
    \exp:w
    \@@_parse_operand:Nw \c_@@_prec_func_int \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_function:NNN #1#2#3
  {
    \exp_after:wN \@@_parse_apply_function:NNNwN
    \exp_after:wN #3
    \exp_after:wN #2
    \exp_after:wN #1
    \exp:w
    \@@_parse_operand:Nw \c_@@_prec_func_int \@@_parse_expand:w
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Main functions}
%
% \begin{macro}[EXP]{\@@_parse:n, \@@_parse_o:n}
% \begin{macro}[EXP]{\@@_parse_after:ww}
%   Start an \cs{exp:w} expansion so that \cs{@@_parse:n} expands
%   in two steps.  The \cs{@@_parse_operand:Nw} function performs
%   computations until reaching an operation with precedence
%   \cs{c_@@_prec_end_int} or less, namely, the end of the expression.  The
%   marker \cs{s_@@_expr_mark} indicates that the next token is an already
%   parsed version of an infix operator, and \cs{@@_parse_infix_end:N}
%   has infinitely negative precedence.  Finally, clean up a
%   (well-defined) set of extra tokens and stop the initial expansion
%   with \cs{exp_end:}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse:n #1
  {
    \exp:w
      \exp_after:wN \@@_parse_after:ww
      \exp:w
        \@@_parse_operand:Nw \c_@@_prec_end_int
        \@@_parse_expand:w #1
        \s_@@_expr_mark \@@_parse_infix_end:N
      \s_@@_expr_stop
    \exp_end:
  }
\cs_new:Npn \@@_parse_after:ww
    #1@ \@@_parse_infix_end:N \s_@@_expr_stop #2 { #2 #1 }
\cs_new:Npn \@@_parse_o:n #1
  {
    \exp:w
      \exp_after:wN \@@_parse_after:ww
      \exp:w
        \@@_parse_operand:Nw \c_@@_prec_end_int
        \@@_parse_expand:w #1
        \s_@@_expr_mark \@@_parse_infix_end:N
      \s_@@_expr_stop
    {
      \exp_end_continue_f:w
      \@@_exp_after_any_f:nw { \exp_after:wN \exp_stop_f: }
    }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_operand:Nw}
% \begin{macro}[EXP]{\@@_parse_continue:NwN}
%   This is just a shorthand which sets up both \cs{@@_parse_continue:NwN}
%   and \cs{@@_parse_one:Nw} with the same precedence. Note the
%   trailing \cs{exp:w}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_operand:Nw #1
  {
    \exp_end_continue_f:w
    \exp_after:wN \@@_parse_continue:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
    \exp_after:wN \@@_parse_one:Nw
    \exp_after:wN #1
    \exp:w
  }
\cs_new:Npn \@@_parse_continue:NwN #1 #2 @ #3 { #3 #1 #2 @ }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_apply_binary:NwNwN}
% \begin{macro}[EXP]
%   {\@@_parse_apply_binary_chk:NN, \@@_parse_apply_binary_error:NNN}
%   Receives \meta{precedence} \meta{operand_1} |@| \meta{operation}
%   \meta{operand_2} |@| \meta{infix command}.  Builds the appropriate
%   call to the \meta{operation}~|#3|, dispatching on both types.
%   If the resulting control sequence does not exist, the operation is
%   not allowed.
%
%   This is redefined in \pkg{l3fp-extras}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_apply_binary:NwNwN #1 #2#3@ #4 #5#6@ #7
  {
    \exp_after:wN \@@_parse_continue:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
      \exp_after:wN \@@_parse_apply_binary_chk:NN
        \cs:w
          @@
          \@@_type_from_scan:N #2
          _#4
          \@@_type_from_scan:N #5
          _o:ww
        \cs_end:
        #4
      #2#3 #5#6
    \exp:w \exp_end_continue_f:w #7 #1
  }
\cs_new:Npn \@@_parse_apply_binary_chk:NN #1#2
  {
    \if_meaning:w \scan_stop: #1
      \@@_parse_apply_binary_error:NNN #2
    \fi:
    #1
  }
\cs_new:Npn \@@_parse_apply_binary_error:NNN #1#2#3
  {
    #2
    \@@_invalid_operation_o:Nww #1
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_binary_type_o:Nww, \@@_binary_rev_type_o:Nww}
%   Applies the operator |#1| to its two arguments, dispatching
%   according to their types, and expands once after the result.
%   The |rev| version swaps its arguments before doing this.
%    \begin{macrocode}
\cs_new:Npn \@@_binary_type_o:Nww #1 #2#3 \@@_sep: #4
  {
    \exp_after:wN \@@_parse_apply_binary_chk:NN
      \cs:w
        @@
        \@@_type_from_scan:N #2
        _ #1
        \@@_type_from_scan:N #4
        _o:ww
      \cs_end:
      #1
    #2 #3 \@@_sep: #4
  }
\cs_new:Npn \@@_binary_rev_type_o:Nww #1 #2#3 \@@_sep: #4#5 \@@_sep:
  {
    \exp_after:wN \@@_parse_apply_binary_chk:NN
      \cs:w
        @@
        \@@_type_from_scan:N #4
        _ #1
        \@@_type_from_scan:N #2
        _o:ww
      \cs_end:
      #1
    #4 #5 \@@_sep: #2 #3 \@@_sep:
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Infix operators}
%
% \begin{macro}[EXP]{\@@_parse_infix_after_operand:NwN}
%    \begin{macrocode}
\cs_new:Npn \@@_parse_infix_after_operand:NwN #1 #2\@@_sep:
  {
    \@@_exp_after_f:nw { \@@_parse_infix:NN #1 }
    #2\@@_sep:
  }
\cs_new:Npn \@@_parse_infix:NN #1 #2
  {
    \if_catcode:w \scan_stop: \exp_not:N #2
      \if:w 0 \@@_str_if_eq:nn { \s_@@_expr_mark } { \exp_not:N #2 }
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_mark:NNN
      \else:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_juxt:N
      \fi:
    \else:
      \if_int_compare:w
          \@@_int_eval:w
            ( `#2 \if_int_compare:w `#2 > `Z - 32 \fi: ) / 26
          = 3 \exp_stop_f:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_juxt:N
      \else:
        \exp_after:wN \@@_parse_infix_check:NNN
        \cs:w
          @@_parse_infix_ \token_to_str:N #2 :N
          \exp_after:wN \exp_after:wN \exp_after:wN
        \cs_end:
      \fi:
    \fi:
    #1
    #2
  }
\cs_new:Npn \@@_parse_infix_check:NNN #1#2#3
  {
    \if_meaning:w \scan_stop: #1
      \msg_expandable_error:nnn
        { fp } { missing } { * }
      \exp_after:wN \@@_parse_infix_mul:N
      \exp_after:wN #2
      \exp_after:wN #3
    \else:
      \exp_after:wN #1
      \exp_after:wN #2
      \exp:w \exp_after:wN \@@_parse_expand:w
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_parse_infix_after_paren:NN}
%   Variant of \cs{@@_parse_infix:NN} for use after a closing
%   parenthesis.  The only difference is that \cs{@@_parse_infix_juxt:N}
%   is replaced by \cs{@@_parse_infix_mul:N}.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_infix_after_paren:NN #1 #2
  {
    \if_catcode:w \scan_stop: \exp_not:N #2
      \if:w 0 \@@_str_if_eq:nn { \s_@@_expr_mark } { \exp_not:N #2 }
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_mark:NNN
      \else:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_mul:N
      \fi:
    \else:
      \if_int_compare:w
          \@@_int_eval:w
            ( `#2 \if_int_compare:w `#2 > `Z - 32 \fi: ) / 26
          = 3 \exp_stop_f:
        \exp_after:wN \exp_after:wN
        \exp_after:wN \@@_parse_infix_mul:N
      \else:
        \exp_after:wN \@@_parse_infix_check:NNN
        \cs:w
          @@_parse_infix_ \token_to_str:N #2 :N
          \exp_after:wN \exp_after:wN \exp_after:wN
        \cs_end:
      \fi:
    \fi:
    #1
    #2
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Closing parentheses and commas}
%
% \begin{macro}[EXP]{\@@_parse_infix_mark:NNN}
%   As an infix operator, \cs{s_@@_expr_mark} means that the next
%   token~(|#3|) has already gone through \cs{@@_parse_infix:NN} and
%   should be provided the precedence~|#1|.  The scan mark~|#2| is
%   discarded.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_infix_mark:NNN #1#2#3 { #3 #1 }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_infix_end:N}
%   This one is a little bit odd: force every previous operator to end,
%   regardless of the precedence.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_infix_end:N #1
  { @ \use_none:n \@@_parse_infix_end:N }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]+\@@_parse_infix_):N+
%   This is very similar to \cs{@@_parse_infix_end:N}, complaining about
%   an extra closing parenthesis if the previous operator was the
%   beginning of the expression, with precedence \cs{c_@@_prec_end_int}.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1
  {
    \cs_new:Npn #1 ##1
      {
        \if_int_compare:w ##1 > \c_@@_prec_end_int
          \exp_after:wN @
          \exp_after:wN \use_none:n
          \exp_after:wN #1
        \else:
          \msg_expandable_error:nnn { fp } { extra } { ) }
          \exp_after:wN \@@_parse_infix:NN
          \exp_after:wN ##1
          \exp:w \exp_after:wN \@@_parse_expand:w
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_):N }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[verb, EXP]{\__fp_parse_infix_,:N}
% \begin{macro}[EXP]{\@@_parse_infix_comma:w, \@@_parse_apply_comma:NwNwN}
%   As for other infix operations, if the previous operations has higher
%   precedence the comma waits.  Otherwise we call
%   \cs{@@_parse_operand:Nw} to read more comma-delimited arguments that
%   \cs{@@_parse_infix_comma:w} simply concatenates into a |@|-delimited
%   array.  The first comma in a tuple that is not a function argument
%   is distinguished: in that case call \cs{@@_parse_apply_comma:NwNwN}
%   whose job is to convert the first item of the tuple and an array of
%   the remaining items into a tuple.  In contrast to
%   \cs{@@_parse_apply_binary:NwNwN} this function's operands are not
%   single-object arrays.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1
  {
    \cs_new:Npn #1 ##1
      {
        \if_int_compare:w ##1 > \c_@@_prec_comma_int
          \exp_after:wN @
          \exp_after:wN \use_none:n
          \exp_after:wN #1
        \else:
          \if_int_compare:w ##1 < \c_@@_prec_comma_int
            \exp_after:wN @
            \exp_after:wN \@@_parse_apply_comma:NwNwN
            \exp_after:wN ,
            \exp:w
          \else:
            \exp_after:wN \@@_parse_infix_comma:w
            \exp:w
          \fi:
          \@@_parse_operand:Nw \c_@@_prec_comma_int
          \exp_after:wN \@@_parse_expand:w
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_,:N }
\cs_new:Npn \@@_parse_infix_comma:w #1 @
  { #1 @ \use_none:n }
\cs_new:Npn \@@_parse_apply_comma:NwNwN #1 #2@ #3 #4@ #5
  {
    \exp_after:wN \@@_parse_continue:NwN
    \exp_after:wN #1
    \exp:w \exp_end_continue_f:w
    \@@_exp_after_tuple_f:nw { }
      \s_@@_tuple \@@_tuple_chk:w { #2 #4 } \@@_sep:
    #5 #1
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsubsection{Usual infix operators}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_infix_+:N, \@@_parse_infix_-:N,
%     \@@_parse_infix_juxt:N,
%     \@@_parse_infix_/:N, \@@_parse_infix_mul:N,
%     \@@_parse_infix_and:N, \@@_parse_infix_or:N,
%   }
% \begin{macro}[EXP]+\@@_parse_infix_^:N+
%   As described in the \enquote{work plan}, each infix operator has an
%   associated |\..._infix_...| function, a computing function, and
%   precedence, given as arguments to \cs{@@_tmp:w}.  Using the general
%   mechanism for arithmetic operations.  The power operation must be
%   associative in the opposite order from all others.  For this, we use
%   two distinct precedences.
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1#2#3#4
  {
    \cs_new:Npn #1 ##1
      {
        \if_int_compare:w ##1 < #3
          \exp_after:wN @
          \exp_after:wN \@@_parse_apply_binary:NwNwN
          \exp_after:wN #2
          \exp:w
          \@@_parse_operand:Nw #4
          \exp_after:wN \@@_parse_expand:w
        \else:
          \exp_after:wN @
          \exp_after:wN \use_none:n
          \exp_after:wN #1
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_^:N }   ^
  \c_@@_prec_hatii_int \c_@@_prec_hat_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_juxt:N } *
  \c_@@_prec_juxt_int \c_@@_prec_juxt_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_/:N }   /
  \c_@@_prec_times_int \c_@@_prec_times_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_mul:N } *
  \c_@@_prec_times_int \c_@@_prec_times_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_-:N }   -
  \c_@@_prec_plus_int  \c_@@_prec_plus_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_+:N }   +
  \c_@@_prec_plus_int  \c_@@_prec_plus_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_and:N } &
  \c_@@_prec_and_int   \c_@@_prec_and_int
\exp_args:Nc \@@_tmp:w { @@_parse_infix_or:N }  |
  \c_@@_prec_or_int    \c_@@_prec_or_int
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsubsection{Juxtaposition}
%
% \begin{macro}[EXP]+\@@_parse_infix_(:N+
%   When an opening parenthesis appears where we expect an infix
%   operator, we compute the product of the previous operand and the
%   contents of the parentheses using \cs{@@_parse_infix_mul:N}.
%    \begin{macrocode}
\cs_new:cpn { @@_parse_infix_(:N } #1
  { \@@_parse_infix_mul:N #1 ( }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Multi-character cases}
%
% \begin{macro}[EXP]{\@@_parse_infix_*:N}
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1
  {
    \cs_new:cpn { @@_parse_infix_*:N } ##1##2
      {
        \if:w * \exp_not:N ##2
          \exp_after:wN #1
          \exp_after:wN ##1
        \else:
          \exp_after:wN \@@_parse_infix_mul:N
          \exp_after:wN ##1
          \exp_after:wN ##2
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_^:N }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]+\@@_parse_infix_|:Nw+
% \begin{macro}[EXP]+\@@_parse_infix_&:Nw+
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1#2#3
  {
    \cs_new:Npn #1 ##1##2
      {
        \if:w #2 \exp_not:N ##2
          \exp_after:wN #1
          \exp_after:wN ##1
          \exp:w \exp_after:wN \@@_parse_expand:w
        \else:
          \exp_after:wN #3
          \exp_after:wN ##1
          \exp_after:wN ##2
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_|:N } | \@@_parse_infix_or:N
\exp_args:Nc \@@_tmp:w { @@_parse_infix_&:N } & \@@_parse_infix_and:N
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsubsection{Ternary operator}
%
% \begin{macro}[EXP]{\@@_parse_infix_?:N, \@@_parse_infix_::N}
%    \begin{macrocode}
\cs_set_protected:Npn \@@_tmp:w #1#2#3#4
  {
    \cs_new:Npn #1 ##1
      {
        \if_int_compare:w ##1 < \c_@@_prec_quest_int
          #4
          \exp_after:wN @
          \exp_after:wN #2
          \exp:w
          \@@_parse_operand:Nw #3
          \exp_after:wN \@@_parse_expand:w
        \else:
          \exp_after:wN @
          \exp_after:wN \use_none:n
          \exp_after:wN #1
        \fi:
      }
  }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_?:N }
  \@@_ternary:NwwN \c_@@_prec_quest_int { }
\exp_args:Nc \@@_tmp:w { @@_parse_infix_::N }
  \@@_ternary_auxii:NwwN \c_@@_prec_colon_int
  {
    \msg_expandable_error:nnnn
      { fp } { missing } { ? } { ~for~?: }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Comparisons}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_infix_<:N, \@@_parse_infix_=:N,
%     \@@_parse_infix_>:N, \@@_parse_infix_!:N
%   }
% \begin{macro}[EXP]
%   {
%     \@@_parse_excl_error:,
%     \@@_parse_compare:NNNNNNN,
%     \@@_parse_compare_auxi:NNNNNNN,
%     \@@_parse_compare_auxii:NNNNN,
%     \@@_parse_compare_end:NNNNw,
%     \@@_compare:wNNNNw,
%   }
%    \begin{macrocode}
\cs_new:cpn { @@_parse_infix_<:N } #1
  { \@@_parse_compare:NNNNNNN #1 1 0 0 0 0 < }
\cs_new:cpn { @@_parse_infix_=:N } #1
  { \@@_parse_compare:NNNNNNN #1 1 0 0 0 0 = }
\cs_new:cpn { @@_parse_infix_>:N } #1
  { \@@_parse_compare:NNNNNNN #1 1 0 0 0 0 > }
\cs_new:cpn { @@_parse_infix_!:N } #1
  {
    \exp_after:wN \@@_parse_compare:NNNNNNN
    \exp_after:wN #1
    \exp_after:wN 0
    \exp_after:wN 1
    \exp_after:wN 1
    \exp_after:wN 1
    \exp_after:wN 1
  }
\cs_new:Npn \@@_parse_excl_error:
  {
    \msg_expandable_error:nnnn
      { fp } { missing } { = } { ~after~!. }
  }
\cs_new:Npn \@@_parse_compare:NNNNNNN #1
  {
    \if_int_compare:w #1 < \c_@@_prec_comp_int
      \exp_after:wN \@@_parse_compare_auxi:NNNNNNN
      \exp_after:wN \@@_parse_excl_error:
    \else:
      \exp_after:wN @
      \exp_after:wN \use_none:n
      \exp_after:wN \@@_parse_compare:NNNNNNN
    \fi:
  }
\cs_new:Npn \@@_parse_compare_auxi:NNNNNNN #1#2#3#4#5#6#7
  {
    \if_case:w
      \@@_int_eval:w \exp_after:wN ` \token_to_str:N #7 - `<
        \@@_int_eval_end:
         \@@_parse_compare_auxii:NNNNN #2#2#4#5#6
    \or: \@@_parse_compare_auxii:NNNNN #2#3#2#5#6
    \or: \@@_parse_compare_auxii:NNNNN #2#3#4#2#6
    \or: \@@_parse_compare_auxii:NNNNN #2#3#4#5#2
    \else: #1 \@@_parse_compare_end:NNNNw #3#4#5#6#7
    \fi:
  }
\cs_new:Npn \@@_parse_compare_auxii:NNNNN #1#2#3#4#5
  {
    \exp_after:wN \@@_parse_compare_auxi:NNNNNNN
    \exp_after:wN \prg_do_nothing:
    \exp_after:wN #1
    \exp_after:wN #2
    \exp_after:wN #3
    \exp_after:wN #4
    \exp_after:wN #5
    \exp:w \exp_after:wN \@@_parse_expand:w
  }
\cs_new:Npn \@@_parse_compare_end:NNNNw #1#2#3#4#5 \fi:
  {
    \fi:
    \exp_after:wN @
    \exp_after:wN \@@_parse_apply_compare:NwNNNNNwN
    \exp_after:wN \c_one_fp
    \exp_after:wN #1
    \exp_after:wN #2
    \exp_after:wN #3
    \exp_after:wN #4
    \exp:w
    \@@_parse_operand:Nw \c_@@_prec_comp_int \@@_parse_expand:w #5
  }
\cs_new:Npn \@@_parse_apply_compare:NwNNNNNwN
    #1 #2@ #3 #4#5#6#7 #8@ #9
  {
    \if_int_odd:w
        \if_meaning:w \c_zero_fp #3
          0
        \else:
          \if_case:w \@@_compare_back_any:ww #8 #2 \exp_stop_f:
            #5 \or: #6 \or: #7 \else: #4
          \fi:
        \fi:
        \exp_stop_f:
      \exp_after:wN \@@_parse_apply_compare_aux:NNwN
      \exp_after:wN \c_one_fp
    \else:
      \exp_after:wN \@@_parse_apply_compare_aux:NNwN
      \exp_after:wN \c_zero_fp
    \fi:
    #1 #8 #9
  }
\cs_new:Npn \@@_parse_apply_compare_aux:NNwN #1 #2 #3\@@_sep: #4
  {
    \if_meaning:w \@@_parse_compare:NNNNNNN #4
      \exp_after:wN \@@_parse_continue_compare:NNwNN
      \exp_after:wN #1
      \exp_after:wN #2
      \exp:w \exp_end_continue_f:w
      \@@_exp_after_o:w #3\@@_sep:
      \exp:w \exp_end_continue_f:w
    \else:
      \exp_after:wN \@@_parse_continue:NwN
      \exp_after:wN #2
      \exp:w \exp_end_continue_f:w
      \exp_after:wN #1
      \exp:w \exp_end_continue_f:w
    \fi:
    #4 #2
  }
\cs_new:Npn \@@_parse_continue_compare:NNwNN #1#2 #3@ #4#5
  { #4 #2 #3@ #1 }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Tools for functions}
%
% \begin{macro}[EXP]{\@@_parse_function_all_fp_o:fnw}
%   Followed by \Arg{function name} \Arg{code} \meta{float array} |@|
%   this checks all floats are floating point numbers (no tuples).
%    \begin{macrocode}
\cs_new:Npn \@@_parse_function_all_fp_o:fnw #1#2#3 @
  {
    \@@_array_if_all_fp:nTF {#3}
      { #2 #3 @ }
      {
        \@@_error:nffn { bad-args }
          {#1}
          { \fp_to_tl:n { \s_@@_tuple \@@_tuple_chk:w {#3} \@@_sep: } }
          { }
        \exp_after:wN \c_nan_fp
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_function_one_two:nnw}
% \begin{macro}[EXP]
%   {
%     \@@_parse_function_one_two_error_o:w,
%     \@@_parse_function_one_two_aux:nnw,
%     \@@_parse_function_one_two_auxii:nnw
%   }
%   This is followed by \Arg{function name} \Arg{code} \meta{float
%   array} |@|.  It checks that the \meta{float array} consists of one
%   or two floating point numbers (not tuples), then leaves the
%   \meta{code} (if there is one float) or its tail (if there are two
%   floats) followed by the \meta{float array}.  The \meta{code} should
%   start with a single token such as \cs{@@_atan_default:w} that deals
%   with the single-float case.
%
%   The first \cs{@@_if_type_fp:NTwFw} test catches the case of no
%   argument and the case of a tuple argument.  The next one
%   distinguishes the case of a single argument (no error, just add
%   \cs{c_one_fp}) from a tuple second argument.  Finally check there is
%   no further argument.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_function_one_two:nnw #1#2#3
  {
    \@@_if_type_fp:NTwFw
      #3 { } \s_@@ \@@_parse_function_one_two_error_o:w \s_@@_stop
    \@@_parse_function_one_two_aux:nnw {#1} {#2} #3
  }
\cs_new:Npn \@@_parse_function_one_two_error_o:w #1#2#3#4 @
  {
    \@@_error:nffn { bad-args }
      {#2}
      { \fp_to_tl:n { \s_@@_tuple \@@_tuple_chk:w {#4} \@@_sep: } }
      { }
    \exp_after:wN \c_nan_fp
  }
\cs_new:Npn \@@_parse_function_one_two_aux:nnw #1#2 #3\@@_sep: #4
  {
    \@@_if_type_fp:NTwFw
      #4 { }
      \s_@@
      {
        \if_meaning:w @ #4
          \exp_after:wN \use_iv:nnnn
        \fi:
        \@@_parse_function_one_two_error_o:w
      }
      \s_@@_stop
    \@@_parse_function_one_two_auxii:nnw {#1} {#2} #3\@@_sep: #4
  }
\cs_new:Npn \@@_parse_function_one_two_auxii:nnw #1#2#3\@@_sep: #4\@@_sep: #5
  {
    \if_meaning:w @ #5 \else:
      \exp_after:wN \@@_parse_function_one_two_error_o:w
    \fi:
    \use_ii:nn {#1} { \use_none:n #2 } #3\@@_sep: #4\@@_sep: #5
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_tuple_map_o:nw, \@@_tuple_map_loop_o:nw}
%   Apply |#1| to all items in the following tuple and expand once
%   afterwards.  The code |#1| should itself expand once after its
%   result.
%    \begin{macrocode}
\cs_new:Npn \@@_tuple_map_o:nw #1 \s_@@_tuple \@@_tuple_chk:w #2 \@@_sep:
  {
    \exp_after:wN \s_@@_tuple
    \exp_after:wN \@@_tuple_chk:w
    \exp_after:wN {
      \exp:w \exp_end_continue_f:w
      \@@_tuple_map_loop_o:nw {#1} #2
        { \s_@@ \prg_break: } \@@_sep:
      \prg_break_point:
    \exp_after:wN } \exp_after:wN \@@_sep:
  }
\cs_new:Npn \@@_tuple_map_loop_o:nw #1#2#3 \@@_sep:
  {
    \use_none:n #2
    #1 #2 #3 \@@_sep:
    \exp:w \exp_end_continue_f:w
    \@@_tuple_map_loop_o:nw {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_tuple_mapthread_o:nww, \@@_tuple_mapthread_loop_o:nw}
%   Apply |#1| to pairs of items in the two following tuples and expand once
%   afterwards.
%    \begin{macrocode}
\cs_new:Npn \@@_tuple_mapthread_o:nww #1
    \s_@@_tuple \@@_tuple_chk:w #2 \@@_sep:
    \s_@@_tuple \@@_tuple_chk:w #3 \@@_sep:
  {
    \exp_after:wN \s_@@_tuple
    \exp_after:wN \@@_tuple_chk:w
    \exp_after:wN {
      \exp:w \exp_end_continue_f:w
      \@@_tuple_mapthread_loop_o:nw {#1}
        #2 { \s_@@ \prg_break: } \@@_sep: @
        #3 { \s_@@ \prg_break: } \@@_sep:
      \prg_break_point:
    \exp_after:wN } \exp_after:wN \@@_sep:
  }
\cs_new:Npn \@@_tuple_mapthread_loop_o:nw #1#2#3 \@@_sep: #4 @ #5#6 \@@_sep:
  {
    \use_none:n #2
    \use_none:n #5
    #1 #2 #3 \@@_sep: #5 #6 \@@_sep:
    \exp:w \exp_end_continue_f:w
    \@@_tuple_mapthread_loop_o:nw {#1} #4 @
  }
%    \end{macrocode}
% \end{macro}
%
% ^^A end[todo]
%
% \subsection{Messages}
%
%    \begin{macrocode}
\msg_new:nnn { fp } { deprecated }
  { '#1'~deprecated;~use~'#2' }
\msg_new:nnn { fp } { unknown-fp-word }
  { Unknown~fp~word~#1. }
\msg_new:nnn { fp } { missing }
  { Missing~#1~inserted #2. }
\msg_new:nnn { fp } { extra }
  { Extra~#1~ignored. }
\msg_new:nnn { fp } { early-end }
  { Premature~end~in~fp~expression. }
\msg_new:nnn { fp } { after-e }
  { Cannot~use~#1 after~'e'. }
\msg_new:nnn { fp } { missing-number }
  { Missing~number~before~'#1'. }
\msg_new:nnn { fp } { unknown-symbol }
  { Unknown~symbol~#1~ignored. }
\msg_new:nnn { fp } { extra-comma }
  { Unexpected~comma~turned~to~nan~result. }
\msg_new:nnn { fp } { no-arg }
  { #1~got~no~argument;~used~nan. }
\msg_new:nnn { fp } { multi-arg }
  { #1~got~more~than~one~argument;~used~nan. }
\msg_new:nnn { fp } { bad-args }
  { Arguments~in~#1#2~are~invalid. }
\msg_new:nnn { fp } { infty-pi }
  { Math~command~#1 is~not~an~fp }
\cs_if_exist:cT { @unexpandable@protect }
  {
    \msg_new:nnn { fp } { robust-cmd }
      { Robust~command~#1 invalid~in~fp~expression! }
  }
%    \end{macrocode}
%
%    \begin{macrocode}
%</code>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintChanges
%
% \PrintIndex
