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) linesI see that
\unskip
invokes the proceduredelete_last
, which has the following comment intex.web
: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 isvmode
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
topage_tail
lies the current page, and fromcontrib_head
tocontrib_tail
lie recent contributions. Searching forcontrib_
, I find the variables used in several places:show_activities
,link(contrib_head)
is queried, to know if there are any recent contributions;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)
thenlink(contrib_head):=q
adds the topskip to the page when the first box is added;contrib_head
andcontrib_tail
;\box255
), then shipout\box255
;\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):vmode
(by a horizontal command such as\indent
, a letter,\discretionary
etc), after inserting\everypar
into the input stream;\everydisplay
token list in the input stream and having set various parameters upon entering display mode, if the surrounding mode isvmode
;vmode
;\par
in horizontal mode if the resulting mode isvmode
;\halign
ends invmode
;vmode
;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);\insert
or\vadjust
ends with an end-group character, if the resulting mode isvmode
;\box255
is empty, and placed all the current page material into recent contributions (see above);\par
invmode
;\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 whyfails 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 inappend_glue
because the procedureappend_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.