1952 Hopper A-0 編譯器#
案發現場#
1952 年,哈佛大學畢業的數學博士 Grace Hopper(葛麗絲·霍普)在美國海軍的 UNIVAC 電腦前,面對一個所有人都視為理所當然、她卻認為錯誤的「常識」:
「電腦只懂機器碼,所以人類必須用機器碼寫程式。」
案發現場的困境極為具體。1950 年代初的程式設計師,每天的工作是這樣的:把數學公式拆解成機器碼指令(一串二進位數字或助憶碼),手工查表分配記憶體位址,然後把指令一行一行打進機器。寫一個平方根副程式要幾天;更痛苦的是重複勞動——每個程式都要重寫同樣的「讀入資料、除錯、輸出」例行程序。Hopper 觀察到:UNIVAC 的程式設計師有一本「副程式簿」,裡面抄著常用的副程式碼,每次寫新程式就手工抄錄進去。她問了一個石破天驚的問題:
「為什麼不讓電腦自己把副程式抄進去?」
這個問題在當年是異端邪說。大多數工程師相信:機器是精確的,讓機器「寫」程式會引入錯誤;編譯是浪費機器時間的事(機器時間極為昂貴);真正的工程師就該用機器碼。Hopper 的老闆甚至告訴她:「電腦只能做算術,不能寫程式。」
同時期,英國曼徹斯特大學的 Kilburn 團隊在 1949 年實作了 Short Code 的前身(Booth 的符號組合),美國的 John Mauchly 也在 1949 年提出了 Short Code——一個用「簡短符號」寫數學式、再由機器解譯執行的系統。一場「機器翻譯」的革命正在醞釀,而 Hopper 將成為它的總工程師。
偵查過程#
Hopper 的偵查思路,是把「翻譯」這件事拆解成三個層次的推理。
第一層推理:副程式 = 可命名的資產。 Hopper 的第一個洞察來自她的數學背景:副程式就像數學中的函數 \(f(x)\) ——只要定義一次,就可以無限次呼叫。她把每個常用副程式編碼成一個短代碼(short code):例如 sqrt 對應代碼 SQ,sin 對應 SN。程式設計師只需寫 SQ X(對 X 開根號),機器會自動展開成完整的機器碼。她的第一個系統 A-0(1952)就是這樣的「副程式組合器」:
SQ, X # 呼叫平方根副程式,作用於 X
SN, Y # 呼叫正弦副程式,作用於 Y
A-0 的操作流程:程式設計師寫一連串短代碼(打在紙帶上),A-0 編譯器讀入後,到「副程式庫」中找出對應的機器碼,自動配置記憶體位址、自動串接,產出完整的可執行程式。這是歷史上第一個編譯器(assembler-compiler)。用今天的語言說,A-0 做了兩件事:符號解析(組譯)+ 副程式連結(linking)——這兩件事至今仍是所有編譯工具鏈的核心步驟。
第二層推理:從「翻譯副程式」到「翻譯數學式」。 A-0 成功後,Hopper 發現了更深層的問題:就算副程式可以自動串接,程式設計師還是要把數學公式手工拆解成副程式呼叫。例如公式
工程師要手工拆成「先算 \(x^2\) 、再乘 \(a\) 、再算 \(b \cdot x\) 、再加 \(c\) 」的一連串呼叫。Hopper 問:為什麼不讓機器直接看懂整條公式?
這引出了她的第二個系統 A-2(1953)與後來的 FLOW-MATIC(1955):一個能直接處理「接近人類語言」的編譯器。FLOW-MATIC 的革命性在於:它用英文單字寫程式:
COMPARE PRODUCT NUMBER (A) WITH PRODUCT NUMBER (B).
IF GREATER GO TO OPERATION 10.
READ INVENTORY FILE (A).
ADD QUANTITY SOLD TO TOTAL SOLD.
這段程式,今天的程式設計師一眼就能看懂。FLOW-MATIC 證明了一件事:程式語言的語法可以接近人類語言,而機器仍然能翻譯執行它。 這個信念直接催生了 COBOL(1959)——Hopper 是 COBOL 的精神母親,COBOL 的語法大量繼承自 FLOW-MATIC。
第三層推理(核心技術推導):編譯器的三段式架構。 Hopper 從 A-0 到 FLOW-MATIC 的演進中,推導出了編譯器的標準架構,這個架構至今仍是教科書內容:
- 詞法分析(Lexing):把輸入的字符流切成「詞」(token)。例如
ADD QUANTITY TO TOTAL被切成ADD、QUANTITY、TO、TOTAL四個 token。Hopper 用「查表法」實作:對每個單字查副程式庫,找到就替換成機器碼。 - 語義處理(Expansion):把每個 token 展開成對應的機器碼片段,並自動分配位址。這是最關鍵的創新:程式設計師不再需要手工分配記憶體,編譯器會追蹤每個變數的位置。Hopper 發明了「符號位址表」(symbol table)——變數名到記憶體位址的對照表。
- 組裝輸出(Assembly):把所有機器碼片段串接成完整程式,處理跳躍位址的修正。
用現代 Python 重現這個三段式架構:
def compile_flow_matic(source, subroutine_lib):
"""重現 Hopper 的編譯器架構:Lexing -> Expansion -> Assembly"""
tokens = source.split() # 第一段:詞法分析
code, symtab, addr = [], {}, 0
for tok in tokens:
if tok in subroutine_lib: # 第二段:副程式展開
frag = subroutine_lib[tok]
code.append((addr, frag)); addr += len(frag)
elif tok not in ('TO', 'THE', 'A', 'B'): # 跳過虛詞
if tok not in symtab: # 自動分配位址
symtab[tok] = addr; addr += 1
code.append((addr, f"STORE {tok}"))
return code, symtab # 第三段:組裝輸出
注意其中一個巧妙的細節:Hopper 發現 FLOW-MATIC 程式裡的英文虛詞(TO、THE、OF)對機器沒有意義,可以直接跳過。這就是「語法糖」的最早實踐——人類語言的冗餘,讓程式更好讀,機器則無視之。
第四層推理:FLOW-MATIC 的商業計算革命。 Hopper 把 FLOW-MATIC 用在真正的商業任務上:薪資計算、庫存管理、帳務處理。她證明了編譯器的經濟價值:一個熟練的程式設計師用機器碼一天寫 15 行,用 FLOW-MATIC 一天能寫 100+ 行——生產力提升近十倍。這個數據,說服了整個產業。1959 年,當國防部召開會議制定 COBOL 規格時,Hopper 的 FLOW-MATIC 是唯一的實作範例,COBOL 的「英文式語法」直接來自她。
並行偵查:Short Code 的解譯器路線。 同時期還有另一條路線:Mauchly 的 Short Code(1949,在 UNIVAC 上由 Betty Holberton 實作)不採用「編譯」,而是逐句解譯——機器每次讀到一個短碼,就現場執行對應操作,不產生中間程式。Short Code 的代碼極簡:
X0 = (Y0 + Z0) / A0 # 寫成短碼:
00 X0 07 Y0 07 Z0 05 A0
其中 07 代表加、05 代表除。解譯器路線的優點是簡單、即時;缺點是每次執行都要重新解譯,比編譯慢 50 倍。這條路線最終演化成今天的 Python、JavaScript(直譯語言),而 Hopper 的編譯路線演化成 C、COBOL、FORTRAN(編譯語言)。編譯 vs 解譯的兩大路線之爭,在 1950 年代初就已經定調。
結案報告#
Grace Hopper 的遺產,改寫了整個軟體產業的命運:
- 第一個編譯器:A-0(1952)是史上第一個編譯器,「讓機器寫程式」從異端變成正統。
- COBOL 之母:FLOW-MATIC 的英文式語法直接成為 COBOL 的藍本。COBOL 至今仍支撐著全球銀行、保險、政府的核心系統——世界上仍有數百億行 COBOL 在執行。
- 符號位址表與自動記憶體分配:編譯器的核心技術,從 A-0 傳到 1957-FORTRAN.md 的優化編譯器,再到今天的所有編譯器。
- 編譯 vs 解譯的兩大路線:她的編譯路線與 Short Code 的解譯路線,定調了後續所有語言的分類——1958-LISP.md 走解譯路線,C 走編譯路線,今天的 Python/JVM 則是兩者混合。
- 「除錯」一詞的普及:1947 年她的團隊在 Harvard Mark II 中發現一隻飛蛾卡在繼電器裡造成故障,把它貼在工作日誌上寫道「First actual case of bug being found」——「bug(蟲)」與「debugging(除蟲)」從此成為程式設計的標準用語。
- 海軍少將的傳奇:Hopper 一直服務到 1986 年以 79 歲高齡退役,是美國海軍史上最年長的現役軍官。她終身推廣「程式應該用人類語言寫」的信念,並在無數演講中傳遞一句名言:
「人類最危險的一句話是:『我們一直都是這樣做的。』」
證據與工具#
證據一:FLOW-MATIC 風格編譯器(Python 完整重現)
LIB = {
'ADD': ['LOAD A', 'ADD B'],
'COMPARE':['LOAD A', 'CMP B'],
'READ': ['IO READ'],
}
def compile_flow_matic(source):
tokens = source.split()
code, symtab, addr = [], {}, 0
stopwords = {'TO', 'THE', 'OF', 'A', 'B', 'WITH', 'FILE', '.'}
for tok in tokens:
if tok in LIB: # 副程式展開
for instr in LIB[tok]:
code.append((addr, instr)); addr += 1
elif tok in stopwords or tok.isdigit():
continue # 虛詞跳過
else:
if tok not in symtab:
symtab[tok] = addr; addr += 1
code.append((addr, f'SYMB {tok}'))
return code, symtab
src = "COMPARE PRODUCT NUMBER WITH QUANTITY ADD TOTAL"
code, symtab = compile_flow_matic(src)
for addr, instr in code:
print(f"{addr:03d} {instr}")
print("符號表:", symtab)
證據二:Short Code 解譯器(Python)
# Short Code 的代碼表(簡化自 1949 UNIVAC 版本)
OPCODE = {'07': lambda a,b: a+b, '05': lambda a,b: a/b,
'06': lambda a,b: a*b, '02': lambda a,b: a-b}
def short_code_interpret(program, mem):
"""Short Code:逐句解譯,不產生中間程式"""
mem = dict(mem)
i = 0
while i < len(program):
op = program[i]
if op in OPCODE: # op arg1 arg2 result
a, b, dest = program[i+1], program[i+2], program[i+3]
mem[dest] = OPCODE[op](mem[a], mem[b])
print(f"{op} {a} {b} -> {dest} = {mem[dest]}")
i += 4
else:
i += 1
return mem
# 計算 (Y + Z) / A,Y=10, Z=20, A=3
mem = {'X0': 0, 'Y0': 10, 'Z0': 20, 'A0': 3}
prog = ['06','Y0','A0','T0', # T0 = Y * A (先通分,僅示範流程)
'07','Z0','T0','T1', # T1 = Z + T0
'05','T1','A0','X0'] # X0 = T1 / A
result = short_code_interpret(prog, mem)
print("X0 =", result['X0'])
證據三:編譯 vs 解譯的效能對比
import time
def compiled_runtime(n=100000):
"""編譯路線:翻譯一次,執行 n 次都是機器速度"""
t0 = time.perf_counter()
total = 0
for _ in range(n):
total += 2 * 3 + 1 # 「編譯後」的常數已可優化
return time.perf_counter() - t0
def interpreted_runtime(n=1000):
"""解譯路線:每次執行都要重新解析"""
t0 = time.perf_counter()
total = 0
for _ in range(n):
total = eval("2 * 3 + 1") # eval = 每次重新「解譯」
return time.perf_counter() - t0
c, i = compiled_runtime(), interpreted_runtime()
print(f"編譯式(10萬次): {c:.4f}s, 解譯式(1千次): {i:.4f}s")
print(f"即使解譯只跑了 1/100 的次數,仍比編譯式慢 —— 印證 Short Code 比 A-0 慢 50 倍的歷史紀錄")
三份證據串成 Hopper 的完整遺產:編譯器三段式架構(證據一)、Short Code 解譯路線(證據二)、編譯優於解譯的效能證明(證據三)。「程式應該用人類語言寫」——這個信念,讓程式設計從工程師的特權,變成了千萬人的日常。