第 50 章 / 共 58 章 卷十 代码讲解

Brainfuck 解释器

Brainfuck 是世界上最著名的极简编程语言——只有 8 条指令,却图灵完备。实现一个 Brainfuck 解释器是理解"虚拟机"和"指令分发"的最佳入门项目:代码量极小(核心不到 50 行),但涵盖了解释器的全部核心概念——指令指针、数据磁带、循环跳转、I/O 处理。本章从零构建一个完整的 Brainfuck 解释器,并加入跳转表优化。

本章导读

解释器(Interpreter)是"读取程序并执行"的程序。与编译器不同,解释器不生成目标代码,而是逐条读取指令、查表分发、执行操作。Brainfuck 的 8 条指令让这个过程极其直观——没有类型系统、没有变量名、没有函数调用,只有指针移动、字节加减和循环跳转。

本章学习目标
  • 理解 Brainfuck 的 8 条指令和磁带模型
  • 实现指令分发主循环(switch 模式)
  • 用跳转表预编译括号匹配,将循环从 O(n) 降到 O(1)
  • 处理 I/O:输出字符、读取输入
  • 实现错误检测:括号不匹配、指针越界

50.1 Brainfuck 语言规范

Brainfuck 只有 8 条有效指令,其他字符全部忽略(视为注释):

指令含义类比
>指针右移一格ptr++
<指针左移一格ptr--
+当前格 +1*ptr++
-当前格 -1*ptr--
.输出当前格(ASCII)putchar(*ptr)
,读入一个字节到当前格*ptr = getchar()
[若当前格为 0,跳到匹配的 ] 之后while(*ptr) {
]若当前格非 0,跳回匹配的 [ }

一个 Hello World 程序:

++++++++[>++++[>++>+++>+++>+<<<<-]>+>->+.>>.<-.<<<++++++++++.>>.+++.------.--------.>>+.>++.

看起来像乱码?没关系——这正是 Brainfuck 的设计目的:用最少量的语义构建图灵完备计算。

50.2 磁带模型

Brainfuck 的"内存"是一条无限长的字节磁带,初始全为 0:

// 磁带:30000 字节(标准实现)
array<int> tape = array<int>(30000)   // 初始化为 0
int ptr = 0                             // 数据指针,指向当前格

// 程序存储为字符串
string program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>->+.>>.<-.++++++++++.>>.+++.------.--------.>>+.>++."
int pc = 0                              // 程序计数器(指令指针)

三个核心状态:

  • tape:数据磁带,30000 个字节格
  • ptr:数据指针,指向当前操作的格子
  • pc:程序计数器,指向当前执行的指令

50.3 解释器主循环

解释器的核心是一个 while 循环 + 指令分发:

func interpret(string code, string input) as string
    array<int> tape = array<int>(30000)
    int ptr = 0
    int pc = 0
    int inputPos = 0
    string output = ""

    while pc < code.len()
        int ch = code[pc]

        if ch == 62 then            // '>'
            ptr += 1
        elif ch == 60 then          // '<'
            ptr -= 1
        elif ch == 43 then          // '+'
            tape[ptr] = (tape[ptr] + 1) % 256
        elif ch == 45 then          // '-'
            tape[ptr] = (tape[ptr] + 255) % 256
        elif ch == 46 then          // '.'
            output += string.char(tape[ptr])
        elif ch == 44 then          // ','
            if inputPos < input.len() then
                tape[ptr] = input[inputPos]
                inputPos += 1
            else
                tape[ptr] = 0       // EOF → 0
            end
        elif ch == 91 then          // '['
            if tape[ptr] == 0 then
                // 跳到匹配的 ] 之后
                int depth = 1
                while depth > 0
                    pc += 1
                    if code[pc] == 91 then depth += 1 end
                    if code[pc] == 93 then depth -= 1 end
                end
            end
        elif ch == 93 then          // ']'
            if tape[ptr] != 0 then
                // 跳回匹配的 [
                int depth = 1
                while depth > 0
                    pc -= 1
                    if code[pc] == 93 then depth += 1 end
                    if code[pc] == 91 then depth -= 1 end
                end
            end
        end
        // 其他字符忽略(注释)

        pc += 1
    end

    return output
end

注意 ASCII 码:'>' = 62,'<' = 60,'+' = 43,'-' = 45,'.' = 46,',' = 44,'[' = 91,']' = 93。xlang 的字符串索引返回字符的 ASCII 值。

50.4 括号匹配与跳转表

基础版每次遇到 [ 或 ] 都要线性扫描找匹配括号——O(n) 复杂度。优化:预计算跳转表:

// 预编译:构建跳转表
func buildJumpTable(string code) as array<int>
    array<int> jumps = array<int>(code.len())
    array<int> stack = array<int>(256)   // 括号栈
    int sp = 0

    int i = 0
    while i < code.len()
        if code[i] == 91 then        // '['
            stack[sp] = i
            sp += 1
        elif code[i] == 93 then      // ']'
            sp -= 1
            int open = stack[sp]
            jumps[open] = i          // [ → 匹配的 ]
            jumps[i] = open          // ] → 匹配的 [
        end
        i += 1
    end

    return jumps
end

有了跳转表,循环跳转变成 O(1) 数组查找:

// 优化后的 [ 和 ] 处理
elif ch == 91 then          // '['
    if tape[ptr] == 0 then
        pc = jumps[pc]      // O(1) 跳到匹配的 ]
    end
elif ch == 93 then          // ']'
    if tape[ptr] != 0 then
        pc = jumps[pc]      // O(1) 跳回匹配的 [
    end
end

对于深层嵌套循环(如 Sierpinski 三角形),这个优化可以将性能提升 10-100 倍。

50.5 完整解释器

组合所有部分,加上错误检查:

// bf.xl — Brainfuck 解释器(带跳转表优化)

func checkBrackets(string code) as bool
    int depth = 0
    int i = 0
    while i < code.len()
        if code[i] == 91 then depth += 1 end
        if code[i] == 93 then depth -= 1 end
        if depth < 0 then
            println("错误:多余的 ] 在位置 " + i.toString())
            return false
        end
        i += 1
    end
    if depth != 0 then
        println("错误:未闭合的 [,缺少 " + depth.toString() + " 个 ]")
        return false
    end
    return true
end

func buildJumpTable(string code) as array<int>
    array<int> jumps = array<int>(code.len())
    array<int> stack = array<int>(1024)
    int sp = 0
    int i = 0
    while i < code.len()
        if code[i] == 91 then
            stack[sp] = i
            sp += 1
        elif code[i] == 93 then
            sp -= 1
            jumps[stack[sp]] = i
            jumps[i] = stack[sp]
        end
        i += 1
    end
    return jumps
end

func run(string code, string input) as string
    if not checkBrackets(code) then return "" end

    array<int> jumps = buildJumpTable(code)
    array<int> tape = array<int>(30000)
    int ptr = 0
    int pc = 0
    int inputPos = 0
    string output = ""

    while pc < code.len()
        int ch = code[pc]
        if ch == 62 then ptr += 1
        elif ch == 60 then ptr -= 1
        elif ch == 43 then tape[ptr] = (tape[ptr] + 1) % 256
        elif ch == 45 then tape[ptr] = (tape[ptr] + 255) % 256
        elif ch == 46 then output += string.char(tape[ptr])
        elif ch == 44 then
            if inputPos < input.len() then
                tape[ptr] = input[inputPos]
                inputPos += 1
            else
                tape[ptr] = 0
            end
        elif ch == 91 then
            if tape[ptr] == 0 then pc = jumps[pc] end
        elif ch == 93 then
            if tape[ptr] != 0 then pc = jumps[pc] end
        end
        pc += 1
    end
    return output
end

// 测试
string hello = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>->+.>>.<-.++++++++++.>>.+++.------.--------.>>+.>++."
println(run(hello, ""))   // Hello World!

50.6 输入输出处理

Brainfuck 的 I/O 模型:

  • 输出(.):将当前格的值作为 ASCII 码输出一个字符
  • 输入(,):读取一个字节到当前格;EOF 时写入 0(标准行为之一)
// 输出:tape[ptr] → ASCII 字符
elif ch == 46 then
    output += string.char(tape[ptr])

// 输入:从预定义的 input 字符串读取
elif ch == 44 then
    if inputPos < input.len() then
        tape[ptr] = input[inputPos]
        inputPos += 1
    else
        tape[ptr] = 0   // EOF 处理
    end
EOF 的三种约定

Brainfuck 标准未规定 EOF 行为。常见实现:① 写入 0(本章采用);② 写入 -1(即 255);③ 保持当前值不变。不同程序可能依赖不同约定。

50.7 测试:Hello World

// 经典 Hello World
string hello = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>->+.>>.<-.++++++++++.>>.+++.------.--------.>>+.>++."
string result = run(hello, "")
assert result == "Hello World!\n", "Hello World failed"
println("✓ Hello World: " + result)

// 执行过程解析(前几个字符):
// ++++++++ → tape[0] = 8
// [        → 开始循环(tape[0] != 0)
// >++++    → tape[1] += 4
// [>++>+++>+++>+<<<<-] → 内层循环:tape[1] 次,给 tape[2-5] 累加
// ...
// 最终 tape[1]='H'(72), tape[2]='e'(101), tape[3]='l'(108)...

50.8 测试:Cat 程序

Cat 程序:读入什么就输出什么(直到 EOF):

// Cat:读一个字符,输出,循环直到 EOF(0)
string cat = ",[.,]"
string input = "Hello xlang!"
string result = run(cat, input)
assert result == "Hello xlang!", "Cat failed"
println("✓ Cat: " + result)

// 执行过程:
// , → 读入 'H'(72) 到 tape[0]
// [ → tape[0] != 0,进入循环
// . → 输出 'H'
// , → 读入 'e'(101)
// ] → tape[0] != 0,跳回 [
// ... 重复直到读入 EOF(0)
// ] → tape[0] == 0,退出循环

50.9 调试与错误处理

添加调试模式和错误检测:

// 带调试输出的解释器
func runDebug(string code, string input, bool verbose) as string
    // ... 同 run() ...
    // 在每条指令后打印状态:
    if verbose then
        println("pc=" + pc.toString() +
            " ch=" + string.char(ch) +
            " ptr=" + ptr.toString() +
            " tape[ptr]=" + tape[ptr].toString())
    end
end

// 常见错误检测
func validate(string code) as bool
    // 1. 括号匹配
    if not checkBrackets(code) then return false end

    // 2. 指针越界检查(静态分析困难,运行时检测)
    // 在 run() 中加入:
    // if ptr < 0 or ptr >= 30000 then
    //     println("运行时错误:指针越界 pc=" + pc.toString())
    //     return ""
    // end

    return true
end

50.10 扩展方向

  • 编译器:将 Brainfuck 编译为 C 代码(每条指令映射为 C 语句),用 TCC 即时编译执行
  • 优化器:合并连续的 +/-/>/<(如 +++ → tape[ptr]+=3),消除 [] 空循环
  • 调试器:单步执行、断点、磁带可视化
  • Brainfuck 变体:Befunge(二维)、Ook!(用 "Ook." 代替符号)、Brainfuck-16(16 位格)
  • 自解释器:用 Brainfuck 写一个 Brainfuck 解释器(约 4000 字符)

深入细节

深入 1:为什么 Brainfuck 是图灵完备的

Brainfuck 的磁带等价于图灵机的纸带,指针等价于读写头,[ ] 循环等价于条件跳转。理论上它可以计算任何可计算函数——只是写起来极其痛苦。图灵完备性不关乎效率,只关乎能力边界。

深入 2:跳转表 vs 线性扫描的性能差异

线性扫描:每次遇到 ] 都向前找 [,嵌套深度 d 的循环每次跳转 O(d)。跳转表:预计算 O(n),运行时 O(1)。对于 Sierpinski 三角形(嵌套 5 层,执行数百万次循环),跳转表版本快 50 倍以上。

深入 3:字节溢出的处理

Brainfuck 的格子是 8 位无符号整数(0-255)。255 + 1 = 0(回绕),0 - 1 = 255。本章用 (val + 1) % 256 和 (val + 255) % 256 实现回绕。某些实现用有符号字节(-128 到 127),行为不同。

⚠️ 常见错误

❌ 错误 1:] 的跳转方向搞反

[ 在 tape[ptr]==0 时向后跳到 ];] 在 tape[ptr]!=0 时向前跳到 [。搞反会导致无限循环或跳过循环体。

❌ 错误 2:忘记 pc += 1

主循环末尾必须 pc += 1。跳转后也需要 +1(跳到 ] 的下一个位置开始执行)。忘记会导致重复执行同一条指令。

❌ 错误 3:字节溢出未处理

tape[ptr] += 1 在 255 时应变为 0,不是 256。不处理回绕会导致输出错误字符。

❌ 错误 4:括号栈溢出

深层嵌套(如 1000 层)可能超出栈数组大小。使用足够大的栈(1024+)或动态 list。

本章小结

主题核心要点
指令集8 条:> < + - . , [ ],其他字符为注释
磁带30000 字节格,初始为 0,指针 ptr 定位当前格
主循环while pc < len:读指令 → 分发执行 → pc++
跳转表预计算 [ ↔ ] 映射,运行时 O(1) 跳转
I/O. 输出 ASCII;, 读入字节;EOF → 0
回绕(val+1)%256 和 (val+255)%256 实现 8 位回绕
验证括号匹配检查 + 运行时指针越界检测
测试Hello World、Cat、Sierpinski 三角形

复习题

  1. Brainfuck 的 8 条指令分别对应什么操作?哪些是 I/O 指令?
  2. [ 和 ] 的跳转条件分别是什么?它们组合实现了什么控制结构?
  3. 跳转表相比线性扫描的优势是什么?预计算的时间复杂度是多少?
  4. 为什么 tape[ptr] = (tape[ptr] + 255) % 256 等价于减 1?
  5. EOF 时 , 指令写入 0 和写入 255 会导致什么不同的程序行为?
  6. 如何检测 Brainfuck 程序的括号是否匹配?
  7. 为什么说 Brainfuck 是图灵完备的?它等价于什么计算模型?
  8. 如果要实现 Brainfuck 调试器,需要保存哪些状态?

编程练习

练习 1:Sierpinski 三角形

运行经典的 Sierpinski 三角形 Brainfuck 程序,验证输出正确。测量基础版(线性扫描)和跳转表版的执行时间差异。

参考思路:Sierpinski 程序约 200 字符;用 time.now() 计时对比。

练习 2:指令合并优化

实现预处理器:将连续的 + 合并为 ADD n,连续的 > 合并为 MOVE n。把 ">>>" 变为一条 MOVE 3 指令,减少主循环迭代次数。验证优化后输出不变。

参考思路:预处理阶段扫描连续相同指令,生成 (op, count) 对;主循环按 count 批量执行。

练习 3:Brainfuck 调试器

实现单步调试模式:每执行一条指令暂停,打印 pc、当前指令、ptr、tape[ptr] 和前 10 个格子的值。支持 "step"(执行一步)和 "run"(运行到结束)命令。

参考思路:主循环中每步后打印状态;用 io.readLine() 读取用户命令。