開放電腦計畫 (11) – 中間碼轉組合語言編譯器:使用 node.js + javascript 實作 (作者:陳鍾誠)
前言
在上一期當中我們介紹了 j0c 這個編譯器的設計方式,並且設計了一種稱為 ir0 的中間碼格式,用來做為 j0c 編譯器的輸出格式。
在本文中,我們將介紹一個可以將中間碼 ir0 格式轉換成 CPU0 組合語言 (as0) 的程式,該程式稱為 ir2as0 ,這樣才能接上先前的 as0 組譯器,成為一套完整的工具鏈。
轉換程式
以下是這個轉換程式的原始碼,該程式會將 ir0 格式的中間碼,轉換成 as0 格式的組合語言。
檔案:ir2as.js
// ir2as0 中間碼轉換為組合語言,用法範例: node ir2as0 test.ir0 > test.as0
var fs = require("fs");
var util = require("util");
var format = util.format; // 字串格式化
var log = console.log; // 將 console.log 名稱縮短一點
// 讀入中間檔,並分割成一行一行的字串
var lines = fs.readFileSync(process.argv[2], "utf8").split("\n");
// 輸出組合語言
var asm=function(label, op, p, p1, p2) {
var asCode = format("%s\t%s\t%s\t%s\t%s", label, op, p, p1, p2);
log(asCode);
}
var cmpCount = 0; // 比較運算的標記不可重複,故加上一個 counter 以玆區分
// 將一行中間碼 line 轉換為組合語言
function ir2as(line) {
var tokens = line.split("\t"); // 將中間碼分割成一個一個的欄位
var label = tokens[0]; // 取出標記 label
var iop = tokens[1], aop=""; // 取出運算 iop
var p = tokens.slice(2); // 取出參數部份
if (label !== "") // 若有標記,直接輸出一行只含標記的組合語言
asm(label, "", "", "", "");
switch (iop) { // 根據運算 iop 的內容,決定要轉成甚麼組合語言
case "=": // 範例:= X Y 改為 LD R1, Y; ST R1, X
asm("", "LD", "R1", p[1], "");
asm("", "ST", "R1", p[0], "");
break;
// 範例: + X A B 改為 LD R1, A; LD R2, B; ADD R3, R1, R2; ST R3, X;
case "+": case "-": case "*": case "/": case "<<":
asm("", "LD", "R1", p[1], "");
asm("", "LD", "R2", p[2], "");
aop = {"+":"ADD", "-":"SUB", "*":"MUL", "/":"DIV"}[iop];
asm("", aop, "R3", "R1", "R2");
asm("", "ST", "R3", p[0], "");
break;
// 範例: ++ X 改為 LDI R1, 1; LD R2, X; ADD R2, R1, R2; ST R2, X;
case "++": case "--":
asm("", "LDI", "R1", "1", "");
asm("", "LD", "R2", p[0], "");
aop = {"++":"ADD", "--":"SUB" }[iop];
asm("", aop, "R2", "R1", "R2");
asm("", "ST", "R2", p[0]);
break;
// 範例: < X, A, B 改為 LD R1, A; LD R2, B; CMP R1, R2; JLT CSET0; LDI R1, 1; JMP EXIT0; CSET0: LDI R1, 0; CEXIT0: ST R1, X
case "<": case "<=": case ">": case ">=": case "==": case "!=":
asm("", "LD", "R1", p[1], "");
asm("", "LD", "R2", p[2], "");
asm("", "CMP", "R1", "R2", "");
aop = {"<":"JLT", "<=":"JLE", ">":"JGT", ">=":"JGE", "==":"JEQ", "!=":"JNE"}[iop];
asm("", aop, "CSET"+cmpCounter, "", "");
asm("", "LDI", "R1", "1", "");
asm("", "JMP", "CEXIT"+cmpCounter, "", "");
asm("CSET"+cmpCount, "LDI", "R1", "0", "");
asm("CEXIT"+cmpCount, "ST", "R1", p[0], "");
break;
// 範例: call X, F 改為 CALL F; ST R1, X;
case "call":
asm("", "CALL", p[1], "", "");
asm("", "ST", "R1", p[0], "");
break;
// 範例: arg X 改為 LD R1, X; PUSH R1;
case "arg":
asm("", "LD", "R1", p[0], "");
asm("", "PUSH","R1", "", "");
break;
case "function": // 範例: sum function 只生成標記 sum,沒有生成組合語言指令
break;
case "endf": // 函數結束,沒有生成組合語言指令
break;
case "param": // 範例: param X 改為 POP R1; ST R1, X;
asm("", "POP", "R1", "", "");
asm("", "ST", "R1", p[0], "");
break;
case "return": // 範例: return X 改為 LD R1, X; RET;
asm("", "LD","R1", p[0], "");
asm("", "RET","", "", "");
break;
case "if0": // 範例: if0 X Label 改為 CMP R0, X; JEQ Label;
asm("", "CMP","R0", p[0], "");
asm("", "JEQ",p[1], "", "");
break;
case "goto": // 範例: goto Label 改為 JMP label
asm("", "JMP", p[0], "", "");
break;
case "array": // 範例: X array 改為 LD R1, X; CALL ARRAY; (註: X=new array())
asm("", "LD", "R1", p[0], "");
asm("", "CALL", "ARRAY", "", "");
break;
case "[]": // 範例: [] X A i 改為 LD R1, A; LD R2, i; CALL AGET; ST R1, X (註: X=A[i])
asm("", "LD", "R1", p[1], "");
asm("", "LD", "R2", p[2], "");
asm("", "CALL", "AGET", "", "");
asm("", "ST", "R1", p[0], "");
break;
case "length": // 範例: length len, A 改為 LD R1, A; CALL ALEN; ST R1, len;
asm("", "LD", "R1", p[1], "");
asm("", "CALL", "ALEN", "", "");
asm("", "ST", "R1", p[0], "");
break;
case "apush": // 範例: apush A, X 改為 LD R1,A; LD R2, X; CALL APUSH
asm("", "LD", "R1", p[0], "");
asm("", "LD", "R2", p[1], "");
asm("", "CALL", "APUSH", "", "");
break;
case "table": // 範例: table T 改為 LD R1,T; CALL TABLE
asm("", "LD", "R1", p[0], "");
asm("", "CALL", "TABLE", "", "");
break;
case "map": // 範例: map table field value 改為 LD R1, table; LD R2, field; LD R3, value; CALL TMAP
asm("", "LD", "R1", p[0], "");
asm("", "LD", "R2", p[1], "");
asm("", "LD", "R3", p[2], "");
asm("", "CALL", "TMAP", "", "");
break;
case "":
break;
default:
log("Error : %s not found!", iop);
}
}
// 將所有中間碼都轉換為組合語言
for (var i in lines) {
if (lines[i].trim().length > 0) {
log("// %s", lines[i]);
ir2as(lines[i]);
}
}
執行結果
首先我們要使用 j0c 編譯器將 j0 語言的程式,編譯為 ir0 的中間碼格式。然後再利用 ir2as0 將中間碼轉換成 CPU0 的組合語言,以下是一個將 test.j0 編譯 test.ir0 中間檔,然後再利用 ir2as0 將中間檔轉換為 test.as0 組合語言的過程。
C:\Dropbox\Public\web\oc\code\js>node j0c test.j0 > test.ir0
C:\Dropbox\Public\web\oc\code\js>node ir2as0 test.ir0 > test.as0
以下是 test.j0 => test.ir0 => test.as0 這個編譯轉換過程當中的檔案內容。
高階語言檔: test.j0
s = sum(10);
function sum(n) {
s = 0;
i=1;
while (i<=10) {
s = s + i;
i++;
}
return s;
}
m = max(3,5);
function max(a, b) {
if (a > b)
return a;
else
return b;
}
function total(a) {
s = 0;
for (i in a) {
s = s + a[i];
}
return s;
}
a = [ 1, 3, 7, 2, 6];
t = total(a);
word = { e:"dog", c:"狗" };
中間碼檔案: test.ir0
arg 10
call T1 sum
= s T1
sum function
param n
= s 0
= i 1
L1
<= T2 i 10
if0 T2 L2
+ T3 s i
= s T3
++ i
goto L1
L2
return s
endf
arg 3
arg 5
call T4 max
= m T4
max function
param a
param b
> T5 a b
if0 T5 L3
return a
L3
return b
endf
total function
param a
= s 0
= i 0
L4 length T6 a
< T7 i T6
if0 T7 L5
[] T8 a i
+ T9 s T8
= s T9
goto L4
L5
return s
endf
array T10
apush T10 1
apush T10 3
apush T10 7
apush T10 2
apush T10 6
= a T10
arg a
call T11 total
= t T11
table T12
map T12 e "dog"
map T12 c "狗"
= word T12
組合語言檔: test.as0
// arg 10
LD R1 10
PUSH R1
// call T1 sum
CALL sum
ST R1 T1
// = s T1
LD R1 T1
ST R1 s
// sum function
sum
// param n
POP R1
ST R1 n
// = s 0
LD R1 0
ST R1 s
// = i 1
LD R1 1
ST R1 i
// L1
L1
// <= T2 i 10
LD R1 i
LD R2 10
CMP R1 R2
JLE CSET0
LDI R1 1
JMP CEXIT0
CSET0 LDI R1 0
CEXIT0 ST R1 T2
// if0 T2 L2
CMP R0 T2
JEQ L2
// + T3 s i
LD R1 s
LD R2 i
ADD R3 R1 R2
ST R3 T3
// = s T3
LD R1 T3
ST R1 s
// ++ i
LDI R1 1
LD R2 i
ADD R2 R1 R2
ST R2 i undefined
// goto L1
JMP L1
// L2
L2
// return s
LD R1 s
RET
// endf
// arg 3
LD R1 3
PUSH R1
// arg 5
LD R1 5
PUSH R1
// call T4 max
CALL max
ST R1 T4
// = m T4
LD R1 T4
ST R1 m
// max function
max
// param a
POP R1
ST R1 a
// param b
POP R1
ST R1 b
// > T5 a b
LD R1 a
LD R2 b
CMP R1 R2
JGT CSET0
LDI R1 1
JMP CEXIT0
CSET0 LDI R1 0
CEXIT0 ST R1 T5
// if0 T5 L3
CMP R0 T5
JEQ L3
// return a
LD R1 a
RET
// L3
L3
// return b
LD R1 b
RET
// endf
// total function
total
// param a
POP R1
ST R1 a
// = s 0
LD R1 0
ST R1 s
// = i 0
LD R1 0
ST R1 i
// L4 length T6 a
L4
LD R1 a
CALL ALEN
ST R1 T6
// < T7 i T6
LD R1 i
LD R2 T6
CMP R1 R2
JLT CSET0
LDI R1 1
JMP CEXIT0
CSET0 LDI R1 0
CEXIT0 ST R1 T7
// if0 T7 L5
CMP R0 T7
JEQ L5
// [] T8 a i
LD R1 a
LD R2 i
CALL AGET
ST R1 T8
// + T9 s T8
LD R1 s
LD R2 T8
ADD R3 R1 R2
ST R3 T9
// = s T9
LD R1 T9
ST R1 s
// goto L4
JMP L4
// L5
L5
// return s
LD R1 s
RET
// endf
// array T10
LD R1 T10
CALL ARRAY
// apush T10 1
LD R1 T10
LD R2 1
CALL APUSH
// apush T10 3
LD R1 T10
LD R2 3
CALL APUSH
// apush T10 7
LD R1 T10
LD R2 7
CALL APUSH
// apush T10 2
LD R1 T10
LD R2 2
CALL APUSH
// apush T10 6
LD R1 T10
LD R2 6
CALL APUSH
// = a T10
LD R1 T10
ST R1 a
// arg a
LD R1 a
PUSH R1
// call T11 total
CALL total
ST R1 T11
// = t T11
LD R1 T11
ST R1 t
// table T12
LD R1 T12
CALL TABLE
// map T12 e "dog"
LD R1 T12
LD R2 e
LD R3 "dog"
CALL TMAP
// map T12 c "狗"
LD R1 T12
LD R2 c
LD R3 "狗"
CALL TMAP
// = word T12
LD R1 T12
ST R1 word
結語
截至目前為止,我們已經為開放電腦計畫實作了一組簡單的工具鏈,包含用 node.js + javascript 設計的 j0c 編譯器、ir2as0 中間碼轉換器、 as0 組譯器、vm0 虛擬機、以及用 Verilog 設計的 CPU0, MCU0 處理器等等。
這套工具鏈的設計都是以「簡單易懂」為原則,採用 Keep It Simple and Stupid (KISS) 的原則,希望能透過這樣的方式,揭露電腦的各個設計層面,讓讀者可以透過開放電腦計畫理解電腦從軟體到硬體的設計原理。
不過、我們還沒有完成整個計畫,開放電腦計畫顯然還有些缺憾,像是我們還沒有設計作業系統 (OS),也沒有用 Verilog 設計開放電腦的週邊裝置電路,另外在 FPGA 實際燒錄也只有很簡單的範例程式,還沒辦法形成一套從軟體到硬體串接的很完整的系統。
因此、我們打算在 2014 年暑假在成大與蘇文鈺老師一起舉辦一個「開放FPGA電腦創世紀黑客松」,我們已經為這個活動建立了一個 facebook 社團,歡迎對「開放電腦計畫」或 FPGA 有興趣的朋友們,一起來參與這個活動,以下是該社團的網址:
歡迎大家一同來參加!