原文: 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 社区仍是蛮热情的. 谢谢你们.