18

I'm reading source2e.pdf and I have problems in understanding the sense of the definition of LaTeX 2ε's \@esphack:

Commands like \label{...} , which themselves do not/shall not produce any visible output, might in the source/in the .tex-input-file be surrounded by spaces. If so, you don't want two space-tokens as this would double the horizontal glue.

If I got it right, \@bsphack and \@esphack are there for avoiding the coming into being of a space-token after tokenizing and processing the command in case there already came one into being before tokenizing and processing the command.

So let's look at the definition of \@bsphack:

> \@bsphack=macro:
->\relax \ifhmode \@savsk \lastskip \@savsf \spacefactor \fi .
l.1    \show\@bsphack

In words:

When in horizontal mode or in restricted horizontal mode, then save the value of \lastskip to \@savsk and save the value of \spacefactor to \@savsf.

Now let's look at the definition of \@esphack:

> \@esphack=macro:
->\relax \ifhmode \spacefactor \@savsf \ifdim \@savsk >\z@ \ifdim \lastskip =\z
@ \nobreak \hskip \z@skip \fi \ignorespaces \fi \fi .
l.2    \show\@esphack

In words:

When in horizontal mode or in restricted horizontal mode, then:

  • restore the value of \spacefactor to the value saved as \savsf.
  • In case \@savsk is larger than zero, i.e., in case there was some horizontal glue before carrying out the command which at its start called \@bsphack, then do
    \ifdim \lastskip =\z@ \nobreak \hskip \z@skip \fi \ignorespaces .

I understand that \ignorespaces will make sense as there was already some horizontal glue before carrying out the command which at its start called \@bsphack.

But I don't understand what the
\ifdim \lastskip =\z@ \nobreak \hskip \z@skip \fi -part is good for.

What is the sense of this?

In the situation where this is carried out, the saved value in \@savsk is larger than \z@, thus one can conclude that something from within the command which at its start called \@bsphack did change the value of \lastskip to \z@.

But doing something like \nobreak\hskip\z@skip will not revert that change/will not restore \lastskip to its previous value.

Thus: What is the gist/sense/benefit of performing that \hskip of zero-width?

If you wish \lastskip to be restored, shouldn't it then be something like:

\def\@esphack{%
  \relax
  \ifhmode
    \spacefactor\@savsf
    \ifdim\@savsk>\z@
      \ifdim\lastskip=\z@
        \nobreak
        \hskip-\@savsk
        \nobreak
        \hskip\@savsk
        % the total skip is zero and \lastskip is restored.
      \fi
      \ignorespaces
    \fi
  \fi
}%

Or perhaps just

\def\@esphack{%
  \relax
  \ifhmode
    \spacefactor\@savsf %now \spacefactor is restored.
    \nobreak
    \hskip-\@savsk
    \nobreak
    \hskip\@savsk
    % the total skip is zero and \lastskip is restored.
    \ifdim\@savsk>\z@
      \ignorespaces
    \fi
  \fi
}%

Is it possible that in some previous release, it was done this way and in later releases somebody erroneously "optimized" the \nobreak\hskip-\@savsk\nobreak\hskip\@savsk to \nobreak\hskip\z@skip, overlooking that this won't restore \lastskip any more?

If I understand correctly, you need to restore \lastskip correctly as otherwise things with consecutive sequences of \@bsphack..\@esphack, e.g., \label{foo}\label{bar} won't work correctly:

If by now you do something like
A \label{foo} \label{bar} A
or
A \label{foo}\label{bar} A
, \lastskip will in any case be 0 after \label{foo} which affects the behavior of \label{bar} because with \label{bar} \@savsk will not be larger than \z@ any more so that \ignorespaces won't get carried out although it should be carried out!

With the example below you can see a subtle difference when redefining \@esphack so that it restores \lastskip:

\documentclass{article}

\newsavebox\mybox

\begin{document}

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{1}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{1}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{2} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{2} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{3}\label{4} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{3}\label{4} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|  \label{5} \label{6} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{5} \label{6} B

\makeatletter
\def\@esphack{%
  \relax
  \ifhmode
    \spacefactor\@savsf
    \ifdim\@savsk>\z@
      \ifdim\lastskip=\z@
        \nobreak
        \hskip-\@savsk
        \nobreak
        \hskip\@savsk
        % the total skip is zero and \lastskip is restored.
      \fi
      \ignorespaces
    \fi
  \fi
}%
\makeatother

\noindent\null\hrulefill\null

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{a}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{a}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{b} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{b} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{c}\label{d} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{c}\label{d} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{e} \label{f} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{e} \label{f} B

\end{document}

enter image description here

Does my modification of \@esphack have drawbacks?

Am I overlooking some caveat/something relevant?

David Carlisle
  • 757,742

1 Answers1

11

You see more change comments, and older versions in the source:

% \begin{macro}{\@esphack}
%   Companion to |\@bsphack|.  If this command is not properly paired
%   with |\@bsphack| one might end up with a low-level \TeX{} error:
%   ``BAD spacefactor''. One possible cause is calling |\@bsphack| in
%   vertical mode, then doing something that gets you (sometimes) into
%   horizontal mode and finally calling |\@esphack|. Even if no error
%   is generated that is wrong, because |\@esphack| will then use the
%   saved values for |\@savsk| and |\@savsf| from some earlier
%   invocation of |\@bsphack| which will have nothing to do with the
%   current situation.
% \changes{v1.3d}{2015/01/11}{Allow hyphenation (Donald Arseneau pr/3498) (latexrelease)}
%    \begin{macrocode}
%</2ekernel>
%<latexrelease>\IncludeInRelease{2018/10/10}%
%<latexrelease>                 {\@esphack}{hyphenation and nobreak after space hack}%
%<*2ekernel|latexrelease>
\def\@esphack{%
  \relax
  \ifhmode
    \spacefactor\@savsf
    \ifdim\@savsk>\z@
%    \end{macrocode}
% \changes{v1.3f}{2015/11/07}
%         {Only space if there is no space at the end of the hlist latex/4443}
%    \begin{macrocode}
      \ifdim\lastskip=\z@ 
        \nobreak \hskip\z@skip
      \fi
      \ignorespaces
    \fi
%    \end{macrocode}
% \changes{v1.3i}{2018/10/10}
%         {Don't introduce breakpoints if @nobreak is true and after sections}
%    \begin{macrocode}
  \else
    \ifvmode
      \if@nobreak\nobreak\else\if@noskipsec\nobreak\fi\fi
    \fi
%    \end{macrocode}
%
%    \begin{macrocode}
  \fi}%
%</2ekernel|latexrelease>
%<latexrelease>\EndIncludeInRelease
%<latexrelease>\IncludeInRelease{2015/10/01}%
%<latexrelease>                 {\@esphack}{hyphenation and nobreak after space hack}%
%<latexrelease>\def\@esphack{%
%<latexrelease>  \relax
%<latexrelease>  \ifhmode
%<latexrelease>    \spacefactor\@savsf
%<latexrelease>    \ifdim\@savsk>\z@
%<latexrelease>      \ifdim\lastskip=\z@ 
%<latexrelease>        \nobreak \hskip\z@skip
%<latexrelease>      \fi
%<latexrelease>      \ignorespaces
%<latexrelease>    \fi
%<latexrelease>  \fi}%
%<latexrelease>\EndIncludeInRelease
%<latexrelease>\IncludeInRelease{2015/01/01}%
%<latexrelease>                 {\@esphack}{hyphenation and nobreak after space hack}%
%<latexrelease>\def\@esphack{%
%<latexrelease>  \relax
%<latexrelease>  \ifhmode
%<latexrelease>    \spacefactor\@savsf
%<latexrelease>    \ifdim\@savsk>\z@
%<latexrelease>      \nobreak \hskip\z@skip
%<latexrelease>      \ignorespaces
%<latexrelease>    \fi
%<latexrelease>  \fi}%
%<latexrelease>\EndIncludeInRelease
%<latexrelease>\IncludeInRelease{0000/00/00}%
%<latexrelease>                 {\@esphack}{hyphenation and nobreak after space hack}%
%<latexrelease>\def\@esphack{%
%<latexrelease>  \relax
%<latexrelease>  \ifhmode
%<latexrelease>    \spacefactor\@savsf
%<latexrelease>    \ifdim\@savsk>\z@
%<latexrelease>      \ignorespaces
%<latexrelease>    \fi
%<latexrelease>  \fi}%
%<latexrelease>\EndIncludeInRelease
%<*2ekernel>
%    \end{macrocode}
% \end{macro}

Basically the bit you query, you need to add a space (or zero width) so as not to inhibit hyphenation, and then need to add the \nobreak so the space doesn't introduce a breakpoint.


Splitting the 0pt skip into \hskip-\zzz\hskip\zzz works better here as you know how the following \lastskip test is being used, however it leaves the horizontal list slightly vulnerable to any following \unskip that is removing a skip (rather than just testing it) as it will only remove half of the pair leaving a possibly unintended negative skip in the list.

I came up with this table example although arguably the result is also better in this case as the remaining negative skip cancels out the original space before the first index.. But other reasonable uses of \unskip would need to be checked....

enter image description here

\documentclass{article}

\newsavebox\mybox
\makeindex
\tabcolsep=0pt
\begin{document}

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{1}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{1}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{2} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{2} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{3}\label{4} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{3}\label{4} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|  \label{5} \label{6} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{5} \label{6} B

\begin{tabular}{|r|}
A A \index{A}\\
B B \index{B}\\
\end{tabular}


\makeatletter
\def\@esphack{%
  \relax
  \ifhmode
    \spacefactor\@savsf
    \ifdim\@savsk>\z@
      \ifdim\lastskip=\z@
        \nobreak
        \hskip-\@savsk
        \nobreak
        \hskip\@savsk
        % the total skip is zero and \lastskip is restored.
      \fi
      \ignorespaces
    \fi
  \fi
}%
\makeatother

\noindent\null\hrulefill\null

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{a}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{a}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{b} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{b} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{c}\label{d} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{c}\label{d} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{e} \label{f} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{e} \label{f} B

\begin{tabular}{|r|}
A A \index{A}\\
B B \index{B}\\
\end{tabular}

\end{document}

Rather than use \ifdim\lastskip>0pt as a test for "is there preceding glue" I think we could use the e-tex \ifnum\lastnodetype=11 test to avoid adding spurious -ve/+ve glue pairs or spurious 1sp glue nodes:

\documentclass{article}

\newsavebox\mybox
\makeindex
\tabcolsep=0pt
\begin{document}

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{1}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{1}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{2} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{2} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{3}\label{4} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{3}\label{4} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|  \label{5} \label{6} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{5} \label{6} B

\begin{tabular}{|r|}
A A \index{A}\\
B B \index{B}\\
\end{tabular}


\makeatletter
\def\@bsphack{%
  \relax
  \ifhmode
    \@savsk\lastskip
    \ifdim\@savsk=\z@\ifnum\lastnodetype=11 \@savsk1sp \fi\fi
    \@savsf\spacefactor
  \fi}


% \def\@esphack{%
%   \relax
%   \ifhmode
%     \spacefactor\@savsf
%     \ifdim\@savsk>\z@
%       \ifdim\lastskip=\z@
%         \nobreak
%         \hskip1sp
%         % the total skip is zero and \lastskip is restored.
%       \fi
%       \ignorespaces
%     \fi
%   \fi
% }%

\makeatother

\noindent\null\hrulefill\null

\par\noindent\begin{lrbox}{\mybox}
\verb*|A\label{a}B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A\label{a}B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{b} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{b} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{c}\label{d} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil} A \label{c}\label{d} B

\par\noindent\begin{lrbox}{\mybox}
\verb*|A \label{e} \label{f} B|:
\end{lrbox}\hbox to 4.5cm{\usebox\mybox\hfil}  A \label{e} \label{f} B

\begin{tabular}{|r|}
A A \index{A}\\
B B \index{B}\\
\end{tabular}

\end{document}
David Carlisle
  • 757,742
  • I just added another example to my question to make my point about consecutive commands that are wrapped into \@bsphack..\@esphack clearer. –  Mar 24 '19 at 23:29
  • Thanks for your reply. I see. But wouldn't it be better to add that space (of zero width) in a way which also restores \lastskip so that one can have consecutive commands that don't produce visible output in a row? If by now you do something like A \label{foo} \label{bar} B , \lastskip will in any case be 0 after \label{foo} which affects the behavior of \label{bar} because with \label{bar} \@savsk will not be larger than \z@ any more so that \ignorespaces won't get carried out although it should be carried out? Is \nobreak\hskip-\@savsk\nobreak\hskip\@savsk unsafe? –  Mar 24 '19 at 23:40
  • To be honest: It is not clear to me what is meant by "not to inhibit hyphenation". Do people intend to place, e.g., \label into the middle of a word? If so: What is the insertion of (zero-width-)glue good for in the context of not inhibiting hyphenation? –  Mar 24 '19 at 23:56
  • @Questioner see the bug report mentioned above, with the original definition words following a figure environment, or \index{..} were not hyphenated https://www.latex-project.org/cgi-bin/ltxbugs2html?category=LaTeX&responsible=anyone&state=anything&keyword=&pr=latex%2F3498&search= – David Carlisle Mar 25 '19 at 00:55
  • Thank you for the clarification about hyphenation. Does changing \nobreak\hskip\z@skip to \nobreak\hskip-\@savsk\nobreak\hskip\@savsk have any drawbacks/pitfalls? (I edited my question for pointing out why I think that restoring \lastskip is necessary for the case of having consecutive commands whose definitions are wrapped in \@bsphack..\@esphack.) –  Mar 25 '19 at 01:12
  • @Questioner the modification suggestion is not unreasonable but it's a tricky area and I'd need to think about whether it has drawbacks, ending with a -ve and +skip means that \lastskip sees a positive skip at a place where there is no visible space, which might be bad in other contexts, I'd need to think about it. You could make a change suggestion at the new issue tracker at https://github.com/latex3/latex2e/issues so we don't lose the suggestion (you can reference this post) – David Carlisle Mar 25 '19 at 10:30
  • Seems with that modification \lastskip sees that positive skip (only?) in case \@savsk was positive due to a chain of consecutive \@bsphack..\@esphack that was preceded by a positive skip/a visible space... (By the way: I implemented a similar \@bsphack/\@esphack-variant in my answer to Generating a Persistent and Unique IDs (for Requirements in a Specification) ) – Ulrich Diez Mar 25 '19 at 20:21
  • 1
    @UlrichDiez yes but that isn't the case I'm worried about, it is cases where this is followed by uses of \unskip rather than \lastskip, I added an example above (although not clearly bad example) – David Carlisle Mar 26 '19 at 08:10
  • @Questioner A perhaps safer alternative than \nobreak\hskip-\@savsk\nobreak\hskip\@savsk would be \nobreak\hskip1sp that would keep a single glue node but be enough to trigger the "positive skip" case in a following space hack. – David Carlisle Mar 26 '19 at 12:04
  • @Questioner I added a new version testing the last node type, we might use this.... – David Carlisle Mar 27 '19 at 14:33