使用 Clojure 打造 4917 微處理器的模擬器

4917 微處理器是 澳大利亞新南威爾斯大學 (UNSW) 教授 Richard Buckland 所開授課程 COMP1917 裡面所講述的一個專為該課程設計的虛擬微處理器,此微處理器並未在市面上販 售。

4917 是一個 4-bit,具有 4 個暫存器以及 16 個記憶體空間 (4bit * 16) 的微控器,屬 於 Von Newman 架構 (程式和資料儲存在同一份記憶體), 和當今電腦、手機使用的 64-bit CPU 相比相差甚遠,但是非常適合用來學習一個 CPU 的運作以及模擬器的撰寫。

在這篇文章中,我們將使用 Clojure 1.7 的新功能 Reader Conditionals 一次實現 node.js (clojurescript) 以及 JVM (clojure) 的版本。

4917 的暫存器 (Register)

4917 共具有 4 個暫存器,分別為 R0, R1, PC, IS, 其用途如下表

暫存器 (Register) 用途
R0 暫存資料用
R1 暫存資料用
PC 儲存目前指向記憶體的位址
IS 儲存目前執行的指令集 (Instruction Set)

有些人的 4917 模擬器會將 PC (Program Counter) 命名為 IP (Instruction Pointer), 這個名稱比較常用在 x86 的 CPU 上,但是用途是相同的。

從指令集認識 4917

4917 共有 16 種指令,其中 0 ~ 7 這一類的指令屬於 1-byte 指令,不接受而外的參數, 而 8 ~ 15 這幾個操作碼則會接受 Program counter (或稱為 Instruction pointer) 所指 向的下一個目標作為資料 <data> 來進行接下來的動作,屬於 2-byte 指令。

實際上 4917 為 4-bit 微處理器,其使用的 memory 或是暫存空間都是以 4-bit (1byte = 8bit) 作為單位的,本文參考部分文章的描述,仍以 byte 當作他的單位。 (實際上 4-bit 稱作 nibble)

下表列出了其指令與含義:

1-byte 指令

指令 (op code) 含義
0 結束程式
1 R0 和 R1 相加,結果存入 R0
2 R0 和 R1 相減,結果存入 R0
3 R0 數值加一
4 R1 數值加一
5 R0 數值減一
6 R1 數值減一
7 響蜂鳴器

2-byte 指令,指令後面的 byte 的內容以 <data> 表示

指令 (op code) 含義
8 印出 <data> 內容
9 從位址 <data> 讀取資料存入 R0
10 從位址 <data> 讀取資料存入 R1
11 將 R0 內容存入 位址 <data> 內
12 將 R1 內容存入 位址 <data> 內
13 將 PC 指向 位址 <data> (即 JUMP 命令)
14 如果 R0 為 0,將 PC 指向 位址 <data>
15 如果 R0 不為 0, 將 PC 指向 位址 <data>

從範例來看 4917 的指令運作

我們可以使用簡單的範例來了解 4917 的運作,假設目前的記憶體內容如下:

這樣 4917 會怎樣運作呢? 首先因為 PC 指向了 8,而 8 是 2-byte 指令,代表印出下一 個位置 (PC + 1) 的內容,因此會在終端機上顯示 5 ,接著程式遇到了 0, 結束這一回 合。

很簡單對不對?我們來看比較難一點點的範例,假設目前記憶體內容如下:

首先剛開始的指令 9 是 2-byte 指令,因此我們必須將 13 當作其參數一起考量,而 [ 9, 13 ] 則代表著

R0 = memory[13] = 6

而接下來的命令 10 同樣也是 2-byte 指令,因此必須將 3 當作其參數一起來看,[ 10, 3 ] 則代表著

R1 = memory[3] = 3

再接下來我們碰到了 1 則是 1-byte 命令,因此目前運作如下

R0 = R0 + R1 = 6 + 3 = 9

再接下來我們碰到了 3 是 1-byte 命令,因此目前運作如下

R0 = R0 + 1 = 9 + 1 = 10

在接著則是命令 11,這是一個 2-byte 命令,會將 R0 的內容寫入到其參數 (9) 所在的記 憶體位址,因此經過這個命令後,記憶體變成如下

最後一個遇到的命令是 8 代表將下一個資料印出來,因此我們就會看到 10 顯示在終端機 上了,也就是說,剛剛的程式進行了以下的運作

R0 = 6
R1 = 3
R0 = R0 + R1
R0 = R0 + 1
print R0

建立我們的 Clojure/Clojurescript 專案

了解到了 4917 的運作模式以及指令集後,我們可以開始寫程式囉,首先使用 lein 建立我們的專案

coldnew@Rosia ~ $ lein new emulator-4917

由於預設的 lein 專案缺少很多東西,因此我們必須一一添加 (或是你可以使用比較合適的 樣板),首先在 project.clj 裡面修改部分設定成如下

(defproject emulator-4917 "0.1.0-SNAPSHOT"
  ;; skip ...

  :source-paths ["src"]

  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/clojurescript "0.0-3308" :scope "provided"]]

  :plugins [[lein-cljsbuild "1.0.6"]]

  :min-lein-version "2.5.1"

  :cljsbuild {:builds
              [{
                :source-paths ["src"]
                :compiler {:output-to "target/emulator-4917.js"
                           :output-dir "target"
                           :source-map "target/emulator-4917.js.map"
                           :target :nodejs
                           :optimizations :none
                           :pretty-print true}}]}
  :aot [emulator-4917.core]
  :main emulator-4917.core)

請注意到由於本篇文章將使用 Clojure 1.7 的新功能 Reader Conditionals ,因此 Clojure/Clojurescript 版本必須依照以上設定或選用更高版本,並且 lein 版本也必須 升級到最新版 2.5.1.

而在這個 project.clj 裡面,由於我們為了讓 Clojurescript 編譯速度加快,我們採用 了 none 最佳化,因此必須另外增加一個 run.js 來協助執行編譯出來的 javascript, 其內容如下

// http://stackoverflow.com/questions/25803420/how-to-compile-clojurescript-to-nodejs
try {
    require("source-map-support").install();
} catch(err) {
}
require("./target/goog/bootstrap/nodejs.js");
require("./target/emulator-4917.js");
require("./target/emulator_4917/core");
emulator_4917.core._main(process.argv[2]); // passing argument

另外,我們必須將 lein 產生出來的 src/emulator_4917/core.clj 改名為 src/emulator_4917/core.cljc 這樣我們才能夠順利使用 Reader Conditionals 這個功 能。

建立初始樣板

我們首先先建立我們程式的雛形,讓其根據不同條件選擇要載入的 library 或是預先執行的方法,我們修改 src/emulator_4917/core.cljc 成以下

(ns emulator-4917.core
  (:require #?(:cljs [cljs.nodejs :as nodejs])
            #?(:cljs [goog.crypt :as gcrypt])
            [clojure.string :as str])
  #?(:clj (:gen-class)))

;; enable *print-fn* in clojurescript
#?(:cljs (enable-console-print!))

(defn -main [& args]
  (let [arg1 (nth args 0)]
    (if arg1
      (println "TODO: read binary file and execute it.")
      (println "Error: Please specify filename."))))

;; setup node.js starter point
#?(:cljs (set! *main-cli-fn* -main))

在上面的樣板中,被 #?() 所包圍的東西會根據不同狀況被解析,這就是 Clojure 1.7 的 Reader Conditionals 功能,我們可以用以下範例來了解他的使用,下面的程式會根據 目前是編譯給 Clojure 還是 Clojurescript 來選擇要執行的項目,如果你今天是用在 Clojure (JVM)上,則其會顯示 Hi, Clojure ,反之若是執行在 Clojurescript (Node.js, browser) 上,則會顯示 Hi, Clojurescript

#?(:clj
   (println "Hi, Clojure")
   :cljs
   (.log js/console "Hi, Clojurescript"))

讀取二進制檔案並解析

為了讓這個模擬器更像模擬器,我們讓他讀取二進制檔案到 memory 去來模擬 CPU 載入 ROM 動作,讀取完成後則將資料變成 Clojure 的陣列,好方便之後的程式運作,也就是說, 假設欲讀取的二進制文件內容如下

coldnew@Rosia ~/emulator-4917 $ hexdump -C examples/bell.bin | head -n 1
00000000  77 70                                             |wp|

我們要想辦法讀取這份文件,並產生 [7, 7, 7, 0] 這樣的陣列才行,而由於牽扯到 了讀取檔案的運作,這部份一定是要分開 Clojure 與 Clojurescript 的實作。

我們先談談在 Clojure 讀取檔案的作法,理論上我們可以使用 slurp 去讀取檔案,但是由 於 slurp 會將讀取到的內容根據編碼來轉換,因此不適合本文的應用,只好使用 Java 的 方式來讀取二進制檔案囉,我們建立一個 parse-binary-file 函式來讀取檔案並且轉換 成 byte-array。

(defn parse-binary-file
  "Clojure method to read binary file and convert to byte-array."
  [file]
  (with-open [out (java.io.ByteArrayOutputStream.)]
    (clojure.java.io/copy (clojure.java.io/input-stream file) out)
    (.toByteArray out)))

而在 Clojurescript 中,因為我們是執行在 Node.js 環境上,可以使用 Node.js 的 fs.readFileSync() 方法來讀取二進制檔案,讀取完成後在用 google Closure library 裡面 的 goog.crypt.stringToByteArray 將其轉換成 byte-array.

(defn parse-binary-file
  "Clojurescript method to read binary file and convert to byte-array."
  [file]
  (->  (nodejs/require "fs")
       (.readFileSync file "binary")
       .toString
       gcrypt/stringToByteArray))

最後,將這兩部份的程式碼整合起來,就會變成如下

(defn parse-binary-file
  [file]
  #?(:clj
     (with-open [out (java.io.ByteArrayOutputStream.)]
       (clojure.java.io/copy (clojure.java.io/input-stream file) out)
       (.toByteArray out))
     :cljs
     (->  (nodejs/require "fs")
          (.readFileSync file "binary")
          .toString
          gcrypt/stringToByteArray)))

完成了 parse-binary-file 後,由於這樣產生出來的陣列內容為 [ 0x77, 0x70 ] 和 我們期望的 [ 7, 7, 7, 0 ] 有所落差,因此我們需要另外一個方式將 0x77 變成 [ 7, 7 ] 這樣的組合,我們可以用以下函式來達到這件事情

(defn to-4bit-array
  "Convert 0xf4 to [f 4]"
  [s]
  (let [h (bit-and (bit-shift-right s 4) 0x0f) ; (0xf4 >> 4) & 0x0f   => f
        l (bit-and s 0x0f)]                    ; 0xf4 & 0x0f => 4
    [h l]))

接著,我們使用 mapto-4bit-array 套用在 parse-binary-file 得到的結果上

(map to-4bit-array (parse-binary-file file)) ; => [ [7, 7] [7, 0] ]

這樣得到的結果仍舊不是我們想要的,因為他變成了多維陣列,因此我們使用 flatten 將 多維陣列變成一維的

(flatten [ [7, 7] [7, 0] ]) ; => (7, 7, 7, 0)

到此為止,我們完成了讀取二進制檔案並將其變成命令陣列的功能,將其合起來就變成 parse-rom ,我們將用他來讀取二進制檔案並傳送命令陣列給 4917 模擬器處理。

(defn parse-rom [file]
  (flatten
   (map to-4bit-array (parse-binary-file file))))

建立 4917 這顆 CPU

在 Clojure 這種函數式的語言中我們要怎樣定義一個 CPU 呢?最簡單的方式就是透過 defrecord 去創建這個 CPU 的狀態,以 4917 來說,他共有四個暫存器(R0, R1, PC, IS) 以及 16 個 4-bit 的記憶體空間,因此我們可以這樣定義他的 State.

(defrecord State [memory r0 r1 pc is])

這樣子我們就可以透過 State 來創建我們的 CPU 狀態,舉例來說如下

(->State (vec (repeat 16 0)) 0 0 0 0)
;; => #user.State{:memory [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], :r0 0, :r1 0, :pc 0, :is 0}

但是直接用 State 創建 CPU 其實不是很優雅的方式,我們可以將他綁到我們自制的 make-cpu 函式去,讓建立 CPU 更簡單

(defn make-cpu
  ([& {:keys [memory r0 r1 pc is]
       :or {r0 0
            r1 0
            pc 0
            is 0}}]
   (State. (vec (take 16 (concat memory (repeat 16 0)))) r0 r1 pc is)))

透過這樣的 make-cpu 函式,假設我們要創建一個預設 r0 為 5, 記憶體內容為 [5, 2, 2] 的 CPU,則可以這樣作

(make-cpu :memory [5, 2, 2] :r0 5)
;; => #user.State{:memory [5 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0], :r0 5, :r1 0, :pc 0, :is 0}

有了建立 CPU 當前狀態的函式後,是時候去定義這 16 個指令集了

定義指令集

前面說到了 4917 這顆 CPU 共有 16 種指令 (0 ~ 15),因此我們來一個一個定義吧,

指令 0 : 結束程式

首先定義指令 0,當 4917 接收到這個命令後,會結束程式。結束程式的方式,在 Clojure 中我們可以透過

(System/exit 0)

來達成,而在 Node.js 中,則是可以使用 process.exit(0) 來進行

(.exit nodejs/process 0)

因此我們的指令 0 就變成了這個樣子

(defn cmd0
  "cmd 0: exit application."
  [{:keys [memory r0 r1 pc is]}]
  (println "Terminate application.")
  #?(:clj
     (System/exit 0)
     :cljs
     (.exit nodejs/process 0)))

指令 1 : R0 = R0 + R1

指令 1 是我們第一個實作指令,首先我們將目前 CPU 的狀態傳送到指令 1 中,再根據需 求透過 make-cpu 建立新的狀態並回傳,由於 PC (Program Counter) 在執行完此命令後 應該要指向下一個記憶體位址,因此要記得增加 PC 的數值,讓 CPU 可以順利指向下一個 位址。

(defn cmd1
  "cmd 1: R0 = R0 + R1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 (+ r0 r1) :r1 r1 :pc (inc pc) :is 1))

指令 2 : R0 = R0 - R1

指令 2 和指令 1 非常相似,唯一的差別在於 :r0 存放的內容是 r0 - r1 的結果。

(defn cmd2
  "cmd 2: R0 = R0 - R1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 (- r0 r1) :r1 r1 :pc (inc pc) :is 2))

指令 3 : R0 = R0 + 1

指令 3 使用類似指令 1 的實作,我們使用 (inc r0) 來進行 r0 + 1 的動作,當然,你 也可以使用 (+ r0 1)

(defn cmd3
  "cmd 3: R0 = R0 + 1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 (inc r0) :r1 r1 :pc (inc pc) :is 3))

指令 4 : R1 = R1 + 1

指令 4 和指令 3 非常相似,只是將目標暫存器從 r0 改成 r1 而已。

(defn cmd4
  "cmd 4: R1 = R1 + 1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :r1 (inc r1) :pc (inc pc) :is 4))

指令 5 : R0 = R0 -1

使用類似指令 3 的實作模式,我們使用 (dec r0) 來進行 r0 - 1 的動作,當然,你 也可以使用 (- r0 1)

(defn cmd5
  "cmd 5: R0 = R0 - 1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 (dec r0) :r1 r1 :pc (inc pc) :is 5))

指令 6 : R1 = R1 -1

指令 6 和指令 5 非常相似,只是將目標暫存器從 r0 改成 r1 而已。

(defn cmd6
  "cmd 6: R1 = R1 - 1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :r1 (dec r1) :pc (inc pc) :is 6))

指令 7 : 響鈴

若 4917 真的有實體的話,指令 7 的功用應該是響蜂鳴器才對,這邊使用 println 將訊息印 出來作為替代,記得要增加 PC (Program Counter) 的數值。

(defn cmd7
  "cmd 7: Ring bell"
  [{:keys [memory r0 r1 pc is]}]
  (println "Ring the bell!!")
  (make-cpu
   :memory memory :r0 r0 :r1 r1 :pc (inc pc) :is 7))

指令 8 : 印出 <data>

指令 8 就好玩一點點了,我們可以透過 nth 命令,取得記憶體陣列(memory)的某個位置的 數值

(nth [ a b c d ] 2) ; => c

由於 <data> 是在 PC 指向的位址的下一格,因此印出 <data> 的方式就變成這樣

(println (nth memory (inc pc)))

了解這點後,我們就可以實作我們的 cmd8 了,要注意的事情是,這一類 2-byte 命令執行 完後,PC (Program Counter) 的數值是 +2 ,而不是和指令 1 ~ 7 那樣的只是遞增 PC 數值而已。

(defn cmd8
  "cmd 8: Print <data>"
  [{:keys [memory r0 r1 pc is]}]
  (println (nth memory (inc pc)))
  (make-cpu
   :memory memory :r0 r0 :r1 r1 :pc (+ pc 2) :is 8))

指令 9 : 從位址 <data> 讀取資料存入 R0

指令 9 中,我們必須知道 <data> 得內容後,再根據其數值去找尋記憶體位址的內容,並 將其存入 R0 中,而取得 <data> 內容的方式在指令 8 時說過了,使用 nth 可以達成

(nth memory (inc pc))

因此取得 <data> 後,我們可以再使用 nth 一次,來取得記憶體中第 <data> 個內容

(nth memory (nth memory (inc pc)))

因此最後的 cmd9 的實作就是這個樣子:

(defn cmd9
  "cmd 9: Load value from <data> to R0"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r1 r1 :pc (+ pc 2) :is 9
   :r0 (nth memory (nth memory (inc pc)))))

指令 10 : 從位址 <data> 讀取資料存入 R1

指令 10 和指令 9 很像,只是將目標轉換成 R1 而已,我們可以參考指令 9 進行實作。

(defn cmd10
  "cmd 10: Load value from <data> to R1"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :pc (+ pc 2) :is 10
   :r1 (nth memory (nth memory (inc pc)))))

指令 11 : 將 R0 內容存入 位址 <data> 內

現在我們知道了 nth 可以用來取得陣列中某一位置的內容,那我們要怎樣更新陣列的內容 呢?這時候就是 assoc 出場的時候啦! assoc 用在陣列的時候,可以根據你提供的位置以及 新的內容,產生出修改後的陣列

(assoc [0 0 0 0] 1 5) ; => [0 5 0 0]
(assoc [0 0 0 0] 4 6) ; => [0 0 0 0 6]

而從前面的指令運作,我們知道 <data> 的內容是這樣取得的

(nth memory (inc pc))

將這兩個概念合併,就得到我們的指令 11 得實作囉~

(defn cmd11
  "cmd 11: Store R0 into <data> position"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory (assoc memory (nth memory (inc pc)) r0)
   :r0 r0 :r1 r1 :pc (+ pc 2) :is 11))

指令 12 : 將 R1 內容存入 位址 <data> 內

指令 12 和指令 11 非常相似,只是將來源暫存器從 r0 改成 r1 而已。

(defn cmd12
  "cmd 12: Store R1 into <data> position"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory (assoc memory (nth memory (inc pc)) r1)
   :r0 r0 :r1 r1 :pc (+ pc 2) :is 12))

指令 13 : 將 PC 指向 位址 <data> (即 JUMP 命令)

指令 13 其實就是一般 CPU 都會有的 JUMP 命令,透過更改 PC (Program Counter) 的 位址,來讓 CPU 去讀取不同記憶體位址。

(defn cmd13
  "cmd 13: jump to address <data>"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :r1 r1 :pc (nth memory (inc pc)) :is 13))

指令 14 : 如果 R0 為 0,將 PC 指向 位址 <data>

指令 14 很單純,我們比對傳入的 r0 數值,若是 0 的話則將 PC (Program Counter) 調 整為 <data>,反之則進行 pc + 2 的動作,而判斷數值是否為 0,在 clojure 可以使用 zero? 進行判別

(zero? 0) ; => true
(zero? 3) ; => false

因此我們指令 14 的實作就這樣完成了

(defn cmd14
  "cmd 14: jump to address <data> if R0 == 0"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :r1 r1
   :pc (if (zero? r0)
         (nth memory (inc pc))
         (+ pc 2))
   :is 14))

指令 15 : 如果 R0 不為 0, 將 PC 指向 位址 <data>

指令 15 其實和指令 14 一樣,我們只要調整 if 判斷式參數的順序就夠了,如果 R0 為 0,則進行 pc + 2 ,反之則將 PC (Program Counter) 內容改為 <data> 數值。

(defn cmd15
  "cmd 15: jump to address <data> if R0 != 0"
  [{:keys [memory r0 r1 pc is]}]
  (make-cpu
   :memory memory :r0 r0 :r1 r1
   :pc (if (zero? r0)
         (+ pc 2)
         (nth memory (inc pc)))
   :is 15))

建立執行指令的迴圈

當指令集完成後,是時候執行一個迴圈,不斷的執行記憶體內的命令直到結束,在這邊我們 的迴圈直接使用 recur 來完成,recur 是 Clojure 迴圈的一個方式,基本上就是讓函數自己 呼叫自己,直到結束,假如我們要實作倒數計時器,則可以這樣寫

(defn countdown [n]
  (if (zero? n)
    (println "finished!")
    (do
      (println n)
      (recur (dec n)))))

(countdown 2)
;; => 2
;; => 1
;; => finished!

上面的 countdown 命令,會自己呼叫自己直到輸入的參數為 0 為止,依照這個概念我們執 行指令集的迴圈,可以寫成下面這樣,實際根據指令進行執行的部分則由 execute 函式 完成。由於此 CPU 的指令 0 即代表 終止程式 ,因此我們在此並未額外判斷要結 束 recur 回圈的條件。

(defn run [command]
  (let [cpu (execute command)]
    ;; (print cpu) ;; for debug only
    (recur cpu)))

而在 execute 函式中,我們使用了 try … catch 來確保執行的狀態不會出錯,注意到 Node.js 在 Exception 的地方和 Clojure 不同,要使用 js/Error 來替代,假設進入 到了 catch 的區塊,則回傳 0 給 curr 變數,讓他進入到 default 的命令,在這邊就 是 cmd0 ,也就是說一旦程式出錯,就強行終止程式。

(defn execute [state]
  (let [curr
        (try (nth (:memory state) (:pc state))
             (catch
                 #?(:clj
                    Exception
                    :cljs
                    js/Error) _ 0))]
    ;; (println state)
    (case curr
      ;; 1-byte instruction
      1 (cmd1 state)
      2 (cmd2 state)
      3 (cmd3 state)
      4 (cmd4 state)
      5 (cmd5 state)
      6 (cmd6 state)
      7 (cmd7 state)
      ;; 2-byte instruction
      8 (cmd8 state)
      9 (cmd9 state)
      10 (cmd10 state)
      11 (cmd11 state)
      12 (cmd12 state)
      13 (cmd13 state)
      14 (cmd14 state)
      15 (cmd15 state)
      ;; default
      (cmd0 state))))

進行最後的修改

我們可以丟幾道簡單的程式讓 4917 模擬器執行看看

(run (make-cpu :memory [ 7 0 ]))
;; => Ring the bell!!

或是讓他印出 5 這個數字

(run (make-cpu :memory [ 8 5 ]))
;; => 5

確認模擬器執行成功後,是時候修正我們得 main 方法了,調整成如下就完成整個程式囉 ~

(defn -main [& args]
  (let [arg1 (nth args 0)]
    (if arg1
      (run (make-cpu :memory (parse-rom arg1)))
      (println "Error: Please specify filename."))))

取得完整程式碼與執行程式

本篇文章的完整程式碼已經放置於 GitHub 上,你可以使用以下方式取得程式碼

git clone https://github.com/coldnew/emulator-4917.git

而在 examples 資料夾共放了以下幾種 4917 的範例二進制程式

檔案名稱 功能說明
countdown.bin 從 5 開始倒數計數到 1
countup.bin 從 0 開始不斷往上數
bell.bin 響三次鈴聲
calculate_6+3+1.bin 計算 6 + 3 + 1

執行程式 (JVM)

若要使用 JVM 來執行本篇文章的程式,則可以直接使用 lein run 進行

lein run -- examples/countdown.bin

或是你也可以透過 lein uberjar 功能將其打包成 .jar 檔案再執行

lein uberjar
java -jar target/emulator-4917-0.1.0-SNAPSHOT-standalone.jar examples/countdown.bin

執行程式 (Node.js)

本篇文章預設是使用 none 最佳化編譯, 因此需透過 run.js 來協助執行程式

node run.js  examples/countdown.bin

你也可以修改 project.clj 將其變成如下

:optimizations :advanced
:pretty-print false

接著重新編譯就可以獲得最佳化(並且沒人看得懂的) javascript 了,執行方式如下:

lein cljsbuild once
node target/emulator-4917.js examples/countdown.bin