这是完整的、按时间顺序的学习过程记录。包含所有错误、困惑、修复和顿悟。
$ brew install gforth
==> Pouring gforth--0.7.3_3.arm64_tahoe.bottle.1.tar.gz
🍺 /opt/homebrew/Cellar/gforth/0.7.3_3: 246 files, 4.1MB
安装顺利。版本 0.7.3,2008 年的软件,但依然好用。
3 4 + . \ → 7第一反应:"这也太反人类了。" 操作数在前,操作符在后。但写第二行时开始理解——这不是"反人类",是直接操作计算过程。
: square ( n -- n^2 ) dup * ;
5 square . \ → 25: 和 ; 原来是定义 word 的"语法"。但它们自己也是 word。一切皆 word。
阶乘很顺利:
: factorial dup 1 <= if drop 1 else dup 1- recurse * then ;
10 factorial . \ → 3628800FizzBuzz 的嵌套 IF 很丑(三个 then then then),但能跑。
写完变量和数组后,想在解释模式直接打印数组:
5 0 do i myarr @ . loop报错了!
:1: Interpreting a compile-only word
5 0 >>>do<<< i . loop
原来 do...loop 是编译时 word(compile-only),不能在解释模式直接用。必须包在 : ... ; 里。这是 Forth 编译/解释双模式的第一个坑。
修复:
: show-arr 5 0 do i myarr @ . loop CR ;
show-arr \ 正常工作: array ( n -- ) create cells allot does> swap cells + ;
5 array myarr这是我学 Forth 最兴奋的一刻。一行代码不是创建了一个数组,而是创建了数组这个概念。以后每次说 5 array xxx,就诞生一个新数组。
写了一个迷你音乐记谱语言:
: C 262 ; : D 294 ; \ ...
: quarter 60000 tempo @ / ;
: note ." ♪ " swap . ." Hz for " . ." ms " ;
C quarter note \ ♪ 262 Hz for 500 ms实际运行输出和预期完全一致。改成 160 bpm 后时值自动从 500ms 变成 375ms。
: constant ( n -- ) create , does> @ ;gforth 警告:redefined Constant with constant
我不小心覆盖了 gforth 的内置 constant。在 Forth 里这是合法的——你可以重定义任何东西。但 gforth 会警告你。
我试图写一个对数组每个元素执行操作的 word:
: array-map ( xt n addr -- )
swap 0 do
dup i cells + dup @
rot rot swap >r >r
r@ execute
r> r> !
loop drop ;问题:do...loop 在 return stack 上存储循环计数器,而我又在循环体内用 >r r> 操作 return stack。两者冲突了。这是 Forth 编程的一个著名坑——return stack 是共享资源,do...loop 会偷偷使用它。
我没有在这次学习中解决这个问题。承认失败。
variable state 0 state !gforth 又警告了:redefined state——内置已有 state 变量(用于表示编译/解释模式)。我的代码覆盖了它。虽然在这个实验中没有副作用,但在真实项目中可能导致灾难。
开始读 /tmp/jonesforth/jonesforth.S。前 100 行是 Jones 的哲学宣言:
"You really can write a complete FORTH in, say, 2000 lines of code."
读到 NEXT 宏(第 305 行)时被震撼:
.macro NEXT
lodsl
jmp *(%eax)
.endm两条指令。3 字节。这就是整个 Forth 的执行引擎。
同时启动三个子 agent:
- Alpha → 哲学与历史
- Beta → 动手代码
- Gamma → 源码分析
给每个团队发了我先学到的东西作为"起步指引"。
写了状态机实验(遇到了 redefined state 警告)。
写了 return stack 实验(swap-vars、countdown)。
继续读 JonesForth 的 Direct/Indirect Threaded Code 部分。
收到 17KB 的哲学与历史报告。质量非常高。
收到 534 行的实战报告。写了链表、Sierpinski 三角形(用 bitwise AND)、河内塔。 注意:Beta 的素数筛代码可能有 bug(我验证时它挂起了),但 Sierpinski 和其他实验是正确的。这也是真实的——不是每段代码都完美。
收到 27KB 的技术分析。覆盖了 JonesForth 完整架构、sectorforth(512 字节的 Forth)、bootstrapping 理论。
给 Parafee 写了最终讲解。打包成 GitHub 仓库。
根因:在 do...loop 内用 >r r> 与循环计数器冲突。
修复:用变量 map-xt 和 map-addr 存储执行令牌和基地址,避免 return stack。
额外发现:5 myarr 返回第 5 个元素地址(越界),必须用 0 myarr 获取基地址。编译模式下要用 ['] 而不是 '。
调试过程经历了 3 次失败:
- 第一次:忘了 do...loop 不能在解释模式用
- 第二次:
' double在编译模式下会读取下一个 token 导致空字符串错误 - 第三次:
5 myarr算出越界地址,Invalid memory address - 第四次:全部修复,
['] double 5 0 myarr array-map正确运行
根因:原始代码中 dup +loop 的 dup 位置不对,导致栈上残留值或步长为 0,无限循环。
修复:重写 mark-composites,确保 dup 在 +loop 之前正确提供步长。
输出:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 ✓
根因:不只是 off-by-one。over + swap 的栈操作顺序本身就是错的:
over + swap在(a b)上:(a b a)→(a a+b)→(a+b a)— 旧值在栈顶!tuck +在(a b)上:(b a b)→(b a+b)— 新值在栈顶 ✓
修复:tuck + 代替 over + swap。
验证:fib(10)=55 ✓
答案:
include分文件vocabulary做命名空间- Chuck Moore 法则:每个 word ≤ 一行
- 交互式测试:定义后立即验证
所有修复代码在 experiments/06-solved-problems.fth,全部通过验证。