[Tex/LaTex] How to generate “You can’t use \unskip in vertical mode”

errorsprogrammingtex-core

We all know (or believe so) that \unskip is not possible in main vertical mode and this statement can be found in various answers on the site. And yes LaTeX does a lot of gymnastics in \addvspace or \addpenalty to deal with this problem. So I was a bit surprised when in The second column always appears lower than the first one in the multicol environment a simple workaround using \unskip worked.

This was on the main vertical list!

So looking into the TeX program itself, the condition is that the error will be triggered if the last item on the list is a glue item and this glue item has already been contributed to the current page. But when does this happen?

XXX
\par
\vskip 1cm 
\vskip 5cm
\unskip\unskip
XXX
\bye

This works perfectly and according to the TeXbook there are only 5 cases when stuff gets moved from recent contributions to the current page (page 122):

Here is a list of the times when that can happen: (a) At the beginning
or end of a paragraph, provided that this paragraph is being
contributed to the main vertical list. (b) At the beginning or end of
a displayed equation within such a paragraph. (c) After completing an
\halign in vertical mode. (d) After contributing a box or penalty or
insertion to the main vertical list. (e) After an \output routine
has ended.

Unfortunately it is not quite accurate (probably there are some other places discussing this further that I have overlooked). For example, if you add an empty line after the \vskip in the example above, then it triggers the error even though that \par clearly doesn't trigger or ends any paragraph.

I can imagine to create it from case (e), e.g., by making the output routine add a glue to the main vertical list, though I haven't tried that.

Any other cases in which this could be triggered in reality?

Best Answer

Looking at the source tex.web, I gather the following, none of which I am sure of. From the (separate) lines

primitive("unskip",remove_item,glue_node);

any_mode(remove_item): delete_last;

I see that \unskip invokes the procedure delete_last, which has the following comment in tex.web:

Like \lastbox, this command is not allowed in vertical mode (except internal vertical mode), since the current list in vertical mode is sent to the page builder. But if we happen to be able to implement it in vertical mode, we do.

In other words, TeX tries its best to remove something, but this is sometimes impossible. In fact, the code for delete_last "apologizes for [the] inability to do the operation now" if the current mode is vmode and (I believe) there are no recent contributions, unless \unskip follows a non-glue node (in which case \unskip does nothing). Your assessment that we need to find out when things are moved out of the recent contributions is thus correct.

The main vertical list is split into two parts: from page_head to page_tail lies the current page, and from contrib_head to contrib_tail lie recent contributions. Searching for contrib_, I find the variables used in several places:

  • in the procedure show_activities, link(contrib_head) is queried, to know if there are any recent contributions;
  • the procedure build_page appears to move material from the recent contributions to the current page, so it is crucial for us;
  • link(contrib_head):=link(p) is used in two places to add a node to the recent contributions;
  • q:=new_skip_param(top_skip_code) then link(contrib_head):=q adds the topskip to the page when the first box is added;
  • when breaking the page at a given node, whatever follows in the current page is placed before the recent contributions, by manipulating contrib_head and contrib_tail;
  • the default output routine appears to place the current page back into the recent contributions (except of course what has already been split and placed into \box255), then shipout \box255;
  • the same happens after a user-defined output routine (save for the \box255 business).

From this, we see that build_page is probably what you are after. It is called in the following cases (labelled with a letter corresponding to your list):

  • (a) when a paragraph is started in vmode (by a horizontal command such as \indent, a letter, \discretionary etc), after inserting \everypar into the input stream;
  • (b) after insering the \everydisplay token list in the input stream and having set various parameters upon entering display mode, if the surrounding mode is vmode;
  • (b) after going back to horizontal mode (and saving the language etc) after a display is finished, if the surrounding mode is vmode;
  • (a) after ending a paragraph with \par in horizontal mode if the resulting mode is vmode;
  • (c) when \halign ends in vmode;
  • (d) after inserting a penalty in vmode;
  • (d) within the procedure box_end, if the finished box (or box register, or...) is non-null and is to be added to the main vertical list (rather than stored, shipped out, or used as a leader);
  • (d) when an \insert or \vadjust ends with an end-group character, if the resulting mode is vmode;
  • (e) after TeX has finished a user's output routine, checked that \box255 is empty, and placed all the current page material into recent contributions (see above);
  • when encountering \par in vmode;
  • after inserting \hbox to \hsize{}\vfill\penalty-'10000000000 if there is still some material in the main vertical list when meeting the primitive \end.

A few differences crop up compared to your list. After a default output routine, build_page does not seem to be used. The case of \end is missing from your list. More importantly, the case of \par in unrestricted vertical mode is not explicitly given in your list (although (a) would fit it, I guess). This case explains why

XXX\par
\vskip 1cm
%
\unskip

fails upon removing the %. Contrarily to expectations, \par in vertical mode is not ignored: it moves material from the recent contributions to the current page, but also resets some parameters (\looseness, \hangindent, \hangafter and \parshape). This comes to me as a surprise.

I found one last thing. The procedure build_page is not used in append_glue because the procedure append_glue "is used in at least one place where that would be a mistake". I do not know where the mistake would be. But this explains why \vskip 1cm does not make TeX move any material to the current page.

Related Question