原文: http://howistart.org/posts/nim/1html
Nim 是一門年輕的, 讓人興奮的命令式編程語言, 即將發佈 1.0 辦法.
我對與 Nim 最主要的興趣在於性能/生成力的比值, 以及使用 Nim 寫程序帶來的樂趣.
這份教程裏我會展現一下我是怎麼展開一個 Nim 項目的.react
如今咱們的目標是寫一個 Brainfuck 語言的簡單的解釋器.
Nim 是一個使用的編程語言, 有着各類有趣的功能, Brainfuck 正好相反:
它很不實用, 它的全局功能就 8 個簡單字符代碼的指令.
不過 Brainfuck 對咱們來講仍是不錯的, 由於它夠簡單, 寫解釋器也就很簡單.
後面咱們還會寫一個高性能的編譯器, 把 Brainfuck 在編譯時轉換爲 Nim.
全部代碼會被包裝成 [nimble 模塊]而後在網上發佈git
安裝 Nim 步驟不錯, 你能夠看官方的說明. Windows 的二進制包是現成的.
其餘操做系統你能夠用 build.sh
教程編譯生成的 C 代碼, 通常的操做系統一分鐘內能完成.github
這向咱們透露了 Nim 第一個有意思的事實: 它主要是編譯爲 C (也能夠 C++, ObjectiveC, 甚至 JavaScript)
而後用高度優化的 C 編譯器把代碼編譯成爲實際的程序.
你直接就能從 C 的生態系統當中獲益.web
若是你選擇從 Nim 的編譯器 自舉, 也就是 Nim 語言自身實現的版本, 那麼,
你能夠看一看編譯器是怎麼一步一步把本身編譯起來的(兩分鐘之內能完成):編程
bash$ git clone https://github.com/Araq/Nim $ cd Nim $ git clone --depth 1 https://github.com/nim-lang/csources $ cd csources && sh build.sh $ cd .. $ bin/nim c koch $ ./koch boot -d:release
這樣你獲得的是開發版本的 Nim. 要追上最新版本, 按下邊兩步應該就能夠了:json
bash$ git pull $ ./koch boot -d:release
若是能歷來沒作過, 那麼這個時候安裝一下 git
也是很不錯的.
大部分的 nimble 模塊託管在 GitHub 上, 咱們須要用 git
來獲取.
在基於 Debian 的發行版當中(好比 Ubuntu), 這樣就能安裝:小程序
bash$ sudo apt-get install git
安裝好之後, 把 nim
二進制文件加入到你的 PATH 環境變量當中去. 用 Bash 的話是這樣作:vim
bash$ echo 'export PATH=$PATH:$your_install_dir/bin' >> ~/.profile $ source ~/.profile $ nim Nim Compiler Version 0.10.2 (2014-12-29) [Linux: amd64] Copyright (c) 2006-2014 by Andreas Rumpf :: nim command [options] [projectfile] [arguments] Command: compile, c compile project with default code generator (C) doc generate the documentation for inputfile doc2 generate the documentation for the whole project i start Nim in interactive mode (limited) ...
當 nim
命令返回起版本跟用法, 就能夠繼續後面的步驟了.後端
如今 [Nim 的標準模塊]只要 import 一下就行了.
其餘的模塊均可以用 nimble 來獲取, 也就是 Nim 的包管理工具.
咱們要看一下基礎的安裝說明.
一樣, Windows 平臺有編譯好的包, 不過從源碼編譯也挺輕鬆的:
bash$ git clone https://github.com/nim-lang/nimble $ cd nimble $ nim c -r src/nimble install
Nimble 的二進制目錄也要加到 PATH 環境變量當中去:
bash$ echo 'export PATH=$PATH:$HOME/.nimble/bin' >> ~/.profile $ source ~/.profile $ nimble update Downloading package list from https://github.com/nim-lang/packages/raw/master/packages.json Done.
如今咱們來瀏覽可用的 nimble 模塊或者從命令行當中進行搜索:
bash$ nimble search docopt docopt: url: git://github.com/docopt/docopt.nim (git) tags: commandline, arguments, parsing, library description: Command-line args parser based on Usage message license: MIT website: https://github.com/docopt/docopt.nim
咱們來安裝剛纔找到的 docopt 模塊, 待會可能會用到:
bash$ nimble install docopt ... docopt installed successfully.
看看安裝模塊多塊(我這裏小於 1 秒). 這是 Nim 另外一個好處.
基本上模塊的源代碼只是被下載, 共享的模塊當中沒有什麼要被編譯的.
而是在咱們使用到模塊的時候, 模塊纔會被靜態編譯到程序當中.
能夠找到關於 Nim 的編輯器支持 的一個列表,
好比 Emacs(nim-mode), Vim(nimrod.vim[nimrod-vim], 個人用的), 還有 Sublime(Nimlime).
對於這篇教程範圍來講, 什麼編輯器都是能夠的.
如今咱們開始建項目:
bash$ mkdir brainfuck $ cd brainfuck
第一步: 要在終端打印 Hello World
, 咱們先創建一個 hello.nim
包含如下內容:
nimecho "Hello World"
編譯代碼, 而後運行, 先用兩個獨立的步驟:
bash$ nim c hello $ ./hello Hello World
而後能夠用一個步驟, 指明 Nim 編譯器在生成二進制文件之後順便運行一下:
bash$ nim c -r hello Hello World
把代碼改得稍微複雜一點, 那麼運行起來就能久一點:
nimvar x = 0 for i in 1 .. 100_000_000: inc x # increase x, 增長 x, 順便說下這是註釋 echo "Hello World ", x
如今咱們是初始化變量 x
爲 0
, 每次增長 1
一共一億次. 繼續編譯, 運行.
注意這一次運行了多久. Nim 的性能很不堪麼? 固然不是, 事實上正好相反.
上邊咱們是在調試模式下生成的二進制文件, 添加了整數溢出的檢測, 數組超出範圍, 以及不少, 並且咱們一點沒作優化.
使用 -d:release
選項能夠幫助咱們切換到 release 模式, 提供全速:
bash$ nim c hello $ time ./hello Hello World 100000000 ./hello 2.01s user 0.00s system 99% cpu 2.013 total $ nim -d:release c hello $ time ./hello Hello World 100000000 ./hello 0.00s user 0.00s system 74% cpu 0.002 total
實際上者也太快了. C 編譯器直接把整個 for
循環給優化沒了. Oops.
要建立一個新項目用 nimble init
能夠成成基本的模塊配置文件:
bash$ nimble init brainfuck
新生成的 brainfuck.nimble
應該是這樣的:
ini[Package] name = "brainfuck" version = "0.1.0" author = "Anonymous" description = "New Nimble project for Nim" license = "BSD" [Deps] Requires: "nim >= 0.10.0"
咱們加上實際做者, 描述, 還有 docopt
這個依賴, 按照 [nimble 開發者信息]中描述的.
最重要的, 咱們要定義好想要建立的二進制文件:
ini[Package] name = "brainfuck" version = "0.1.0" author = "The 'How I Start Nim' Team" description = "A brainfuck interpreter" license = "MIT" bin = "brainfuck" [Deps] Requires: "nim >= 0.10.0, docopt >= 0.1.0"
由於咱們已經安裝了 git
, 咱們要記錄源碼全局的版本, 還有發到線上, 那麼初始化一下 Git 倉庫:
bash$ git init $ git add hello.nim brainfuck.nimble .gitignore
其中個人 .gitignore
是這樣的:
bashnimcache/ *.swp
Git 須要 ignore 掉 Vim 的 swap 文件, 還有 nimcache
文件中包含的生成的當前項目的 C 代碼.
若是你對 Nim 怎麼生成 C 代碼感興趣, 能夠看一下.
要展現 nimble 的能力, 咱們來初始化 brainfuck.nim
, 寫上 main 程序:
nimecho "Welcome to brainfuck"
咱們能夠像以前編譯 hello.nim
同樣進行編程, 不過考慮咱們已經在模塊裏定義好 brainfuck
的二進制文件,
咱們用 nimble
來作這個工做吧:
bash$ nimble build Looking for docopt (>= 0.1.0)... Dependency already satisfied. Building brainfuck/brainfuck using c backend... ... $ ./brainfuck Welcome to brainfuck
nimble install
能夠用來在咱們的系統當中安裝二進制文件, 而後咱們能夠隨處運行:
bash$ nimble install ... brainfuck installed successfully. $ brainfuck Welcome to brainfuck
程序能運行了是很棒的事情, 可是 nimble build
實際上作的是 release build.
這會比調試中的 builg 過程更漫長, 並且去掉開發過程當中很重要的檢查,
因此這個時候 nim c -r brainfuck
仍是比較適合這種狀況的.
開發過程中多執行幾回程序, 感覺一下每一個地方是怎麼運行的.
Nim 有文檔能夠參考, 不過你不知道怎麼找到某些東西的話, 還有個索引你能夠搜索.
咱們開始修改 brainfuck.nim
開發咱們的解釋器吧:
nimimport os
首先咱們引入 os 模塊, 那麼咱們能夠讀取命令行的參數:
nimlet code = if paramCount() > 0: readFile paramStr(1) else: readAll stdin
paramCount()
能夠告訴咱們傳給應用的命令行參數的個數.
咱們拿到命令行參數的話, 咱們設想會是文件名, 那麼直接經過 readFile paramStr(1)
讀取文件.
不然咱們直接從標準輸入讀取所在的東西. 兩種狀況下, 結果都是存儲在 code
變量,
這個變量被 let
關鍵字聲明爲不可修改的.
要看是否正常運行, 咱們能夠 echo
一下 code
:
nimecho code
而後試一試:
nim$ nim c -r brainfuck ... Welcome to brainfuck I'm entering something here and it is printed back later! I'm entering something here and it is printed back later!
你輸入完"代碼"之後要用 ctrl-d 來結束.
或者你能夠傳入一個文件名, nim c -r brainfuck
命令後面全部的都做爲命令行參數傳給生成的可執行文件:
nim$ nim c -r brainfuck .gitignore ... Welcome to brainfuck nimcache/ *.swp
而後咱們寫:
nimvar tape = newSeq[char]() codePos = 0 tapePos = 0
咱們定義一些會用到的變量. 須要保存 code
字符串當中的當前位置(codePos
)延遲 tape
上的位置(tapePos
).
Brainfuck 運行在一卷無限長延伸的 tape
上, 表示爲一個 seq
的 char
(字符的序列).
序列是 Nim 當中動態長度的 array, 除了協程 newSeq
你也能夠用 var x = @[1, 2, 3]
初始化.
咱們花一點時間來回味一下不用爲變量申明類型帶來的方便, 它們都是自動推斷的.
若是非要寫得更明確一點, 咱們能夠寫:
nimvar tape: seq[char] = newSeq[char]() codePos: int = 0 tapePos: int = 0
而後咱們寫一個小的 procedure, 而後在後邊立刻調用:
nimproc run(skip = false): bool = echo "codePos: ", codePos, " tapePos: ", tapePos discard run()
有些事情能夠注意的:
skip
參數, 初始化爲 false
bool
bool
類型的, 可是咱們什麼都沒返回麼? 每一個返回結果都是默認二進制 0, 咱們是返回的 `falseresult
變量在每一個 proc 表示返回值, 設置爲 result = true
return true
能夠當即返回結果discard
掉調用 run()
返回的 bool 數值.brainfuck.nim(16, 3) Error: value of type 'bool' has to be discarded
.繼續以前, 咱們來想一下 Brainfuck 是怎樣運行的.
若是以前你接觸過圖靈機, 那麼其中一些地方你會感到很熟悉.
咱們會輸入一個字符串 code
, 還有一個包含 char
的 tape
會在一個方向無線延伸.
輸入的字符串當中會出現 8 中命令, 其餘的字符都會被忽略掉:
操做符 含義 Nim 對應代碼 > 在 tape 上向右移動 inc tapePos < 在 tape 上向左移動 dec tapePos + 增長 tape 上的數值 inc tape[tapePos] - 減少 tape 上的數值 dec tape[tapePos] . 輸出 tape 上的數值 stdout.write tape[tapePos] , 輸入值到 tape 上 tape[tapePos] = stdin.readChar [ 若是 tape 上的值是 \0, 向前移動到匹配了 ] 以後的命令 ] 若是 tape 上不是 \0, 向後移動到匹配 [ 以後的命令
僅僅依靠上邊這些, Brainfuck 成爲了最簡單的圖靈徹底的編程語言之一.
前面 6 條指令能夠被轉化爲 Nim 當中的 case 區別:
nimproc run(skip = false): bool = case code[codePos] of '+': inc tape[tapePos] of '-': dec tape[tapePos] of '>': inc tapePos of '<': dec tapePos of '.': stdout.write tape[tapePos] of ',': tape[tapePos] = stdin.readChar else: discard
到這裏咱們是處理單個字符的輸入, 而後咱們寫一個處理所有字符的循環:
nimproc run(skip = false): bool = while tapePos >= 0 and codePos < code.len: case code[codePos] of '+': inc tape[tapePos] of '-': dec tape[tapePos] of '>': inc tapePos of '<': dec tapePos of '.': stdout.write tape[tapePos] of ',': tape[tapePos] = stdin.readChar else: discard inc codePos
咱們來測試一下這樣一個簡單的程序:
text$ echo ">+" | nim -r c brainfuck Welcome to brainfuck Traceback (most recent call last) brainfuck.nim(26) brainfuck brainfuck.nim(16) run Error: unhandled exception: index out of bounds [IndexError] Error: execution of an external program failed
結果讓人詫異, 咱們的代碼 crash 了! 什麼地方寫錯了?
tape 被認爲是無限延伸的, 但咱們到如今一點都沒增長它的長度!
能夠在 case
代碼上邊很容易地 fix 掉:
nimif tapePos >= tape.len: tape.add '\0'
最後兩條指令, [
和 ]
組成了簡單的循環. 咱們也能夠在代碼裏寫出來:
nimproc run(skip = false): bool = while tapePos >= 0 and codePos < code.len: if tapePos >= tape.len: tape.add '\0' if code[codePos] == '[': inc codePos let oldPos = codePos while run(tape[tapePos] == '\0'): codePos = oldPos elif code[codePos] == ']': return tape[tapePos] != '\0' elif not skip: case code[codePos] of '+': inc tape[tapePos] of '-': dec tape[tapePos] of '>': inc tapePos of '<': dec tapePos of '.': stdout.write tape[tapePos] of ',': tape[tapePos] = stdin.readChar else: discard inc codePos
若是咱們遇到一個 [
咱們就遞歸地調用 run
函數自身,
一直循環直到對應的 ]
tape 上沒有 \0
的一個 tapePos
.
就這樣. 咱們有了一個能夠運行的 Brainfuck 解釋器.
爲了作測試, 咱們建立一個 examples
文件夾, 其中包含 3 個文件:
helloworld.b, rot13.b, mandelbrot.b.
text$ nim -r c brainfuck examples/helloworld.b Welcome to brainfuck Hello World! $ ./brainfuck examples/rot13.b Welcome to brainfuck You can enter anything here! Lbh pna ragre nalguvat urer! ctrl-d $ ./brainfuck examples/mandelbrot.b
在最後一個程序運行的時候你課以看到咱們解釋器有多麼.
使用 -d:release
命令編譯能夠顯著提高性能, 但仍是花了 90 秒的時候在我電腦上畫 Mandelbrot 集.
爲了達到更高的性能, 後面咱們要把 brainfuck 編譯到 Nim, 而不是解釋它.
Nim 的元編程能力對於這項任務是完美的.
首先咱們保持它的簡單. 咱們的解釋器是能夠運行的, 那沒咱們能夠把它變成一個能夠重用的庫.
咱們所須要作的就是把代碼包含在一個大的 proc
當中:
nimproc interpret*(code: string) = var tape = newSeq[char]() codePos = 0 tapePos = 0 proc run(skip = false): bool = ... discard run() when isMainModule: import os echo "Welcome to brainfuck" let code = if paramCount() > 0: readFile paramStr(1) else: readAll stdin interpret code
注意咱們在 proc 後面加上了一個 *
, 這表示 proc 被暴露能夠在模塊外部訪問.
其餘一切都是私有的.
在問問的結尾咱們依然保留咱們的二進制文件.when isMainModule
保證了代碼只會在模塊是主模塊時纔會被編譯.
通過短暫的 nimble install
以後這個 Brainfuck 模塊就全局可用了, 這樣:
nimimport brainfuck interpret "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
看着不錯! 到這裏咱們已經能跟別人共享代碼了, 不過咱們仍是先加上一些文檔:
nimproc interpret*(code: string) = ## Interprets the brainfuck `code` string, reading from stdin and writing to ## stdout. ...
執行 nim doc brainfuck
能夠生成文檔, 你能夠[在線上看到][bf-docs]所有.
就像前面說的, 咱們的解釋器對於 Mandelbrot 程序來講仍是很是慢的.
咱們仍是來寫一個 procedure 在編譯時成成 Nim 代碼的 AST 吧.
nimimport macros proc compile(code: string): PNimrodNode {.compiletime.} = var stmts = @[newStmtList()] template addStmt(text): stmt = stmts[stmts.high].add parseStmt(text) addStmt "var tape: array[1_000_000, char]" addStmt "var tapePos = 0" for c in code: case c of '+': addStmt "inc tape[tapePos]" of '-': addStmt "dec tape[tapePos]" of '>': addStmt "inc tapePos" of '<': addStmt "dec tapePos" of '.': addStmt "stdout.write tape[tapePos]" of ',': addStmt "tape[tapePos] = stdin.readChar" of '[': stmts.add newStmtList() of ']': var loop = newNimNode(nnkWhileStmt) loop.add parseExpr("tape[tapePos] != '\\0'") loop.add stmts.pop stmts[stmts.high].add loop else: discard result = stmts[0] echo result.repr
其中的 addStmt
template 只是用來減小代碼模版的.
咱們也徹底能夠在目前用了 addStmt
的未必謬次明確寫上相同的操做.
(那也就是如今的 template 所作的事情!)parseStmt
把一段 Nim 代碼轉換成對應的 AST, 而後咱們把他存放在數組裏.
大部分的代碼跟解釋器是類似的, 出來代碼如今不是立刻被執行的, 而是被添加到語句的列表裏.[
和 ]
就更復雜了, 它們被翻譯到一個加載了代碼的 while 循環.
這裏咱們取巧了, 使用定長的 tape
而再也不去檢查是否在範圍內, 有沒有溢出.
這只是爲了簡化一下. 要了解代碼的行爲, 在最後一行, echo result.repr
能夠打印出生成的 Nim 代碼.
而後在一個 static
的代碼塊裏調用一下, 這能夠強制在編譯時運行:
nimstatic: discard compile "+>+[-]>,."
編譯過程當中生成的代碼會被打印出來:
nimvar tape: array[1000000, char] var codePos = 0 var tapePos = 0 inc tape[tapePos] inc tapePos inc tape[tapePos] while tape[tapePos] != '\0': dec tape[tapePos] inc tapePos tape[tapePos] = stdin.readChar stdout.write tape[tapePos]
一般能夠用到 dumpTree
這個宏, 能夠打印代碼真實的 AST 出來, 好比:
nimimport macros dumpTree: while tape[tapePos] != '\0': inc tapePos
會顯示出以下的樹:
nimStmtList WhileStmt Infix Ident !"!=" BracketExpr Ident !"tape" Ident !"tapePos" CharLit 0 StmtList Command Ident !"inc" Ident !"tapePos"
好比我就是經過這個辦法知道須要的是 StmtList
.
用 Nim 進行元編程的時候, 一般用 dumpTree
打印出從 AST 生成的代碼會頗有用.
宏生成的代碼能夠被直接插入到程序當中:
nimmacro compileString*(code: string): stmt = ## 編譯 Brainfuck `code` 字符串到 Nim 代碼, ## 從 stdin 讀取數據, 在 stdout 寫輸出內容 compile code.strval macro compileFile*(filename: string): stmt = ## 編譯過程從 `filename` 讀取 Brainfuck 代碼編譯到 Nim ## 從 stdin 讀取, 在 stdout 寫輸出的內容 compile staticRead(filename.strval)
這樣能夠就能夠很容易地吧 Mandelbrot 程序編譯到 Nim 了:
nimproc mandelbrot = compileFile "examples/mandelbrot.b" mandelbrot()
開啓所有的優化僅限編程的話時間會很長(大約 4s), 由於 Mandelbrot 程序很大, GCC 須要時間優化.
最終結果程序的運行只須要一秒鐘:
text$ nim -d:release c brainfuck $ ./brainfuck
Nim 默認使用 GCC 來編譯到中間層的 C 代碼, 不過 Clang 常常編譯得更快, 獲得的代碼也更高效.
因此值得試一試. 要用 Clang 編譯的話, 使用 nim -d:release --cc:clang c hello
.
若是你打算一直使用 Clang 編譯 hello.nim
, 能夠建立 hello.nim.cfg
文件, 內容寫 cc = clang
.
還能夠編輯 Nim 目錄中的 config/nim.cfg
文件修改默認的編譯後端.
說到改變編譯器默認的選項, Nim 編譯器有時挺多嘴的, 能夠在 config/nim.cfg
裏設置 hints = off
關閉.
一個更意想不到的編譯器警告是使用 l
(小寫的 L
)做爲標識符, 由於它看起來像 1
(壹):
texta.nim(1, 4) Warning: 'l' should not be used as an identifier; may look like '1' (one) [SmallLshouldNotBeUsed]
若是你看不上的話, 寫上 warning[SmallLshouldNotBeUsed] = off
就可讓編譯器安靜.
Nim 還有個好處是可使用 C 支持的 debugger, 好比 GDB.
用 nim c --linedir:on --debuginfo c hello
命令編譯而後運行 gdb ./hello
進行 debug.
前面一直是用手寫的代碼解析命令行參數. 既然已經安裝了 dotopt.nim, 如今來用一下:
nimwhen isMainModule: import docopt, tables, strutils proc mandelbrot = compileFile("examples/mandelbrot.b") let doc = """ brainfuck Usage: brainfuck mandelbrot brainfuck interpret [<file.b>] brainfuck (-h | --help) brainfuck (-v | --version) Options: -h --help Show this screen. -v --version Show version. """ let args = docopt(doc, version = "brainfuck 1.0") if args["mandelbrot"]: mandelbrot() elif args["interpret"]: let code = if args["<file.b>"]: readFile($args["<file.b>"]) else: readAll stdin interpret(code)
docopt 模塊一個好處是文檔寫在函數當中做爲規範, 很容易使用:
text$ nimble install ... brainfuck installed successfully. $ brainfuck -h brainfuck Usage: brainfuck mandelbrot brainfuck interpret [<file.b>] brainfuck (-h | --help) brainfuck (-v | --version) Options: -h --help Show this screen. -v --version Show version. $ brainfuck interpret examples/helloworld.b Hello World!
隨着項目變大, 能夠把代碼移到 src
目錄, 再添加一個 test
目錄,
很快咱們會須要這個目錄, 最終文件結構是這樣的:
text$ tree . ├── brainfuck.nimble ├── examples │ ├── helloworld.b │ ├── mandelbrot.b │ └── rot13.b ├── license.txt ├── readme.md ├── src │ └── brainfuck.nim └── tests ├── all.nim ├── compile.nim ├── interpret.nim └── nim.cfg
這樣 nimble 文件也須要修改一下:
nimsrcDir = "src" bin = "brainfuck"
爲了讓代碼容易重用, 咱們作一些重構. 同時保證程序使用讀取 stdin 和寫入stdout.
在直接接受 code: string
這樣的命令行參數以外, 擴展 interpret
procedure 來接收輸入輸出的流.
引入一個 streams 模塊 對 FileStreams
和 StringStream
進行支持:
nim## :Author: Dennis Felsing ## ## This module implements an interpreter for the brainfuck programming language ## as well as a compiler of brainfuck into efficient Nim code. ## ## Example: ## ## .. code:: nim ## import brainfuck, streams ## ## interpret("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.") ## # Prints "Hello World!" ## ## proc mandelbrot = compileFile("examples/mandelbrot.b") ## mandelbrot() # Draws a mandelbrot set import streams proc interpret*(code: string; input, output: Stream) = ## Interprets the brainfuck `code` string, reading from `input` and writing ## to `output`. ## ## Example: ## ## .. code:: nim ## var inpStream = newStringStream("Hello World!\n") ## var outStream = newFileStream(stdout) ## interpret(readFile("examples/rot13.b"), inpStream, outStream)
這裏還爲模塊添加了文檔, 模塊代碼作爲類庫怎樣使用. 看一下生成的文檔.
大部分代碼能夠不變, 除了與 Brainfuck 操做符 .
和 ,
相關的代碼,
後面將使用 output
替代 stdout
, 用 input
代替 stdin
:
nimof '.': output.write tape[tapePos] of ',': tape[tapePos] = input.readCharEOF
爲何有個奇怪的 readCharEOF
而不是 readChar
, 做用是什麼呢?
不少系統的 EOF
(end of file) 表明 -1
, 咱們這個 Brainfuck 程序也常常這樣用.
這也意味着這個 Brainfuck 程序實際上不會在全部的系統都能運行.
同時 streams 模塊也會處理系統不一致, 在 EOF
時返回 0
.
這裏用 readCharEOF
顯式地轉化到 -1
:
nimproc readCharEOF*(input: Stream): char = result = input.readChar if result == '\0': # Streams 返回 0 表示 EOF result = 255.chr # BF 但願 EOF 是 -1
這裏你可能注意到了標識符聲明的順序在 Nim 當中是有影響的.
若是你在 interpret
後面聲明 readCharEOF
, 就不能在 interpret
中調用到.
我我的但願遵循這一點, 由於這構成了每一個模塊中一個簡單代碼到複雜代碼這樣的層級.
若是你仍是但願繞過這一點, 就把 readCharEOF
的聲明從定義拆分出來放到 interpret
前面:
nimproc readCharEOF*(input: Stream): char
而後能夠像以前同樣去使用解釋器, 也很簡單:
nimproc interpret*(code, input: string): string = ## 解釋執行 Brainfuck `code` 字符串, 從 `input` 讀取內容, ## 直接打印出結果. var outStream = newStringStream() interpret(code, input.newStringStream, outStream) result = outStream.data proc interpret*(code: string) = ## 解釋執行 Brainfuck `code` 字符串, 從 stdin 讀取內容, ## 輸出寫到 stdout. interpret(code, stdin.newFileStream, stdout.newFileStream)
如今的 interpret
procedure 能夠返回一個字符串. 這對後邊的測試來講很重要:
nimlet res = interpret(readFile("examples/rot13.b"), "Hello World!\n") interpret(readFile("examples/rot13.b")) # with stdout
編譯器部分的重寫有點複雜. 首先要把 input
跟 output
做爲字符串,
那麼用戶使用這個 proc 的時候就能夠用任何他們想要的 stream 了:
nimproc compile(code, input, output: string): PNimrodNode {.compiletime.} =
還須要兩條語句對輸入跟輸出的 stream 進行初始化而後做爲字符串參數:
nimaddStmt "var inpStream = " & input addStmt "var outStream = " & output
固然我在咱們就要用 outStream
和 inpStream
來代替 stdout 跟 stdin 了, 還有 readCharEOF
代替 readChar
.
主要能夠直接用解釋器已有的 readCharEOF
procedure, 不須要重複寫:
nimof '.': addStmt "outStream.write tape[tapePos]" of ',': addStmt "tape[tapePos] = inpStream.readCharEOF"
咱們還能夠加上語句在用戶用法有誤時彈出好懂的錯誤信息:
nimaddStmt """ when not compiles(newStringStream()): static: quit("Error: Import the streams module to compile brainfuck code", 1) """
而後把 compile
procedure 鏈接到 compileFile
這個宏, 再使用 stdin 跟 stdout:
nimmacro compileFile*(filename: string): stmt = compile(staticRead(filename.strval), "stdin.newFileStream", "stdout.newFileStream")
讀取輸入的字符串, 寫入輸出的字符串:
nimmacro compileFile*(filename: string; input, output: expr): stmt = result = compile(staticRead(filename.strval), "newStringStream(" & $input & ")", "newStringStream()") result.add parseStmt($output & " = outStream.data")
這段複雜的代碼讓咱們可以編譯 rot13
procedure, 鏈接 input
字符串跟 result
內容到編譯後的程序:
nimproc rot13(input: string): string = compileFile("../examples/rot13.b", input, result) echo rot13("Hello World!\n")
將來方便我對給 compileString
寫了同樣的代碼. 能夠在 GitHub 上看 brainfuck.nim
完整代碼.
未翻譯
未翻譯
Nim 的生態系統到這裏已經介紹完了, 但願你喜歡, 並且能跟我同樣享受寫 Nim 代碼.
你要繼續學習 Nim 的話, 我最近寫了 what is special about Nim
和 what makes Nim practical, 還有個小程序的珍貴的收藏.
若是你想要用更傳統的方法開始學 Nim, 官方教程跟 Nim by Example 對你會有用.
Nim 社區仍是蠻熱情的. 謝謝你們.