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.
\vadjust
then the error is triggered. A\par
in vertical mode doesn't enter horizontal mode, but exercises the page builder if something is present in the contribution list (second bullet on page 283 of the TeXbook).@* \[45] The page builder.
fromtex.web
, which seems relevant. Alternatively, search forpage_tail
orcontrib_head
, which seem to mark the separation between the current page and the recent contributions.\par
in vertical mode callsnormal_paragraph
andbuild_page
. The first sets\looseness=0
,\hangindent=0pt
,\hangafter=1
,\parshape=0
. The second moves material from the recent contributions to the current page. Still investigating on all occurrences ofbuild_page
.