TIPBrainfuck 这个名字真的是一点都没起错
,我写的时候感觉我的 Brain 正在被 Fuck。
目录
1. Brainfuck 介绍
Brainfuck 是一种极小化的编程语言,由 Urban Müller 在 1993 年设计。它的特点非常独特:整个语言只有 8 个操作符,但却是 图灵完备(Turing Complete) 的,也就是说,它理论上可以完成任何计算任务。
1.1. Brainfuck 的 8 个操作符
Brainfuck 假设存在一个无限长的数组(实践中通常选择长度为 30000 的 uint8 数组),以及一个指向数据的指针(实践中初始状态指针通常指向第一个内存地址),然后通过 8 个操作符来操作这个数据。
.:输出当前单元的 ASCII 值,:从标准输入读取一个字符,并将其存储到当前单元+:将当前单元的值加一-:将当前单元的值减一>:将数据指针向右移动一个单元<:将数据指针向左移动一个单元[:如果当前单元的值为零,则跳转到与之匹配的]后面的第一个字符]:如果当前单元的值不为零,则跳转到与之匹配的[前面的第一个字符
1.2. Brainfuck 与图灵机
Brainfuck 的假设和操作符设计,很明显是受到了图灵机的启发。图灵机是一种理论计算机模型,由一个无限长的纸带、一个读写头和一个状态寄存器组成。Brainfuck 的数据、指针和操作符,都可以看作是图灵机的对应部分:
- 图灵机的无限长纸带,可以看作是
Brainfuck的无限长的数组。 - 图灵机的读写头,可以看作是
Brainfuck的数据指针。 - 图灵机的读写操作,可以看作是
Brainfuck的+、-、.、,操作符。 - 图灵机读写头的移动,可以看作是
Brainfuck的>、<操作符。 - 图灵机的状态寄存器与控制规则,可以看作是
Brainfuck的[、]操作符,它们提供了条件跳转的功能。
不难从直觉上感觉到 Brainfuck 是图灵完备的。
1.3. 几个例子
我先给出不带注释的版本,然后再给出有注释的版本。看到一坨代码不用害怕,仔细看注释,你会发现它其实很简单。
值得注意的是,因为 Brainfuck 只识别这几种字符,任意其他字符都会被忽略,所以你几乎可以用你习惯的任何语言的注释风格写注释,都可以作为合法的 Brainfuck 代码。(除了 HTML 和 Markdown 的注释)
1.3.1. 通过循环实现 uint8 乘法
注意:输入输出都是以 ASCII 码的形式进行的,比如输入 1 实际上输入的是 49。
,>[-]>[-]>,[<+<+>>-]<<<[>>[<+>>+<-]>[<+>-]<<<-]>.有注释的版本:
, # 在第 0 个位置输入乘数 A>[-]>[-]>, # 在第 3 个位置输入乘数 B,顺便清空第 1、2 个位置[<+<+>>-] # 把 B 复制到第 1、2 个位置<<< # 指针回到第 0 个位置[ # 以 A 为循环计数器 >> # 指针移动到第 2 个位置 [<+>>+<-] # 循环,在第 1 和 3 个位置加 B > # 指针移动到第 3 个位置 [<+>-] # 恢复第 2 个位置的值 <<<- # 循环计数器减 1]>. # 回到第 1 个位置输出 A*B1.3.2. Hello World
[-]>[-]>[-]>[-]>[-]<<<<++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.-------->+.>.有注释的版本(这里用加代替+是为了防止不小心被识别成关键字):
[-] # 清空第 0 个位置>[-]>[-]>[-]>[-] # 清空第 1、2、3、4 个位置<<<< # 指针归位++++++++++ # 把第 0 个位置的值设为 10[ # 把第 0 个位置作为循环计数器 >+++++++ # 第 1 个位置:10*7=70 >++++++++++ # 第 2 个位置:10*10=100 >+++ # 第 3 个位置:10*3=30 >+ # 第 4 个位置:10*1=10 <<<<- # 循环计数器减 1]>++. # 输出 70加2=72(H)>+. # 输出 100加1=101(e)+++++++.. # 输出 101加7=108(l)两次+++. # 输出 108加3=111(o)>++. # 输出 30加2=32(空格)<<+++++++++++++++. #输出 72加15=87(W)>. # 输出 111(o)+++. # 输出 111加3=114(r)------. # 输出 114减6=108(l)--------. # 输出 108减8=100(d)>+. # 输出 32加1=33(!)>. # 输出 10(换行)2. Brainfuck 编译器
听到 Brainfuck 的操作如此简单,相信很多人能几十行代码量写出一个 Brainfuck 解释器。或者一个没有优化而是直接模拟进行操作的编译器。
但是由于 Brainfuck 本身的特性,一旦对于稍微复杂的逻辑,解释运行的效率就会变的极其低下,而且,为了避免在代码里写上 100 个 +,通常会用循环来实现,那效率就更低了。正所谓“工欲善其事,必先利其器”,所以,我写了一个 Brainfuck 编译器,把 Brainfuck 代码直接编译成二进制文件,其中会经过循环展开、常量传播、死代码消除等优化,以提升运行效率。
整体解释器的实现通过 Python 的 llvmlite 库实现。目前实现了一个基本的优化,但是由于我还没来得及学习编译原理,所以代码质量可能不够高,因此我可能需要对代码进一步优化后才会公开。
2.1. 效果展示
[-]>[-]+++[<+++>-]<[>>[-]++++[<++>-]<<-]>.这是一个利用三重循环实现的 Brainfuck 编译器,功能是输出 H,编译过程中生成的中间表示为:
; ModuleID = "brainfuck_compiler"target triple = "unknown-unknown-unknown"target datalayout = ""
define i32 @"main"(){entry: %"memory" = alloca [30000 x i8] %".2" = getelementptr [30000 x i8], [30000 x i8]* %"memory", i32 0, i32 0 %"ptr" = alloca i8* store i8* %".2", i8** %"ptr" call void @"llvm.memset.p0i8.i32"(i8* %".2", i8 0, i32 30000, i1 0) %".5" = load i8*, i8** %"ptr" %".6" = getelementptr i8, i8* %".5", i32 1 store i8* %".6", i8** %"ptr" %".8" = load i8*, i8** %"ptr" %".9" = trunc i32 72 to i8 store i8 %".9", i8* %".8" %".11" = load i8*, i8** %"ptr" %".12" = getelementptr i8, i8* %".11", i32 1 store i8* %".12", i8** %"ptr" %".14" = load i8*, i8** %"ptr" %".15" = trunc i32 0 to i8 store i8 %".15", i8* %".14" %".17" = load i8*, i8** %"ptr" %".18" = getelementptr i8, i8* %".17", i32 -2 store i8* %".18", i8** %"ptr" %".20" = load i8*, i8** %"ptr" %".21" = trunc i32 0 to i8 store i8 %".21", i8* %".20" %".23" = load i8*, i8** %"ptr" %".24" = getelementptr i8, i8* %".23", i32 1 store i8* %".24", i8** %"ptr" %".26" = load i8*, i8** %"ptr" %".27" = load i8, i8* %".26" %".28" = zext i8 %".27" to i32 %".29" = call i32 @"putchar"(i32 %".28") ret i32 0}
declare i32 @"putchar"(i32 %".1")
declare i32 @"getchar"()
declare void @"llvm.memset.p0i8.i32"(i8* %".1", i8 %".2", i32 %".3", i1 %".4")可以看出:三重循环直接被展开成了常量,当然,LLVM 本身对中间表示还有进一步的优化,它的优化能力十分强劲。
IMPORTANT未完待续…
部分信息可能已经过时







