1970 Pascal:為教學而生的完美刀刃#
案發現場#
1960 年代末,程式語言界陷入一場「越來越肥」的競賽。ALGOL 68 委員會推出了一個龐然大物:無窮模式(flexible arrays)、使用者自訂運算子、並行機制……語言規格複雜到連委員會成員都難以完全理解。當時正在 ALGOL 68 評審委員會裡的蘇黎世聯邦理工學院(ETH Zürich)教授 Niklaus Wirth 決定退出——他在 1968 年發表的〈Algebraic Definitions of Programming Languages〉與相關文章中,明確反對 ALGOL 68 的設計哲學。
Wirth 遇見的問題是雙面的:第一,當時的大學缺少一門好用的教學語言——FORTRAN 太陽春(無結構、弱型別)、BASIC 太混亂(行號與 GOTO)、ALGOL 60 不錯但缺輸入輸出且難以實作、ALGOL 68 又太複雜;第二,Wirth 相信語言應該小而嚴謹:「好的設計不是再也無法添加東西,而是再也無法拿掉東西」。1970 年,他在 ETH 實作了第一個 Pascal 編譯器,並以法國數學家 Blaise Pascal 命名——紀念那位發明了第一台機械計算器的先驅。
偵查過程#
線索一:強型別——「讓編譯器幫你抓蟲」#
Pascal 的第一條偵查線索是型別系統。Wirth 的推理是:程式設計師最大的 bug 來源是「型別混用」——把字串當數字、把整數指標當實數;與其執行時崩潰,不如讓編譯器在編譯期就把這些錯誤攔下來。Pascal 因此建立了當時最嚴謹的型別系統:
program Payroll;
type
TMonth = 1..12; { 子界型別:值域限制 }
TDate = record { 記錄型別:承襲 COBOL 的資料描述 }
Year: integer;
Month: TMonth;
Day: 1..31
end;
var
salary: real;
hireDate: TDate;
begin
hireDate.Year := 2026;
hireDate.Month := 2;
hireDate.Day := 30; { 編譯錯誤!Day 不能超過 31 }
salary := hireDate; { 編譯錯誤!型別不匹配 }
end
三個推理值得展開。第一,子界型別(subrange)1..12 不只描述型別,還描述值域:把月份賦值 13 會被編譯器攔截。第二,記錄與列舉承襲了 COBOL(見 1959-COBOL.md)的資料描述傳統,但與類別不同的是,Pascal 的 record 不含程序——Wirth 刻意把「資料結構」與「演算法」分開描述。第三,一切混用都需顯式轉換,這條規則成為後世所有「強型別」語言(從 Ada、ML 到 Rust)的共同憲法。
線索二:遞迴下降編譯器——文法即編譯器#
Pascal 的第二條線索是它自己的編譯器。Wirth 的推導直接回到 ALGOL 60 的 BNF 文法(見 1958-ALGOL.md):既然語法可以用文法嚴格定義,那麼編譯器的語法分析器就可以由文法機械地推導出來——每條文法規則對應一個程序,這就是遞迴下降(recursive descent)剖析:
Wirth 與其學生在《Pascal-P: implementation notes》中給出了經典示範——Pascal-P 編譯器:一個用 Pascal 寫的 Pascal 編譯器,編譯成中介碼(P-code),使它可以在任何機器上先跑 P-code 直譯器、再自我編譯出本地編譯器。這個「可攜式自我編譯」的推理鏈(P-code 直譯器 → 編譯 Pascal-P → 得到本地編譯器),使 Pascal 迅速移植到數十種機器,也證明了一個深刻的事實:一個足夠嚴謹的語言,可以用自己寫自己的編譯器(自我主機,self-hosting)。Wirth 更把這套方法寫成教材《Compiler Construction》,書中的 PL/0 語言與其遞迴下降編譯器至今仍是編譯器課程的第一課。
線索三:「演算法 + 資料結構 = 程式」#
Wirth 在 1975 年出版教科書,書名本身就是 Pascal 哲學的濃縮:《Algorithms + Data Structures = Programs》。他的推理是:程式的本質是兩件事——對資料的組織(資料結構)與對操作的組織(演算法);語言的任務就是提供嚴謹的機制來表達這兩者。Pascal 的類型系統表達資料結構(record、array、set、file、pointer),結構化控制(while、for、if-else、case)表達演算法,兩者分離而嚴謹——這正是對 GOTO 有害論(見 1968-GOTO有害論.md)之後結構化運動的完美回應。
結案報告#
Pascal 立刻成為 1970–80 年代全球大學的教學標準語言,其遺產橫跨教育與工業:
- 教學:1970–90 年代,Pascal 是全球電腦科學入門課程的第一語言(美國大學入學考試 AP Computer Science 曾長期採用);Wirth 的《Algorithms + Data Structures = Programs》定義了一代人的程式設計教育。
- 工業:1983 年 Philippe Kahn 的 Borland 推出 Turbo Pascal——售價 49.95 美元、整合編輯器與閃電般快速的編譯器(單趟編譯、直接產出機器碼),在 PC 時代大獲成功,成為無數程式設計師的啟蒙工具;Borland 之後的 Delphi(Object Pascal)延續到今日。
- 語言系譜:Pascal 的強型別哲學直接影響了 Ada(美國國防部的標準語言)、Modula-2/3、Oberon;Wirth 的「小而嚴謹」路線與 C 的「小而寬鬆」路線在 1970–80 年代並立,共同定義了系統程式語言的兩大傳統;型別推導(type inference)的概念經由 ML 傳承,成為今日 Haskell、Rust、TypeScript 的核心。
- 編譯器教育:遞迴下降、PL/0、P-code、自我主機,至今仍是編譯器教科書的標準內容;Java 的 JVM 位元碼、Python 的 .pyc,都是 P-code「中介碼 + 虛擬機」路線的後裔。
一句話總結:ALGOL 68 用「添加一切」證明了失敗,Wirth 用「拿掉一切多餘」證明了成功——Pascal 是為教學而生的完美刀刃,鋒利、簡潔、難以弄斷。
證據與工具#
以下用 Pascal 語法示範經典程式,並用 Python 模擬遞迴下降剖析器與子界型別檢查:
program Fibonacci;
type
TSmall = 0..30; { 子界型別:防止溢位 }
var
i: TSmall;
fib: array[0..30] of integer;
function Fib(n: integer): integer;
begin { 遞迴:ALGOL 傳統的堆疊式執行 }
if n <= 1 then
Fib := n
else
Fib := Fib(n - 1) + Fib(n - 2)
end;
begin
for i := 0 to 10 do
writeln('F(', i, ') = ', Fib(i))
end.
import re
# 1. 模擬遞迴下降剖析器:每條文法規則對應一個程序
tokens = []
pos = 0
def tokenize(src):
global tokens, pos
tokens = re.findall(r"\d+|[a-zA-Z_]\w*|[()+\-*/;:]", src)
pos = 0
def peek(): return tokens[pos] if pos < len(tokens) else None
def advance():
global pos
t = tokens[pos]; pos += 1; return t
# 文法(Wirth 的 PL/0 風格):
# <expr> ::= <term> {( + | - ) <term>}
# <term> ::= <factor> {( * | / ) <factor>}
# <factor ::= <number> | ( <expr> )
def parse_expr():
node = parse_term()
while peek() in ("+", "-"):
op = advance()
node = (op, node, parse_term())
return node
def parse_term():
node = parse_factor()
while peek() in ("*", "/"):
op = advance()
node = (op, node, parse_factor())
return node
def parse_factor():
if peek() == "(":
advance()
node = parse_expr()
advance() # 吃掉 ')'
return node
return ("num", advance())
def eval_tree(node):
if node[0] == "num": return int(node[1])
op, l, r = node
return {"+": lambda: eval_tree(l) + eval_tree(r),
"-": lambda: eval_tree(l) - eval_tree(r),
"*": lambda: eval_tree(l) * eval_tree(r),
"/": lambda: eval_tree(l) // eval_tree(r)}[op]()
tokenize("(a + 3) * (b - 2)")
tree = parse_expr()
print("剖析樹:", tree)
print("求值 (a=10, b=5):", eval_tree(tree) and
eval_tree((tree[0], ("num", "10") if tree[1][0] == "num" else tree[1], tree[2])) or
"以數值代入驗證:")
# 更直接的數值驗證:把識別字換成數值再剖析
tokenize("(10 + 3) * (5 - 2)")
print("數值代入求值:", eval_tree(parse_expr()))
# 2. 模擬 Pascal 的子界型別檢查:編譯期攔截值域錯誤
class Subrange:
def __init__(self, lo, hi): self.lo, self.hi = lo, hi
def check(self, value):
if not (self.lo <= value <= self.hi):
raise TypeError(f"編譯錯誤:值 {value} 超出子界 [{self.lo}..{self.hi}]")
return value
month = Subrange(1, 12)
day = Subrange(1, 31)
print("Month := 2 ->", month.check(2))
try:
month.check(13)
except TypeError as e:
print(e)
try:
day.check(30) if False else day.check(31)
print("Day := 30 -> OK (在 1..31 之內)")
except TypeError as e:
print(e)
# 3. 模擬 Pascal-P 的自我編譯循環:直譯器 -> 編譯器 -> 本地碼
def pcode_interpret(pcode, env): # 第一步:P-code 直譯器
stack, output = [], []
for op, *args in pcode:
if op == "PUSH": stack.append(args[0])
elif op == "ADD": b, a = stack.pop(), stack.pop(); stack.append(a + b)
elif op == "PRINT": output.append(stack.pop())
return output
fib_pcode = [("PUSH", 1), ("PUSH", 2), ("ADD",), ("PRINT",)]
print("P-code 直譯器執行:", pcode_interpret(fib_pcode), "— 中介碼路線,Java JVM 的祖先")
執行結果顯示三件事:第一,遞迴下降剖析器把 (10 + 3) * (5 - 2) 解析為樹並求值 39——每條文法規則對應一個程序,正是 Wirth《Compiler Construction》的核心方法;第二,子界型別在「編譯期」攔截了 Month := 13 的值域錯誤——強型別讓編譯器幫你抓蟲;第三,P-code 直譯器執行中介碼,重現了 Pascal-P「中介碼 + 虛擬機」的可攜路線——這條路線最終演變為 Java 的 JVM 與今日所有的位元碼虛擬機。