[tex-eplain] a new index bug?

Oleg Katsitadze olegkat at gmail.com
Wed Aug 29 23:07:55 CEST 2007


Thanks to Gérald Tenenbaum who provided his .ind file, I could
reproduce the bug, and found the problem with the disappearing
double-column material, and several other bugs/problems with the
multi-column macros.

All these changes, of course, will break backward compatibility, but
at least some of them can be considered bug fixes.  But if somebody
feels against any of the changes, I can take them out of the patch.

This multi-column business is pretty tricky, so it would be great if
somebody could test the changes in production and/or review the patch
before I apply it.  (I did try the patch on many tests, including
those in the test subdir.)  I've uploaded the patched eplain.tex to

  http://tug.org/eplain/misc/eplain.tex.3-1-91.zip

To Gérald:  Can you please try it with the copy of your document that
you saved, and see if it fixes your columns?  Just unzip it and put it
in the same directory with your document, it should be found instead
of your system eplain.tex.

So, here goes the prose...

1) First on the original problem.  When Eplain balances columns at the
end of the multi-column material, it is possible that the collected
material will not fit when it is split into columns.  Imagine that
column break falls at the middle of a line, or even an unbreakable
block of several lines (e.g., in index, \item followed by \subitem).
This means that this whole block is moved to the next column, the
previous column is stretched out, and some material at the end of the
last column is shifted out.  The solution is to increase column height
and retry the splitting.  The current Eplain does that, but only once.
After the first retry, if anything is still left, it is just
discarded.  Usually one retry is sufficient, but sometimes too much
material is shifted out, and we have to increase column height again
and again, until everything fits.  So this is what my fix does.  The
attached lost.tex illustrates the bug (try compiling it with the old
eplain.tex, then with the new).

2) Next, when the current Eplain does increase column height, it does
so by the amount of \baselineskip.  This made sense when there was
only one retry.  But now that we retry in a loop, I suggest
incrementing column height by 1pt.  This will produce "tighter"
columns.  For example, the effect can be seen at the end of the
double-column material in Eplain's test/column.tex.  The current
Eplain produces way too much space after the columns.  With my patch,
the space is much closer to the actual \belowcolumnskip.

3) When balancing columns by increasing column height, we might hit
page bottom.  In this case I suggest to leave what fits on this page,
carry over the left-over to the next page, and then recursively split
the left-over on the next page.  However, there's another catch -- if
the page contains insertions, there's a slight chance that the
footnotes or top insertions were inserted by the multi-column material
we are going to carry over to the next page, so those footnotes or top
insertions will appear one page ahead of their references.  The worst
thing is that the user will get no warning of this.  Therefore, when
we have left-overs on a page with insertions, we just stuff them into
columns on the same page to produce an overfull box warning.  The
attached inserts.tex illustrates this.

4) Suppose there is some single-column material at the top of the
page, then below there is double-column material which ends on the
same page.  When double-column material starts, Eplain calculates how
high the columns on this page should be in order to fit on the page.
At this point, it has no idea how much interline skip will be inserted
before the double-column material, so it does not account for it in
the calculation.  However, when later we output double-column
material, the interline glue is inserted.  When the columns happen to
occupy _exactly_ the remaining page, this unaccounted-for glue may
shift out the columns to the next page.  The fix is simple -- add
\nointerlineskip before the double columns.  That's what I did.
(Besides fixing the bug, this is also more consistent, because
\singlecolumn inserts \nointerlineskip _after_ the columns end.)
The attached interline.tex illustrates the bug.

5) When \@endcolumns calculates column height, it takes \pagetotal
(the length of the "scroll") and divides it by the number of columns.
This can sometimes result in columns which won't fit on a page and
which therefore will be shifted to the next page -- imagine that the
current page can accommodate columns at most Npt high, so \pagetotal
should be at most (N * num_of_columns)pt; but the last line on the
"scroll" has taken \pagetotal from (N * num_of_columns - 2)pt to, say,
(N * num_of_columns + 10)pt, so this is what \@endcolumns finds in
\pagetotal (TeX will leave the last line on the current page, because
there was no legitimate page break yet after the last line); in this
case the calculated column height will be > Npt, and the columns won't
fit.  The solution is to make columns (\pagetotal/num_of_columns)pt
high if \pagetotal <= \pagegoal, and (\pagegoal/num_of_columns)pt high
otherwise.  The attached pagetotal.tex illustrates the bug.

Phew...

Cheers,
Oleg
-------------- next part --------------
--- xeplain.tex	28 Aug 2007 09:51:32 +0300	1.43
+++ xeplain.tex	29 Aug 2007 16:49:26 +0300	
@@ -3457,36 +3457,19 @@
 % By default, we define \gutterbox to be "empty": 
 \def\gutterbox{\vbox to \dimen0{\vfil\hbox{\hfil}\vfil}}%
 %
-% [This next paragraph added by AHL on 22 Sep 1996.]
-% There is a special case we have to worry about, namely when we
-% switch from multiple columns to single columns. In this case, we may
-% have a bit of text left over that doesn't fit evenly into the
-% multiple columns. In that case, we will have un-even columns. Not a
-% pretty solution, but I don't have a better one at the moment. The
-% conditional \if at forceextraline tests whether we are in this case.
-%
-\newif\if at forceextraline\@forceextralinefalse
 \def\@columnsplit{%
   \splittopskip = \topskip
   \splitmaxdepth = \baselineskip
   %
-  % \dimen@ will be the height that the double-column material on this
-  % page should have, i.e., the height of the page (\singlecolumvsize)
-  % minus single-column material, which includes insertions.  (If you
-  % want your insertions to respect the columns, you will have to
-  % change the output routine.)  If you add more insertions, they
-  % should be taken into account both here and in \singlecolumn.
+  % \dimen@ should be the height that columns on this page should
+  % have, i.e., the height of the page (\singlecolumvsize) minus
+  % single-column material, which includes insertions.  (If you want
+  % your insertions to respect the columns, you will have to change
+  % the output routine.)  If you add more insertions, they should be
+  % taken into account in \@columns and \@endcolumns.
   %
   % Unfortunately, we lose on flexible glue because we must
   % \vsplit to a <dimen>.
-  \dimen@ = \ht255
-    \divide\dimen@ by \@numcolumns
-  %
-  % If we are in an end-column mode and we need extra vertical space
-  % in the last column, advance \dimen at . 
-  \if at forceextraline
-    \advance\dimen@ by \baselineskip
-  \fi
   %
   % Split the long scroll into columns.
   \begingroup
@@ -3509,11 +3492,7 @@
       \global\setbox7 = \vsplit255 to \dimen@ \global\wd7 = \hsize
     \fi
   \endgroup
-  \if at forceextraline                         % If this is the first time
-  \else                                      % through, save the single
-    \setbox\@forcelinebox=\copy\@partialpage % column material. 
-  \fi
-  % 
+  %
   % Set up \box255 with the real output page, as the previous output
   % routine expects.
   \setbox0 = \box255
@@ -3537,6 +3516,8 @@
 % Our output routine splits the columns and then calls the previous one.
 % 
 \def\@columnoutput{%
+  \dimen@ = \ht255
+    \divide\dimen@ by \@numcolumns
   \@columnsplit
   \@recoverclubpenalty 
   \hsize = \@normalhsize % Local to \output's group.
@@ -3562,41 +3543,137 @@
   \nointerlineskip
 }%
 %
-\newbox\@forcelinebox 
+\newbox\@singlecolumnbox 
+\newdimen\column at pagegoal
+\newdimen\column at vsize
+%
 \def\@endcolumns{%
   \global\let\@ndcolumns = \relax
   \par % Shouldn't start in horizontal mode.
-  %
+  % Save the combined height of the columns and the page goal.  We
+  % have to be careful -- the last line of the multi-column material
+  % might have taken \pagetotal just over \pagegoal, in which case we
+  % have to use \pagegoal for the height, otherwise the box produced
+  % when splitting the columns will not fit on the page.
+  \column at pagegoal = \pagegoal
+  \ifdim \pagetotal > \pagegoal
+    \column at vsize = \pagegoal
+  \else
+    \column at vsize = \pagetotal
+  \fi
+  % Grab whatever is left of the multi-column material.
   \global\output = {\global\setbox1 = \box255}%
   \pagegoal = \pagetotal
   \break                     % Exercise the page builder, i.e., \output.
   \setbox2 = \box1           % Save material in box2 in case of overflow.
-  \global\setbox255 = \copy2 % Retrieve what the fake \output set.
-  %
-  % \box255 now has the double-column material.  On the page where we
-  % switch back to one column, the double-column material might not
-  % fill up the page.  We want to split whatever is there.
+  % We won't need \columnoutput anymore.
+  \global\output = \expandafter{\the\previousoutput}%
+  % Save single column material (in case the multi-column material
+  % ends on the same page where it starts; otherwise \@partialpage
+  % will be void).  This also voids \@partialpage.
+  \setbox\@singlecolumnbox = \box\@partialpage
+  % Try to fit what's left into the columns.
+  \@balancecolumns
+}%
+%
+% There are many caveats when balancing columns at the end of
+% multi-column material.  The core of all problems is the following.
+% Eplain collects multi-column material in one long scroll until the
+% scroll's length is at least \vsize * \@numcolumns.  But when we try
+% to split that scroll into columns, there is no guarantee that all
+% the collected material will fit (for example, because a column break
+% occured inside a large unbreakable block, so we had to carry it over
+% to the next column and stretch out the previous column).  So every
+% time we call \@columnsplit, we should expect it to leave something
+% for us in \@partialpage.
+%
+% Now, what should we do with these left-overs?  We increase column
+% height a bit and try to split the scroll again, and so on until
+% everything fits.
+%
+% However, while doing this, we might increase column height so much
+% that the columns no longer fit on a page.  What we do then is output
+% the highest fitting columns, break the page and then restart the
+% whole process on what's left.
+%
+% However, there's another catch -- if a page contains insertions,
+% there's a slight chance that the footnotes or top insertions were
+% inserted by the multi-column material we are going to carry over to
+% the next page, so those footnotes or top insertions will appear one
+% page ahead of their references.  The worst thing is that the user
+% will get no warning of this.  Therefore, when we have left-overs on
+% a page with insertions, we just stuff them into columns to produce
+% an overfull box warning.
+%
+\def\@balancecolumns{%
+  % Split the scroll to the new column height.
+  \global\setbox255 = \copy2  % Retrieve what the fake \output set.
+  \dimen@ = \column at vsize
+    \divide\dimen@ by \@numcolumns
   \@columnsplit
   %
-  % Check that \@columnsplit did not leave any lines in
-  % \@partialpage. If it did, do it again, but this time with longer
-  % columns. 
   \ifvoid\@partialpage
-  \else % There is some left-over. 
-    \setbox0=\box\@partialpage % Merely to void \@partialpage
-    \global\setbox255 = \box2  % Retrieve what the fake \output set.
-    \@forceextralinetrue       % Add \forcelinebox to \box255 to save single
-    \@columnsplit              % column material. 
-    \global\setbox255 = \vbox{\box\@forcelinebox\box255}%
-  \fi
-
-  \global\vsize = \@normalvsize
-  \global\hsize = \@normalhsize
-  \global\output = \expandafter{\the\previousoutput}%
-  %
+    % Everything fits -- we're done.
+    \global\vsize = \@normalvsize
+    \global\hsize = \@normalhsize
+    \dump at balanced@columns
+    \let\next\relax
+  \else
+    % There is some left-over.  Increase column height.
+    \advance \column at vsize by \@numcolumns pt
+    % Check what we should do with the left-over.
+    \test at spill@columns
+    \ifspill at columns
+      % We are to break the page here.  Make up a page from the
+      % single-column material followed by the columns that we've
+      % already split off into \box255.
+      \begingroup
+        \vsize = \@normalvsize
+        \hsize = \@normalhsize
+        \dump at balanced@columns
+        \break
+        \@recoverclubpenalty
+      \endgroup
+      % Now put back what didn't fit and process it recursively.
+      \unvbox\@partialpage
+      \let\next\@endcolumns
+    \else
+      % We should continue incrementing column height.
+      \setbox0=\box\@partialpage % Merely to void \@partialpage.
+      \let\next\@balancecolumns
+    \fi
+  \fi
+  \next
+}%
+%
+\def\dump at balanced@columns{%
   \ifvoid\topins\else\topinsert\unvbox\topins\endinsert\fi
-  \unvbox255
+  \unvbox\@singlecolumnbox
+  % Avoid interline glue here -- we didn't (couldn't) account for it
+  % when assessing \vsize for the columns in \@columns, which means
+  % the columns may not fit if we also add the interline glue.
+  \nointerlineskip
+  \box255
+}%
+%
+% If we hit page bottom while balancing columns and there are no
+% insertions on the page, we can let the left-over spill to the next
+% page.  If there are insertions on the page, we shouldn't let
+% \@partialpage to the next page, to avoid separating a possible
+% reference from an insertion.  The following flag and test are to
+% check these conditions.
+\newif\ifspill at columns
+\def\test at spill@columns{%
+  \spill at columnsfalse
+  \ifdim \column at vsize > \column at pagegoal
+    \ifvoid\footins
+      \ifvoid\topins
+        \spill at columnstrue
+      \fi
+    \fi
+  \fi
 }%
+%
 % [\columnfill]
 % We don't have any way to force a column eject, since the \output
 % routine is only prepared to split up a full page of material. Instead,
-------------- next part --------------
A non-text attachment was scrubbed...
Name: lost.tex
Type: text/x-tex
Size: 238 bytes
Desc: not available
Url : http://tug.org/pipermail/tex-eplain/attachments/20070830/8575940b/attachment.bin 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: inserts.tex
Type: text/x-tex
Size: 362 bytes
Desc: not available
Url : http://tug.org/pipermail/tex-eplain/attachments/20070830/8575940b/attachment-0001.bin 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: interline.tex
Type: text/x-tex
Size: 427 bytes
Desc: not available
Url : http://tug.org/pipermail/tex-eplain/attachments/20070830/8575940b/attachment-0002.bin 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: pagetotal.tex
Type: text/x-tex
Size: 390 bytes
Desc: not available
Url : http://tug.org/pipermail/tex-eplain/attachments/20070830/8575940b/attachment-0003.bin 


More information about the tex-eplain mailing list