☰
Forth 语言快速入门:基于 learnxinyminutes-docs 的栈式编程实战指南
2026/10/6 7:29:24 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

Forth 是由 Charles H. Moore 在 20 世纪 70 年代设计的命令式、基于栈(stack-based)的编程语言与编程环境,曾用于 Open Firmware 等固件项目,也被 NASA 采用。本指南以 learnxinyminutes-docs 仓库中的 es/forth.md(西班牙语翻译版)为骨架,对照英文原版 forth.md,用可复制、可运行的 Gforth 示例带你从"压栈、弹栈"起步,逐步掌握算术、栈操作、单词定义、条件、循环、内存与数组、返回栈及浮点运算,最终能够独立阅读和编写简单的 Forth 程序。

背景与文档定位

本仓库(README.md)以"以代码形式书写的文档"为理念,为每种语言提供一份带注释、可直接运行的教学代码。Forth 指南正是这一风格的典型代表:整篇教程几乎全部由"可执行的注释代码"构成,读者可以在 Gforth 交互式环境中逐行敲入验证。

  • 西班牙语版:es/forth.md,由 Horse M.D. 编写、Zach Larsen 翻译,本文即以它为绝对主体;
  • 英文原版:forth.md,用于校准行为细节与术语;
  • 所有文档都带有 YAML frontmatter(如contributors、translators、filename等元数据字段),仓库通过 lint/frontmatter.py 对文档头部进行格式与键值校验,保证教学文档的规范性。

需要说明的是:本指南聚焦Gforth这一主流的 Forth 实现,但文中绝大部分内容在标准 Forth 系统(如 ANS Forth 兼容实现)中同样适用。

运行环境:进入 Gforth 交互式会话

Gforth 是 GNU 项目下的 Forth 实现,安装后在终端输入gforth即可进入交互式解释器(REPL)。每成功执行一条语句,解释器都会回显ok提示符;教程中的注释\ ok正是模拟了这一交互输出。

在会话中常用的环境指令包括:

\ 加载一个 Forth 源文件: \ s" archivodeforth.fs" included \ 列出字典中所有已定义的单词(输出量很大!): \ words \ 清空屏幕: page \ 退出 Gforth: bye

注释语法

Forth 支持两种注释写法:

\ 这是行注释,反斜杠之后到行尾的内容都会被忽略 ( 这也是注释,但通常只在定义单词时用来书写栈效应说明 )

核心模型:参数栈(数据栈)

Forth 的全部编程活动几乎都围绕参数栈(通常简称为"栈")展开。栈是后进先出(LIFO)的数据结构:数值从左到右依次压入栈顶,单词则从栈上取数、运算、再把结果压回栈。

5 2 3 56 76 23 65 \ ok

这一行把 6 个数字依次压栈,栈底在左、栈顶在右。用.s(show stack,显示栈)可以查看当前栈的全部内容:

.s \ <7> 5 2 3 56 76 23 65 ok

尖括号中的<7>表示栈上共有 7 个元素。要点是:在 Forth 中,一切都是单词(word)或数字(number)——单词是已命名的可执行例程,数字则是直接压栈的字面量。

.(读作 "dot")把栈顶元素弹出并打印:

5 4 + \ ok . \ 9 ok

.s与.的组合是调试 Forth 程序的常用手段:先.s观察栈,再.消费栈顶。

基础算术

算术运算(事实上绝大多数需要数据的单词)都是通过操作栈上的数据完成的。加、减、乘、除、取模对应+ - * / mod:

6 7 * . \ 42 ok 1360 23 - . \ 1337 ok 12 12 / . \ 1 ok 13 2 mod . \ 1 ok

注意13 2 mod的结果是1——这是整数除法求余。此外还有符号与极值运算:

99 negate . \ -99 ok -99 abs . \ 99 ok 52 23 max . \ 52 ok 52 23 min . \ 23 ok
  • negate:取栈顶元素的相反数;
  • abs:取绝对值;
  • max/min:取两个数中的较大者 / 较小者。

栈操作基础:dup、swap、rot、drop、nip

处理栈时,下面五个单词是最高频的基础工具:

3 dup - \ dup 复制栈顶:3 - 3 2 5 swap / \ swap 交换前两个元素:5 / 2 6 4 5 rot .s \ rot 把第三个元素旋转到栈顶:4 5 6 4 0 drop 2 / \ drop 丢弃栈顶(不打印):4 / 2 1 2 3 nip .s \ nip 丢弃第二个元素(类似 drop):1 3

逐个拆解:

  • dup:复制栈顶元素(原栈顶成为倒数第二、副本压栈),3 dup -等价于3 3 -;
  • swap:交换栈顶与第二个元素,2 5 swap /变成5 2 /;
  • rot:把第三个元素旋转到栈顶,6 4 5 rot后栈为4 5 6;
  • drop:直接丢弃栈顶元素而不打印,4 0 drop后栈只剩4,再压入2做除法得2;
  • nip:丢弃第二个元素,1 2 3 nip后栈为1 3(注意与drop的方向相反,它保顶弃二)。

进阶栈操作:tuck、over、roll、pick

处理更复杂的栈布局时,还有四个进阶单词。讨论栈索引时,索引从 0 开始,0 表示栈顶:

1 2 3 4 tuck \ tuck 复制栈顶并插入到第二个槽位之下:1 2 4 3 4 1 2 3 4 over \ over 复制第二个元素到栈顶:1 2 3 4 3 1 2 3 4 2 roll \ roll 把指定位置的元素*移动*到栈顶:1 3 4 2 1 2 3 4 2 pick \ pick 把指定位置的元素*复制*到栈顶:1 2 3 4 2
  • tuck:栈1 2 3 4,复制栈顶4并插到第二个槽位之下,结果1 2 4 3 4;
  • over:复制第二个元素3压到栈顶,结果1 2 3 4 3;
  • n roll:把索引n处的元素移动到栈顶(其余元素依次下沉),2 roll把索引 2 处的3移到栈顶,得1 3 4 2;
  • n pick:把索引n处的元素复制一份压到栈顶,2 pick得1 2 3 4 2。

roll与pick的唯一区别是:前者移动(原位置被清空),后者复制(原位置保留)。

定义新单词::与;

Forth 的"函数"叫单词。:使 Forth 进入编译模式,直到遇到;结束定义:

: cuadrado ( n -- n ) dup * ; \ ok 5 cuadrado . \ 25 ok
  • cuadrado是西班牙语"平方"——定义它接收一个数n,用dup *复制自身再相乘,返回n²;
  • 括号中的( n -- n )是栈效应注释:--左侧是输入栈,右侧是输出栈,约定俗成地标注了该单词对栈的净影响(两个n分别指入参和返回值),便于阅读与文档化;
  • 定义完成后,5 cuadrado就像内建单词一样被调用,输出25。

用see可以反汇编查看某个单词的内部结构:

see cuadrado \ : cuadrado dup * ; ok

条件与布尔逻辑

Forth 的布尔约定:-1表示真,0表示假;任何非零值也通常被视为真。比较运算符会把结果以-1或0的形式压栈:

42 42 = \ -1 ok 12 53 = \ 0 ok

if是只能在编译期使用的单词(compile-only),典型结构为if <要做的事> then <程序其余部分>:

: ?>64 ( n -- n ) dup 64 > if ." ¡Más que 64!" then ; \ ok 100 ?>64 \ ¡Más que 64! ok
  • dup 64 >:复制入参并与 64 比较,产生-1/0标志;
  • ." ...":输出后跟字符串(. "之间无空格);
  • if ... then包围真分支。

带else的完整分支:

: ?>64 ( n -- n ) dup 64 > if ." ¡Más que 64!" else ." ¡Menos que 64!" then ; 100 ?>64 \ ¡Más que 64! ok 20 ?>64 \ ¡Menos que 64! ok

循环

有限循环:do ... loop

do同样是编译期单词,它从栈上取两个数:结束值(不含)与起始值(含),即区间[start, end):

: miloop ( -- ) 5 0 do cr ." ¡Hola!" loop ; \ ok miloop \ ¡Hola! \ ¡Hola! \ ¡Hola! \ ¡Hola! \ ¡Hola! ok

5 0 do从 0 迭代到 4,共 5 次,每次cr换行并输出¡Hola!。

循环体内用i取得当前索引值:

: uno-a-12 ( -- ) 12 0 do i . loop ; \ ok uno-a-12 \ 0 1 2 3 4 5 6 7 8 9 10 11 ok

注意do的上界是排除的:12 0 do输出 0 到 11。英文原版 forth.md 还特别提醒:若do的起止值恰好相等,会形成无限循环(直到算术下溢),因此遇到"起止相同也要跳过循环"的需求应使用?do。

条件循环:?do

?do与do类似,但若起始值与结束值相等则直接跳过循环体:

: cuadrados ( n -- ) 0 ?do i cuadrado . loop ; \ ok 10 cuadrados \ 0 1 4 9 16 25 36 49 64 81 ok

这里循环体把索引i的平方逐个打印,得到前 10 个平方数(0、1、4、9、16、25、36、49、64、81)。

自定义步长:+loop

+loop允许在每次迭代末尾加上一个任意增量(含负数),实现"步进":

: treces ( n n -- ) ?do i . 3 +loop ; \ ok 15 0 treces \ 0 3 6 9 12 ok

15 0 treces从 0 开始、每步 +3、小于 15 时继续,输出 0、3、6、9、12。

不定循环:begin ... until

当循环次数无法预知时,使用begin <要做的事> <标志> until:begin标记循环起点,until弹出标志,标志为真(非零)时退出循环:

: death ( -- ) begin ." ¿Ya hemos llegado?" 0 until ; \ ok

这里标志恒为0(假),因此是一个永真循环——正如西班牙语单词名death(死亡)暗示的那样,它会一直输出¿Ya hemos llegado?。

变量、常量与内存

variable声明一个变量,它会在字典中创建名字并分配一个 cell(一个地址单元)的内存:

variable edad \ ok

用!(store,存储)把值写入变量地址,用@(fetch,读取)把变量地址处的值压栈,用?一步完成"读取并打印":

21 edad ! \ ok edad @ . \ 21 ok edad ? \ 21 ok

@与?的区别在于:@只取值压栈,后续可继续参与运算;?则是@与.的组合,直接输出。

常量(constant)与变量类似,但无需关心内存地址——它在编译期把名字直接绑定到数值上:

100 constant PUNTA-QUE-AQUA-HIERVA \ ok PUNTA-QUE-AQUA-HIERVA . \ 100 ok

这里PUNTA-QUE-AQUA-HIERVA(水的沸点,对应英文原版的WATER-BOILING-POINT)一经定义即代表 100,压栈时不会消耗任何运行时内存访问。

数组:连续内存与索引

创建数组与创建变量类似,但需要分配更多内存。allot在变量之后追加分配指定字节数,cells返回一个 cell 的字节数,因此2 cells allot额外分配 2 个 cell,加上变量自带的 1 个 cell,共 3 个 cell:

variable minumeros 2 cells allot \ ok

erase把指定区域清零(参数顺序:地址、字节数):

minumeros 3 cells erase \ ok

或者用fill(参数顺序:地址、字节数、填充值)填充:

minumeros 3 cells 0 fill

更直接的方式是用create+,(comma)在编译期把具体值依次写入连续内存——注意最后一个,必不可少:

create minumeros 64 , 9001 , 1337 , \ ok (¡el último `,` es importante!)

create minumeros 64 , 9001 , 1337 ,等价于手工向每个索引写入:

64 minumeros 0 cells + ! \ ok 9001 minumeros 1 cells + ! \ ok 1337 minumeros 2 cells + ! \ ok

读取指定索引的值时,用cells +计算地址偏移:

0 cells minumeros + ? \ 64 ok 1 cells minumeros + ? \ 9001 ok

每次都写cells +很啰嗦,因此文档封装了一个辅助单词,把"索引 × cell 大小 + 基地址"的偏移计算收敛起来:

: de-arr ( n n -- n ) cells + ; \ ok minumeros 2 de-arr ? \ 1337 ok

它同样可用于写入:

20 minumeros 1 de-arr ! \ ok minumeros 1 de-arr ? \ 20 ok

返回栈:嵌套执行的"指针仓库"

返回栈(return stack)用于在单词执行其他单词(如循环)时暂存指针与临时数据,其读写与参数栈完全独立。

循环中的i本质上就是读取返回栈顶:i等价于r@(return-fetch):

: miloop ( -- ) 5 0 do r@ . loop ; \ ok

除了读取,还可以用>r(to-return)把参数栈顶压入返回栈、用r>(from-return)弹回参数栈:

5 6 4 >r swap r> .s \ 6 5 4 ok

逐步分析:栈5 6 4→>r把4送进返回栈,参数栈剩5 6→swap得6 5→r>把4取回参数栈,最终栈为6 5 4。

重要约定:由于 Forth 用返回栈保存单词的返回指针(调用地址),>r必须始终与后续的r>配对,否则会破坏控制流、导致程序崩溃或行为异常。

浮点运算

大多数 Forth 实现倾向于回避浮点运算(历史上常用于资源受限的嵌入式环境),但 Gforth 提供完整的浮点支持。浮点字面量以e结尾,相关单词通常以f前缀命名:

8.3e 0.8e f+ f. \ 9.1 ok
  • f+:浮点加法(对应整数栈上的+);
  • f.:弹出浮点栈顶并打印。

浮点数据使用独立的浮点栈,因此存储到变量时要配套使用f!/f@:

variable miflotantevar \ ok 4.4e miflotantevar f! \ ok miflotantevar f@ f. \ 4.4 ok

实用收尾命令

最后是一些日常有用的系统级单词:

\ 输入不存在的单词会清空参数栈;不过也有专门的清栈单词: clearstack \ 清空屏幕: page \ 列出字典中所有单词(列表会非常长): \ words \ 退出 Gforth: \ bye

进一步学习

  • 本指南的完整可执行源码即是仓库中的 es/forth.md,英文原版见 forth.md,两者互为对照,可逐行在 Gforth 中运行验证;
  • 仓库根目录的 README.md 说明了该系列教程"以带注释的可运行代码讲解语言"的文档理念与贡献方式(参见 CONTRIBUTING.md);
  • 原文档"Listo Para Más"(准备了解更多)部分推荐了经典读物Starting Forth、Simple Forth与Thinking Forth——前者适合从零入门,后两者分别侧重简洁实现与编程思想,是深入理解 Forth 哲学(最小化、词表驱动、栈机模型)的进阶路径。

至此,你已经走完了从"往栈上放数"到"定义单词、控制流程、操作内存、使用返回栈与浮点栈"的完整闭环。下一步,不妨把教程中的示例逐条敲进gforth,再尝试用: ... ;组合出自己的第一个 Forth 单词。

  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

相关推荐

上一篇:message-pusher灾备方案:数据中心故障应对
下一篇:为什么选择Charty?Jetpack Compose图表库性能对比与优势分析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询