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

Forth 栈式语言

第 50 章的 Brainfuck 是"极简到痛苦",本章的 Forth 则是"极简到优雅"。Forth 用逆波兰表达式(RPN)消除了括号和优先级规则,用字典实现了用户自定义词(函数),用栈统一管理数据和调用。实现一个 Forth 解释器让你深入理解:栈机模型、词法分析、字典查找、以及"代码即数据"的元编程思想。

本章导读

Forth 诞生于 1970 年,最初用于天文台望远镜控制。它的核心哲学:一切皆词(word)——数字是词,运算符是词,控制结构是词,用户定义也是词。解释器的工作就是:分词→查字典→执行。没有类型系统,没有语法树,只有栈和字典。

本章学习目标
  • 理解 RPN(逆波兰表达式)的求值规则
  • 实现数据栈的基本操作(push/pop/dup/swap/drop)
  • 实现内置算术词和比较词
  • 实现 : name ... ; 用户定义词
  • 实现 IF/THEN、DO/LOOP 控制结构
  • 用 dict 实现字典查找

51.1 逆波兰表达式

中缀表达式 (3 + 4) * 2 在 Forth 中写作 3 4 + 2 *:

// 中缀 → RPN 对照
// (3 + 4) * 2  →  3 4 + 2 *
// 10 - 2 * 3   →  10 2 3 * -
// (1 + 2) * (3 + 4)  →  1 2 + 3 4 + *

// RPN 求值过程(3 4 + 2 *):
// 读入 3 → 栈: [3]
// 读入 4 → 栈: [3, 4]
// 读入 + → 弹出 4 和 3,压入 7 → 栈: [7]
// 读入 2 → 栈: [7, 2]
// 读入 * → 弹出 2 和 7,压入 14 → 栈: [14]
// 结果:14

RPN 的优势:不需要括号、不需要优先级规则、天然适合栈机执行。

51.2 数据栈操作

Forth 的核心是一个整数栈。基本栈操作词:

// 栈操作词
// DUP   : a → a a        (复制栈顶)
// DROP  : a →            (丢弃栈顶)
// SWAP  : a b → b a      (交换栈顶两个)
// OVER  : a b → a b a    (复制次栈顶)
// ROT   : a b c → b c a  (旋转三个)
// .     : a →            (弹出并打印)
// .S    : 栈不变          (打印整个栈,调试用)

// 示例:
// 5 DUP * .  →  输出 25(5 的平方)
// 1 2 3 ROT . . .  →  输出 1 3 2

在 xlang 中用 list 实现栈:

list<int> stack = list<int>()

func push(int val)
    stack.push(val)
end

func pop() as int
    assert stack.len() > 0, "栈下溢"
    return stack.pop()
end

51.3 内置词

内置词分为四类:

// 算术词
// +  : a b → a+b
// -  : a b → a-b
// *  : a b → a*b
// /  : a b → a/b
// MOD: a b → a%b
// NEGATE: a → -a

// 比较词(结果为 -1 真 / 0 假)
// =  : a b → flag
// <  : a b → flag
// >  : a b → flag

// 栈操作词
// DUP DROP SWAP OVER ROT

// I/O 词
// .   : 弹出栈顶并打印
// ."  : 打印后续字符串(到 " 为止)
// CR  : 换行
// EMIT: 弹出 ASCII 码并输出字符

51.4 用户定义词

Forth 最强大的特性——用户可以定义新词:

// 定义格式:: 词名 定义体 ;
// 例:定义平方词
// : SQUARE DUP * ;
// 5 SQUARE .  →  25

// 定义组合词
// : CUBE DUP DUP * * ;
// 3 CUBE .  →  27

// 定义带逻辑的词
// : ABS DUP 0 < IF NEGATE THEN ;
// -5 ABS .  →  5

实现原理:定义时把词体(token 列表)存入字典;执行时逐 token 递归解释。

// 字典:词名 → 词体(token 列表)
dict<string, list<string>> userWords = dict<string, list<string>>()

// 解析定义
func parseDefine(list<string> tokens, int pos) as int
    string name = tokens[pos]
    pos += 1
    list<string> body = list<string>()
    while pos < tokens.len() and tokens[pos] != ";"
        body.push(tokens[pos])
        pos += 1
    end
    userWords[name] = body
    return pos + 1   // 跳过 ";"
end

51.5 条件与循环

Forth 的控制结构也是词:

// 条件:flag IF ... THEN
// 栈顶非 0 则执行 IF 和 THEN 之间的词
// : ABS DUP 0 < IF NEGATE THEN ;

// 带 ELSE:flag IF ... ELSE ... THEN
// : MIN 2DUP < IF DROP ELSE NIP THEN ;

// 循环:limit start DO ... LOOP
// 从 start 到 limit-1 循环,I 获取当前索引
// : COUNTDOWN 0 DO I . LOOP ;
// 5 COUNTDOWN  →  0 1 2 3 4

实现 IF/THEN:找到匹配的 THEN 位置,条件为假则跳过:

func execIf(list<string> tokens, int pos) as int
    int flag = pop()
    // 找到匹配的 THEN(处理嵌套)
    int depth = 1
    int thenPos = pos
    while depth > 0
        if tokens[thenPos] == "IF" then depth += 1 end
        if tokens[thenPos] == "THEN" then depth -= 1 end
        thenPos += 1
    end
    if flag == 0 then
        return thenPos   // 条件假,跳到 THEN 之后
    end
    return pos           // 条件真,继续执行
end

51.6 字典与词查找

解释器的核心是字典查找——每个 token 先查内置词,再查用户词,最后当数字处理:

func execute(string token)
    // 1. 尝试作为数字
    int num = 0
    bool isNum = int.tryParse(token, num)
    if isNum then
        push(num)
        return
    end

    // 2. 查内置词
    if execBuiltin(token) then return end

    // 3. 查用户定义词
    if userWords.has(token) then
        list<string> body = userWords[token]
        int i = 0
        while i < body.len()
            // 递归执行词体(处理控制结构需要返回新位置)
            i = execToken(body, i)
        end
        return
    end

    // 4. 未知词
    println("未知词: " + token)
end

51.7 完整实现

// forth.xl — 迷你 Forth 解释器

list<int> stack = list<int>()
dict<string, list<string>> userWords = dict<string, list<string>>()

func push(int v)
    stack.push(v)
end

func pop() as int
    assert stack.len() > 0, "stack underflow"
    return stack.pop()
end

func execBuiltin(string w) as bool
    if w == "+" then push(pop() + pop()); return true end
    // 注意:Forth 的 - 是 a b → a-b,弹出顺序是 b 先出
    if w == "-" then int b = pop(); push(pop() - b); return true end
    if w == "*" then push(pop() * pop()); return true end
    if w == "/" then int b = pop(); push(pop() / b); return true end
    if w == "MOD" then int b = pop(); push(pop() % b); return true end
    if w == "DUP" then int a = pop(); push(a); push(a); return true end
    if w == "DROP" then pop(); return true end
    if w == "SWAP" then int b = pop(); int a = pop(); push(b); push(a); return true end
    if w == "OVER" then int b = pop(); int a = pop(); push(a); push(b); push(a); return true end
    if w == "." then println(pop().toString()); return true end
    if w == "=" then int b = pop(); int a = pop(); if a == b then push(-1) else push(0) end; return true end
    if w == "<" then int b = pop(); int a = pop(); if a < b then push(-1) else push(0) end; return true end
    if w == ">" then int b = pop(); int a = pop(); if a > b then push(-1) else push(0) end; return true end
    if w == "NEGATE" then push(-pop()); return true end
    if w == "CR" then println(""); return true end
    return false
end

func run(string source)
    list<string> tokens = string.split(source, " ")
    int i = 0
    while i < tokens.len()
        string tok = tokens[i]
        if tok == ":" then
            // 用户定义
            string name = tokens[i + 1]
            list<string> body = list<string>()
            i += 2
            while i < tokens.len() and tokens[i] != ";"
                body.push(tokens[i])
                i += 1
            end
            userWords[name] = body
        elif tok == "" then
            // 跳过空 token
        else
            executeToken(tok)
        end
        i += 1
    end
end

func executeToken(string tok)
    int num = 0
    if int.tryParse(tok, num) then
        push(num)
        return
    end
    if execBuiltin(tok) then return end
    if userWords.has(tok) then
        list<string> body = userWords[tok]
        int j = 0
        while j < body.len()
            executeToken(body[j])
            j += 1
        end
        return
    end
    println("未知词: " + tok)
end

// 测试
run("3 4 + .")              // 7
run(": SQUARE DUP * ;")
run("5 SQUARE .")           // 25
run(": CUBE DUP DUP * * ;")
run("3 CUBE .")             // 27

51.8 REPL 交互

把解释器包装为交互式 REPL:

// REPL 模式
println("Mini Forth REPL(输入 QUIT 退出)")
while true
    print("ok> ")
    string line = io.readLine()
    if line == "QUIT" then break end
    if line == ".S" then
        // 打印栈内容
        print("栈: ")
        int i = 0
        while i < stack.len()
            print(stack[i].toString() + " ")
            i += 1
        end
        println("")
    else
        run(line)
    end
end

51.9 测试用例

// 算术
run("2 3 + .")          // 5
run("10 3 - .")         // 7
run("4 5 * .")          // 20
run("15 4 / .")         // 3
run("17 5 MOD .")       // 2

// 栈操作
run("5 DUP + .")        // 10
run("1 2 SWAP - .")     // 1 (2-1)

// 用户定义
run(": DOUBLE 2 * ;")
run("21 DOUBLE .")      // 42

// 嵌套定义
run(": QUAD DOUBLE DOUBLE ;")
run("3 QUAD .")         // 12

// 比较
run("3 4 < .")          // -1 (真)
run("5 2 > .")          // -1 (真)

51.10 扩展方向

  • IF/ELSE/THEN:完整的条件分支
  • DO/LOOP:计数循环,I 获取索引
  • 变量:VARIABLE 定义,@ 读取,! 写入
  • 字符串:." 打印字符串,S" 压入字符串
  • 递归:RECURSE 词调用当前正在定义的词
  • 编译模式:将词体编译为字节码,用虚拟机执行(性能提升 10x)

深入细节

深入 1:Forth 的减法顺序

RPN 中 10 3 - 表示 10 - 3 = 7,不是 3 - 10。弹出时先弹出的是 b(3),再弹出 a(10),计算 a - b。这是 Forth 初学者最常犯的错误——减法和除法的操作数顺序与直觉相反。

深入 2:字典的覆盖语义

重新定义一个已存在的词,新定义覆盖旧定义。但旧定义中如果引用了被覆盖的词,仍然使用旧版本(因为词体存储的是 token 文本,执行时动态查找)。这实现了"热更新"——重定义 SQUARE 后,之前定义的 CUBE(调用 SQUARE)自动使用新版本。

深入 3:Forth 与 xlang 的哲学对比

Forth:无类型、无作用域、全局字典、栈传递一切。xlang:强类型、词法作用域、模块隔离、命名参数。两者代表语言设计的两个极端。理解 Forth 让你更珍惜 xlang 的类型安全和模块化——同时也理解"简单"的代价。

⚠️ 常见错误

❌ 错误 1:减法/除法操作数顺序反

10 3 - 应该是 10-3=7。如果实现为 pop()-pop(),得到的是 3-10=-7。必须先弹出 b,再弹出 a,计算 a-b。

❌ 错误 2:栈下溢不检查

对空栈执行 pop 会导致运行时错误。每次 pop 前应检查 stack.len() > 0,给出清晰的错误信息。

❌ 错误 3:用户定义词中递归导致无限循环

如果词体中引用了词自身(如 : FOO FOO ;),执行时会无限递归。需要加递归深度限制或检测。

❌ 错误 4:分词时空格处理

连续空格会产生空 token。split 后必须跳过空字符串,否则 int.tryParse("") 会失败。

本章小结

主题核心要点
RPN操作数先入栈,运算符弹出操作数计算后压回结果
栈操作DUP/DROP/SWAP/OVER/ROT + push/pop
内置词算术(+ - * / MOD)、比较(= < >)、I/O(. CR EMIT)
用户定义: NAME body ; 存入字典,执行时逐 token 递归解释
控制流IF/THEN 条件跳转,DO/LOOP 计数循环
字典dict<string, list<string>> 存储词名→词体映射
查找顺序数字 → 内置词 → 用户词 → 报错
REPL循环读取行 → 分词 → 执行 → 打印栈

复习题

  1. 将中缀表达式 (5 + 3) * (7 - 2) 转换为 RPN。
  2. Forth 中 10 3 - 的结果是什么?为什么不是 -7?
  3. DUP 和 OVER 的栈效果有什么不同?
  4. : SQUARE DUP * ; 定义后,5 SQUARE 的执行过程是什么?
  5. 用户定义词存储在什么数据结构中?执行时如何查找?
  6. 如果重新定义一个已存在的词,之前引用它的词会怎样?
  7. Forth 的 IF/THEN 中,"真"和"假"分别用什么值表示?
  8. 为什么 Forth 不需要括号和运算符优先级?

编程练习

练习 1:扩展控制流

为解释器添加 IF/ELSE/THEN 和 DO/LOOP 支持。测试:: ABS DUP 0 < IF NEGATE THEN ; 和 : SUM 0 DO I + LOOP ;(计算 0+1+...+n-1)。

参考思路:IF 弹出标志,找 THEN 位置;DO 弹出 limit 和 start,循环执行到 LOOP。

练习 2:变量支持

添加 VARIABLE 词:VARIABLE X 创建变量 X(初始 0),X @ 读取,5 X ! 写入。测试:VARIABLE COUNT 10 COUNT ! COUNT @ .

参考思路:用 dict<string, int> 存储变量值;VARIABLE 后的词名作为键。

练习 3:斐波那契

用 Forth 定义斐波那契函数:: FIB ... ; 使得 10 FIB . 输出 55。可以用递归或迭代实现。

参考思路:迭代版用 SWAP OVER + 循环;递归版需要 IF 判断基准情况。