There are multiple matters here that have been brought up in various iterations of the question, comments, and answers:
- what TeX does with trailing tabs at the end of a line
- after reading in lines, how TeX treats tabs
- how the visual width of a space is affected by the font
- how TeX treats punctuation, specifically the
.
I know your ultimate question is only about the first one, so let's get the other things out of the way first.
Consider the following test file, which is intended to illustrate the same points as your example:
\def\test{
a b
a\ b
a\ \ b
% There's a tab in the line below
a b
% There's a tab at the end of the line below
a
b
}
\def\testdot{
. .
.\ .
.\ \ .
% There's a tab in the line below
. .
% There's a tab at the end of the line below
.
.
}
\test
\testdot
\vskip 1em \hrule \vskip 1em
\font\myfont=cmtt14\myfont
\test
\testdot
\end
This produces:

Note that the .
s behave differently from the a-b example except in the cases with explicit spaces (.\ .
and .\ \ .
). However, these differences are just because TeX typesets a larger space after sentences for typographic reasons (and all these differences go away if you add \frenchspacing
), so we might as well work with just simple letters to avoid that confusion.
So with that out of the way, here is a simpler case:
\catcode9=12
% There's a tab in the line below
a b
% There's a tab at the end of the line below
a
b
\end
produces:

Conclusion: Trailing tabs are removed when reading a line.
Now your remaining question is why this is so, and whether this is consistent with Knuth's intentions as stated in either The TeXbook or the TeX program (aka Volume A and Volume B).
In section 31 of the TeX program is the input_ln
procedure which reads in lines. He says “trailing blanks are removed”.

He also says this is something TeX implementations are encouraged to rewrite and optimize:

His own implementation is something that strips any trailing character whose xord is 32 (the fact that " "
which appears as
below actually means 32
is an aspect of WEB and string pool files…):

Note the xord
there: it converts any character to an integer. And in the "official" implementation as in the book, the question of tabs in the input file has a simpler answer: they are treated as invalid:

Strictly speaking this means that if you had a hypothetical TeX implementation straight out of tex.web
and with absolutely no system-dependent changes so that it used tex.web
's implementation of input_ln
and the same xord
array (even Knuth never used such an implementation), then you couldn't even have a tab character in your file, anywhere: it would have an xord
of invalid_code
= 127, and when TeX encountered it, it would throw an error. (You can still see such an error by introducing byte 127 in a file: the error message is ! Text line contains an invalid character.
)
As Heiko's answer points out, the web2c
implementation of TeX as used in TeX Live implements input_ln
in C for speed, as suggested by Knuth in section 31. It strips both trailing spaces and trailing tabs. This is consistent with a certain interpretation of "trailing blanks are removed" (which is probably why the change was considered ok). Note that by itself, it's not automatically inconsistent with the TeXbook on p. 46 saying that “TeX deletes any ⟨space⟩ characters (number 32) that occur at the right end of an input line” — that only refers to what happens after the file's bytes are translated using xord
. This is what Knuth means when on pp. 44–45 of The TeXbook it says:
the people who installed your local TeX system can tell you the
correspondence between what you type and the character number that TeX receives
So deleting these trailing tabs would have been perfectly consistent with the tex.web
code of input_ln
(and what's described in The TeXbook above), if its xord
had been set up so that tabs are always treated as spaces (this is not the case). Instead, it follows a setting of xord
and xchr
where it appears all bytes are allowed in files and (other than a newline) byte N is read by TeX as N (nothing else gets translated to invalid_code
). For curiosity, I created a file containing a<character C><newline>b
for every non-printable character C from 0 to 127 (that is: 0 to 31, and 127). I also added a few \catcode
instructions so that all of these (except for 13
) have catcode 12. This is the result:
% xxd -g 1 -c 24 mwe.tex
00000000: 5c 63 61 74 63 6f 64 65 30 3d 31 32 0a 5c 63 61 74 63 6f 64 65 31 3d 31 \catcode0=12.\catcode1=1
00000018: 32 0a 5c 63 61 74 63 6f 64 65 39 3d 31 32 0a 5c 63 61 74 63 6f 64 65 31 2.\catcode9=12.\catcode1
00000030: 31 3d 31 32 0a 5c 63 61 74 63 6f 64 65 31 32 3d 31 32 0a 5c 63 61 74 63 1=12.\catcode12=12.\catc
00000048: 6f 64 65 31 34 3d 31 32 0a 5c 63 61 74 63 6f 64 65 31 32 37 3d 31 32 0a ode14=12.\catcode127=12.
00000060: 61 00 0a 62 0a 0a 61 01 0a 62 0a 0a 61 02 0a 62 0a 0a 61 03 0a 62 0a 0a a..b..a..b..a..b..a..b..
00000078: 61 04 0a 62 0a 0a 61 05 0a 62 0a 0a 61 06 0a 62 0a 0a 61 07 0a 62 0a 0a a..b..a..b..a..b..a..b..
00000090: 61 08 0a 62 0a 0a 61 09 0a 62 0a 0a 61 0b 0a 62 0a 0a 61 0c 0a 62 0a 0a a..b..a..b..a..b..a..b..
000000a8: 61 0d 0a 62 0a 0a 61 0e 0a 62 0a 0a 61 0f 0a 62 0a 0a 61 10 0a 62 0a 0a a..b..a..b..a..b..a..b..
000000c0: 61 11 0a 62 0a 0a 61 12 0a 62 0a 0a 61 13 0a 62 0a 0a 61 14 0a 62 0a 0a a..b..a..b..a..b..a..b..
000000d8: 61 15 0a 62 0a 0a 61 16 0a 62 0a 0a 61 17 0a 62 0a 0a 61 18 0a 62 0a 0a a..b..a..b..a..b..a..b..
000000f0: 61 19 0a 62 0a 0a 61 1a 0a 62 0a 0a 61 1b 0a 62 0a 0a 61 1c 0a 62 0a 0a a..b..a..b..a..b..a..b..
00000108: 61 1d 0a 62 0a 0a 61 1e 0a 62 0a 0a 61 1f 0a 62 0a 0a 61 7f 0a 62 0a 0a a..b..a..b..a..b..a..b..
00000120: 5c 65 6e 64 0a \end.

Summary:
- In the
web2c
implementation of C, the xord
of having byte c
in the input file results in TeX receiving input code c
. (This is allowed by Knuth's conventions: appendix C of The TeXbook is entirely about this kind of thing.)
- In the
web2c
implementation of C, trailing spaces and tabs are removed. This is inconsistent with what is described in The TeXbook, though it would have been consistent if the xord
of a tab character were 32. (Though in that case plain.tex
's definition of \catcode`\^^I=10
would have been mostly unnecessary (applying only when someone explicitly wrote ^^I
in their input file), as a tab character in the input file would already be read by input_ln
as a space.)
{}
, to the end of the 3rd line produces the extra space in the output, so clearly the tab and the end-of-line are being digested as a single space.^^I
is also of potential interest, since it’s the ASCII <tab>. Plain TEX makes <tab> act like a blank space."\relax
.