答案1
您正在查看的文件实际上并不是“源代码”,它是tex0.c
从tex.web
TeX 源代码(由 web2c 编写)衍生而来的 C 代码,而 TeX 是用 web(文档化的 pascal)编写的。几乎所有注释都在此转换中被删除。
因此,您会看到各种跳转到任意标记为lab22
如果你查看 tex.web,你会看到这个过程
@p procedure clear_for_error_prompt;
begin while (state<>token_list)and terminal_input and@|
(input_ptr>0)and(loc>limit) do end_file_reading;
print_ln; clear_terminal;
end;
并在不同的地方引用了这一点,例如
@ @<Get user's advice...@>=
loop@+begin continue: clear_for_error_prompt; prompt_input("? ");
在生成的 C 中,这将发送为
void
clearforerrorprompt ( void )
{
clearforerrorprompt_regmem
while ( ( curinput .statefield != 0 ) && ( curinput .namefield == 0 ) && (
inputptr > 0 ) && ( curinput .locfield > curinput .limitfield ) )
endfilereading () ;
println () ;
}
以及对它的带标签的引用
lab22: clearforerrorprompt () ;
然后从循环的其他部分跳转到该点:
goto lab22 ;
要了解源代码,最好阅读从 tex.web 生成的 tex 排版文档,而不是从中提取的 C。
texdoc tex
在 texlive 中将显示此源的超链接排版版本。此部分显示为:
答案2
(只是为了给另一个答案添加更多细节……)
答案的简短版本:对于问题屏幕截图中具体显示的标签,您可以将标签读作,lab22
即continue
代码将goto
恢复循环的标签,并将标签读作lab20
,restart
即程序将goto
再次启动程序的标签。 (请参阅 TeX 程序的第 15 节或下文。)
长版本:
为什么 TeX 程序有那么多goto
s?
关于 goto 的争论
以前,计算机是用机器语言或(略有改进)汇编语言编程的,其中控制流使用分支/跳转来表示。Donald Knuth(后来编写了 TeX)做了很多这样的编程(例如:1957-1960 年(视频,文本),1960)。随着高级编程语言的发展,它们使用关键字(例如)goto
来表示此类任意跳转,还引入了控制流结构来表示常见的跳转,例如条件(if
)和循环(while
)。然后在 1968 年,CACM 编辑 Niklaus Wirth 发表了 Edsger Dijkstra 的一篇文章(提交为“反对 Goto 语句的案例”),引发了一场重大争议,标题为“Go To 语句被认为有害”。辩论的大致两个阵营是:
该
goto
语句不是必需的,最好避免;高级语言提供的控制流结构已经足够了。现有的控制流结构太有限;有时
goto
确实是必要的/好的。
前一种观点以“结构化编程”(Dijkstra、Wirth、Hoare 等)为代表。Knuth 于 1974 年撰写了一篇百科全书文章,总结了双方(以及更多)观点(PDF,HTML) 一直对后者抱有同情。
无论如何,目前的争论状态(见维基百科摘要)似乎是,虽然标题这封信似乎已经成为教条,几乎每个程序员都避免或害怕使用这单词“goto”,实际上就是当时可用的控制流结构(ALGOL 60,阿尔法,ALGOL 68等)确实被认为是不够的,并且语言确实获得了更多的 Dijkstra 不喜欢的控制流结构,这些结构涵盖了(比如说) Knuth 的论文中提出的例子:早期return
来自函数,以及循环continue
和break
(甚至是标记的,在像 Java 和 Rust 这样的语言中)。
goto
帕斯卡
Knuth 于 1997 年编写了 TeX 的第一个版本(仅供斯坦福大学使用)。帆但当其他地方对该程序产生了足够多的兴趣,并且存在不兼容的实现危险时,他着手用一种当时广泛使用的语言以最大程度的可移植性重写该程序,并且自然选择是帕斯卡。现在帕斯卡(由上面提到的 Wirth 发明)试图大力鼓励结构化编程:
函数没有
return
语句;相反,您必须将其分配给与函数同名的伪变量,并且控制流必须到达函数底部才能退出。循环中没有
break
或;您可以使用布尔值(或使用)。continue
goto
goto
仍然可用但不鼓励,例如必须事先在函数(或程序)的顶部声明标签,并且这些标签必须是数字:不允许使用符号名称!
例如,在现代语言中你可以编写这样的函数(求所有小于 n 的奇数之和):
def sum_odd(n):
if n < 1: return 0
sum = 0
for i in range(n):
if i % 2 != 1: continue
sum += i
return sum
return
(这仅仅是一个带有和的虚构例子continue
;当然这个函数可以在没有它们的情况下编写)在 Pascal 中如果您想要精确的翻译,您必须使用goto
s 并选择一些临时的数字标签:
function sumodd(n: integer)
label
42, 100;
var
i, sum: integer;
begin
if n < 1 then
begin
sumodd := 0;
goto 100
end
sum := 0;
for i := 1 to n - 1 do
begin
if i mod 2 <> 1 then goto 42;
sum := sum + i;
42:
end
sumodd := sum;
100:
end
我猜这足以诱使goto
人们避免使用布尔值等进行重写(在这种情况下很简单,但并非总是如此)。
goto
在 TeX 和 WEB 中
Pascal 的这些特性对于教学语言来说还不错,但是 Knuth(像其他的) 一定觉得编写大型软件程序很烦人,因为现实世界中存在性能等限制,所以他创建了一个名为 WEB 的系统,可以解决许多此类限制。上面的同一个程序在 WEB 中可以写成:
在前面的一些部分中,
define
整个程序的符号名称和宏:define exit = 10 define continue = 22 define return == goto exit
然后在编写函数时,可以使用上述名称(例如,记得
exit
在函数结束前放置一个名为的标签),例如轻微地更好的体验。
这是 TeX 所遵循的系统,它使用goto
但通常遵守某些约定,如该计划第 15 条:
如果您直接使用 TeX 的 Pascal 实现,那么您会在 (WEB 的一部分) 生成的源代码中看到数字标签tangle
,但实际上大多数 TeX 用户使用的发行版 (如 TeX Live) 基于将此 WEB/Pascal 转换为 C (使用类似 的系统web2c
),其中数字标签再次被翻译为以 开头lab
。
常用标签
lab20 = 重新启动
例如这里使用这个(第380条,我重新格式化了缩进):
procedure get_x_token; {sets |cur_cmd|, |cur_chr|, |cur_tok|, and expands macros}
label
restart, done;
begin
restart:
get_next;
if cur_cmd <= max_command then
goto done;
if cur_cmd >= call then
if cur_cmd < end_template then
macro_call
else
begin
cur_cs := frozen_endv;
cur_cmd:=endv;
goto done; {|cur_chr=null_list|}
end
else expand;
goto restart;
done:
if cur_cs = 0 then
cur_tok := (cur_cmd * 256) + cur_chr
else
cur_tok := cs_token_flag + cur_cs;
end;
所以这个函数体结构restart: ... if (...) goto done; ... goto restart; done: ...
基本上是一个无限循环,goto done
当满足某些条件时就会退出。
lab22 = 继续
这用于重新执行循环,例如
while true do
begin
continue:
...
if ... then goto continue;
...
end
或其变体(放在continue
循环体的末尾等)。
问题中的具体例子
另一个令人困惑的问题是,我们运行的(如果使用像 TeX Live 这样的现代发行版)不是直接由 Knuth 编写的 TeX,而是应用了多个更改/补丁的 TeX — 并且这些更改可能并不总是遵循相同的标签约定,有时甚至不是有效的 Pascal/WEB(例如未声明标签),因为它们仅通过 web2c 管道(翻译成 C 并通过 C 编译器运行)进行了测试,而不是通过 Pascal 编译器。问题中显示的内容来自 EncTeX(请参阅加拿大运输安全局,背页)在源代码中,看起来像这:
...
if (i = start) and (not mubyte_start) then
begin
mubyte_keep := 0;
if (end_line_char >= 0) and (end_line_char < 256) then
if mubyte_read [end_line_char] <> null then
begin
mubyte_start := true; mubyte_skip := -1;
p := mubyte_read [end_line_char];
goto continue;
end;
end;
restart:
mubyte_start := false;
if (mubyte_read [buffer[i]] = null) or (mubyte_keep > 0) then
begin
if mubyte_keep > 0 then decr (mubyte_keep);
return ;
end;
p := mubyte_read [buffer[i]];
continue:
if type (p) >= 64 then
begin
last_type := type (p) - 64;
p := link (p);
mubyte_token := info (p); last_found := mubyte_skip;
end
...
变成了你在问题中展示的(几乎不可读的)C 代码:
...
if ( ( *i == curinput .startfield ) && ( ! mubytestart ) )
{
mubytekeep = 0 ;
if ( ( eqtb [27215 ].cint >= 0 ) && ( eqtb [27215 ].cint < 256 ) ) {
if ( mubyteread [eqtb [27215 ].cint ]!= -268435455L )
{
mubytestart = true ;
mubyteskip = -1 ;
p = mubyteread [eqtb [27215 ].cint ];
goto lab22 ;
}
}
}
lab20: mubytestart = false ;
if ( ( mubyteread [buffer [*i ]]== -268435455L ) || ( mubytekeep > 0 ) )
{
if ( mubytekeep > 0 )
decr ( mubytekeep ) ;
return Result ;
}
p = mubyteread [buffer [*i ]];
lab22: if ( mem [p ].hh.b0 >= 64 )
{
lasttype = mem [p ].hh.b0 - 64 ;
p = mem [p ].hh .v.RH ;
mubytetoken = mem [p ].hh .v.LH ;
lastfound = mubyteskip ;
}
...
get_x_token
我的建议是,如果你正在查看源代码以了解或调试(或者更确切地说,查找错误),那么最好先从 LuaTeX(用 C 编写,但首先从 WEB 手动翻译而来:例如这里)或其他非 WEB 重新实现— 它们可能没有 TeX Live 的所有额外功能,但它们应该更容易使用。