1973 — UCSD P-system 與 P-code:跨平台虛擬機的語言路線#
案件摘要#
1973 年起,Kenneth Bowles 在 UCSD 開發 Pascal P-system,採用「P-code」中間碼:程式編譯成 P-code,由 P-machine 直譯器執行。語言層的虛擬機(bytecode VM)就此誕生——「一次編譯,到處執行」的第一樁勝訴判例。
前因 -- 為什麼會有這個案子#
- 1970 年代的微型電腦(Apple II、TRS-80、Z80 機器)CPU 各不相同(6502、Z80、8080),記憶體只有數十 KB。
- 每個新平台都要重寫編譯器後端:編譯器是「每台機器一案」的苦差事;Pascal 的完整編譯器在 64KB 機器上根本放不下。
- 線索:把編譯器切成「平台無關的前端 + 極小的平台相關直譯器」?Wirth 的 Pascal-P(1973)已把 Pascal 編成 P-code;Bowles 的洞見是把它包成完整的作業環境(作業系統 + 編譯器 + 直譯器)一起移植。
線索與推理 -- 數學式、程式、理論#
1. P-code:bytecode 的祖先#
P-code(pseudo-code)是一種緊湊的堆疊式中間碼:指令不帶暫存器編號,操作數隱含在堆疊頂端。
\[ \text{source } \xrightarrow{\text{編譯器前端}} \text{P-code} \xrightarrow{\text{P-machine 直譯器}} \text{執行} \]
典型指令(UCSD P-code 風格):
| 指令 | 語意 |
|---|---|
LDC 5 | push 常數 5 |
LDL 0 | push 區域變數 offset 0 |
ADD | pop 兩數相加 push 結果 |
STL 1 | pop 存入區域變數 offset 1 |
JMP n | 無條件跳到 n |
JPF n | 堆疊頂為假則跳到 n |
HALT | 停機 |
2. P-machine 直譯器(理論定義)#
P-machine 是一個抽象機 \(M = \langle S, D, C, \delta \rangle\) :
- \(S\) :求值堆疊(evaluation stack)
- \(D\) :資料區(frame:static link、dynamic link、區域變數、返回位址)
- \(C\) :程式碼區(P-code 序列)
- \(\delta\) :狀態轉移函數, \(\delta: (S, D, C, pc) \to (S', D', C, pc')\)
每步取 `\(C[pc]\)、解碼、執行、更新堆疊與 \(pc\)——這就是 fetch-decode-execute 迴圈,與真實 CPU 同構,只是由軟體實作。
3. 「一次編譯,到處執行」的數學理由#
對任意平台 \(`X\) ,只要有直譯器 \(I_X\) :
\[ \forall X:\ \text{P-code} \xrightarrow{I_X} \text{same semantics} \]
即語意函數等價: \(\text{sem}(\text{src}) = \text{sem}_X(\text{P-code})\ \ \forall X\) 。 移植成本從「重寫整個編譯器後端 \(O(\text{compiler})\) 」降為「寫一個小直譯器 \(O(\text{interpreter})\) 」:
\[ \text{cost}_{new\ platform} \approx |I_X| \ll |C_{backend}| \]
代價:直譯執行比原生慢 5–20 倍( \(t_{interp} \gg t_{native}\) )——這個缺口後來由 JIT 補上。
4. Python 實作:簡單堆疊 VM(P-code 風格)#
def run(code, locals_size=4):
stack, locals_, pc = [], [0] * locals_size, 0
while pc < len(code):
op = code[pc]; pc += 1
if op[0] == "LDC": stack.append(op[1])
elif op[0] == "LDL": stack.append(locals_[op[1]])
elif op[0] == "STL": locals_[op[1]] = stack.pop()
elif op[0] == "ADD": b, a = stack.pop(), stack.pop(); stack.append(a + b)
elif op[0] == "SUB": b, a = stack.pop(), stack.pop(); stack.append(a - b)
elif op[0] == "MUL": b, a = stack.pop(), stack.pop(); stack.append(a * b)
elif op[0] == "JMP": pc = op[1]
elif op[0] == "JPF": pc = op[1] if not stack.pop() else pc
elif op[0] == "PRINT": print(stack.pop())
elif op[0] == "HALT": break
return locals_
# 計算 1+2+...+10 的 P-code:
# i = 0; sum = 0; loop: i = i+1; sum = sum+i; if i<10 goto loop; print sum
code = [
("LDC", 0), ("STL", 0), # i = 0
("LDC", 0), ("STL", 1), # sum = 0
# loop = pc 4
("LDL", 0), ("LDC", 1), ("ADD"), ("STL", 0), # i = i + 1
("LDL", 1), ("LDL", 0), ("ADD"), ("STL", 1), # sum = sum + i
("LDL", 0), ("LDC", 10), ("LT"), ("JPF", 4), # if i < 10 goto loop
("LDL", 1), ("PRINT"),
("HALT",),
]
run(code) # 輸出 55
結案 -- 後果與影響#
- UCSD P-system 被移植到 Apple II、IBM PC、TI-99 等數十種機器,是 1980 年代初最重要的「可攜式作業環境」,Apple 甚至授權內建於 Apple Pascal。
- P-code 的堆疊式設計直接啟發 Java bytecode(JVM,1995)、.NET CIL/IL(2002)、Python
.pyc、Lua VM。 - 「前端/後端分離 + 中間碼」成為編譯器工程標準(LLVM IR 亦屬此譜系)。
- 直譯太慢的缺口,由 Self 的 JIT(1991)、Deutsch–Schiffman(1984)、HotSpot 等補齊——但架構的種子是 P-code 種下的。
關鍵人物與文獻#
- Kenneth Bowles:UCSD P-system 創始人。
- Niklaus Wirth:Pascal 之父,Pascal-P 與 P-code 概念先驅。
- Wirth, The Programming Language Pascal, Acta Informatica, 1971.
- Bowles, Beginner's Guide for the UCSD Pascal System, 1983.
- Nori et al., Pascal-P Implementation Notes, 1975.
- Apple Computer, UCSD Pascal Reference Manual for the Apple II, 1979.