Atlast 手册

按:Atlast 是 Autodesk 公司创始人、AutoCAD 原作者之一 John Walker 开发的工具包,用于给普通的应用增加可编程能力。本文是 Atlast 1.2 版(32 位)手册的中文版,在公有领域发布。


Atlast 是一项旨在使软件组件技术和开放式架构应用在主流软件市场普及的尝试。它既是一个可轻松集成到现有应用中的软件组件,为其提供现成的宏语言及用户扩展与定制功能;同时也是一个基础平台,能以开放、面向组件的方式构建新应用。

Atlast 是 Autodesk 穿线式语言应用系统工具包(Autodesk Threaded Language Application System Toolkit)的缩写。该工具包由 John Walker 在 Autodesk 公司期间开发。在 1991 年,Autodesk 公司将 Atlast 的版权归还给了原作者,作者将其在公有领域发布。Atlast 与 Autodesk 只有历史性联系:Autodesk 公司既不为其背书,也未实际使用它,更不对其提供技术支持。

Atlast 基于 FORTH-83 语言,但进行了大量扩展和修改,以更好地实现其作为开放式可编程应用嵌入式工具包的使命。该实现采用单文件便携式 C 语言编写,已移植到多种计算机和操作系统,包括 MS-DOS、OS/2、Macintosh 及各类 Unix 机器等。Atlast 原生支持浮点数、类 C 字符串、Unix 兼容文件访问,以及丰富的应用嵌入功能。整数为 32 位(64 位版本中为 64 位),标识符最长 127 个字符;提供全面的栈和堆指针检查以辅助调试。编译时可配置仅包含特定应用所需功能,从而节省内存并提升执行速度(当关闭错误检查时)。


开放、可编程的产品优于甚至能取代设计最精良的封闭式应用程序。通过单个可移植的 C 文件实现的穿线式语言,几乎能让任何现有或新开发的程序变得可编程、可扩展,并向用户增强开放。
—— John Walker,1990 年 3 月 9 日

您或许以为我们早已吸取教训。从诞生之初,Autodesk 就将 AutoCAD® 定位为开放可扩展的系统。我们历经五年艰难斗争,才让这一离经叛道的理念最终获胜。如今,几乎所有行业分析人士都认同:AutoCAD 的成功及 Autodesk 取得的成就,其开放架构的贡献远超设计中的任何其他单一因素。

然而时至今日,我们仍在编写一个又一个封闭程序——用户无法编程,不修改源代码就无法扩展。如果我们基于对市场激励机制的理解,从理性上相信开放系统更优,并通过 AutoCAD 的成功验证了这一假设,那么唯一剩下的问题就是:为什么? 为何不让每个程序都成为开放程序?

答案很简单:因为这很难!传统上,编写封闭程序在开发周期的每个阶段都省力得多:设计更简单、代码量更少、文档更精简、测试阶段考量因素大幅减少。此外,封闭产品被认为对支持的要求更低——尽管后文将论证这一假设可能并不成立。

通往可编程性的痛苦之路

大多数程序最初都是不可编程的封闭应用,随后艰难地通过引入有限的脚本或宏功能迈向可编程,继而发展出日益复杂的解释型宏语言。这种语言随着用户需求的增长而野蛮生长,缺乏连贯设计。最终,程序或许会配备与 C 等现有语言的绑定接口。

另一种方案是采用标准语言作为产品的宏语言。在经历了至今仍困扰我们的糟糕菜单宏语言初期尝试后,AutoCAD 选择了整合 David Betz 的 XLISP——一个简单的 Lisp 解释器。Autodesk 随后对其扩展,增加了浮点数支持、大量 Common Lisp 函数,并最终实现了对 AutoCAD 数据库的访问。

这种方案优势显著:首先,选择标准语言让用户可利用现有书籍和培训资源进行基础学习;而专用宏语言的开发者必须从头创建所有资料。其次,解释型语言的所有程序都以 ASCII 码表示,这使其天然具备跨计算机和操作系统的可移植性。只要解释器能在新系统运行,其支持的程序基本都能正常工作。第三,多数现有语言已发展至设计棱角被磨平的阶段。与沿袭语言设计者思路进行扩展相比,通过零散功能临时堆砌宏语言很容易酿成难以理解的灾难。

但解释器实在太慢了。简单计算每条程序指令所需的额外指令开销就能明白:没有解释器适合严肃计算。当解释器仅作为宏语言使用时,这可能无关紧要。例如早期 AutoLISP® 程序大部分时间都通过 (command) 函数向 AutoCAD 提交命令,程序执行时间完全由 AutoCAD 执行命令的耗时主导,而非 AutoLISP 构建与提交命令的时间。然而当应用需要大量计算时(如 AutoCAD AEC 中的参数化对象计算),AutoLISP 的开销就变成了近乎难以承受的沉重负担。显然的替代方案是提供编译型语言——但这同样存在问题。

Atlast 简介

Atlast™ 是一套让应用程序具备可编程能力的工具包。它经过精心设计,能轻松集成到现有程序或新开发项目中,只需开发者付出极少显性努力,即可为任何引入它的程序带来可编程性的核心优势。事实上,当你开始在设计周期中“以 Atlast 思维”思考时,很可能会发现自己的程序设计构建方式发生了根本性转变。我逐渐将 Atlast 视为“以程序为食的怪兽”——因为将其纳入程序后,往往能大幅减少原本需要编写的专用代码量,同时使最终应用具备开放性、可扩展性,更易适配事件驱动范式等其他运行环境。

这种可移植工具包的理念——集成到各类产品中,使其共享统一编程语言——在权衡其优势后显得理所当然。令人惊讶的是,此类方案在业界并不常见。实际上,在我曲折的行业生涯中,Atlast 唯一真正的前身是 1970 年代中期由马里兰大学的 Kern Sibbald 和 Ben Cranston 开发的通用宏包。该方案在 Univac 主机上实现,为文本编辑器、调试器、文件转储器和排版语言等学校里使用的众多工具提供了共享宏语言。尽管 Atlast 在结构和运行方式上与这个解释型字符串语言截然不同,但跨产品宏语言的概念及其价值认知,都可追溯至这一源头。

那么 Atlast 究竟是什么?本质上…它就是 Forth 的变体。我深知提及 Forth 会引发强烈排斥反应——其剧烈程度或许仅次于令人闻风丧胆的“LISP”一词。事实上,在初次接触 Forth 十二年后,我才真正开始“领悟”其本质:理解其核心思想、真实优劣势,以及它能提供独特解决方案的场合。PostScript 以及我 1988 年分离 AutoCAD 用户界面与几何引擎的失败尝试(Leto 协议项目),促使我重新审视 Forth。Leto 项目让我意识到:要创建不会无限膨胀、复杂难懂且性能低下的界面,必须在核心嵌入可编程性——提供一组可由用户界面模块组合成高阶操作的原语,通过组件间链接调用。这种可编程性必须以可移植形式实现,且不涉及将用户代码连接到 AutoCAD 核心。

在寻找类似解决方案时,PostScript 展现出相似的动机与成效。(尽管有人批评其性能,但我认为问题更多源于图形原语执行速度和低效 ASCII 输入,而非语言本身。)PostScript 确实轻松击败了 Impress、DDL 等竞争对手,其成功再次证明:开放可编程产品总能战胜“功能全面”但不可扩展的方案。

研究 PostScript 必然回溯至其灵感来源——Forth。尽管 Forth 以晦涩著称,且吸引了不少偏执追随者,但其诸多特质使其成为实现应用程序可编程化的理想候选:

  • 小巧。Forth 最小实现极其精简,因为大部分语言可用少量基础原语自我定义。即便是包含浮点运算、数学函数、字符串、文件 I/O、编译器工具、用户定义对象、数组、调试工具和运行时检测的完整实现,其源代码行数仍仅为内置函数少得多的 Lisp 解释器的五分之一,目标代码体积不足 70%。运行时内存需求通常仅为 Lisp 的 1%-2%,甚至经常远低于 C 等编译语言。令人震惊的是,包含浮点运算、C 语言全部数学函数、文件 I/O 和字符串等功能的解释/编译混合语言,竟能构建成仅 50,964 字节的 DOS 大模式可执行文件。

  • 快速。Forth 作为穿线式语言,程序执行不依赖源码级别的解释,而是简单的内存加载与间接跳转。即便对计算密集型代码,其速度损失通常仅为原生编译器的 5-8 倍。虽然看似代价高昂,但相比逐记号解释的 Lisp 解释器 60-70 倍的性能损失,以及许多应用程序宏语言源码解释器的更差表现,这已属优异。多数场景下,Forth 与编译代码执行速度基本相当——尤其当 Forth 主要作为宏语言调用编译语言编写的应用原语时。

  • 可移植。只要在实现中严格规范内存架构与数据类型(这几乎不影响速度),Forth 程序可实现 100% 跨实现兼容。程序能以 ASCII 文件形式在系统间通用交换。借助 Forth 创建对象的能力,应用中定义的数据类型可以自动继承其底层数据类型的可移植性。

  • 易扩展。得益于极简底层架构(与诸如 Lisp 解释器的复杂结构截然不同),任何合格的 C 程序员稍加培训即可在数小时内为 C 实现的 Forth 添加用 C 编写的原语。这些原语能以全速运行,同时可以用于构建任何 Forth 程序,支持参数化、在定义中使用、循环调用等等。这催生了全新的应用构建方式:不再将结构与原语作为统一过程编程,而是先构建应用专属原语并交互测试,再根据效率、安全性及原语开放程度等考量,用 Forth 或 C 编写的胶水代码组装应用。与传统开发不同,这些决策不是非此即彼的选择,而是允许产品沿连续谱定位,后续根据市场反馈调整。

  • 交互性。虽然 Forth 程序中的大部分会编译成与机器码同等紧凑高效的形态,但只需将用户键盘连接至解释器,即可随时提供直接交互(或反之,不需要直接交互时,可以随时切断连接)。相比传统的“编辑—编译—连接—调试”循环,这种交互性显著加速开发进程。Forth 能在不牺牲执行速度的前提下实现这一点,正是其核心吸引力之一所在。

  • 支持多范式操作。掌握在 Forth 环境中封装产品功能的技巧后,就能构建这样的程序:核心功能(如数据库访问、几何计算、图形结果显示、质量属性计算等)可组合成序列,通过程序调用、命令行交互、菜单选择或对话框按钮触发等任意方式激活。由于任何影响程序的激励都只是执行 Forth 词(word),而这些词可用少量 Forth 文本重定义,设计者可自由选择让实现者、第三方开发者或用户编程控制这些操作模式。

  • 出人意料的现代性。尽管 Forth 看似 64K 计算机和电传打字机时代的遗物,但用现代眼光审视,其诸多概念仍极具前瞻性。例如,很少有语言能像它那样定义新基础数据类型及操作方法。Forth 的多词典机制支持创建默认继承父属性的对象,并以高效方式实现这类结构。

Atlast 与 Forth

尽管具备上述优势,Forth 在现代编程环境中仍存在一些显著缺陷。在设计 Atlast 时,我尽可能遵循 Forth 的规范,同时坚持核心目标:创建一个能让开发者从应用程序中剥离可编程性,并将其交由标准模块管理的系统——正如 C 程序员将 I/O 和数学函数求值委托给专用库例程那样。

Atlast 基于 FORTH-83 标准,并整合了该标准中许多可选的扩展和补充词。掌握标准 Forth 与 Atlast 的基本差异后,用户可借助标准 Forth 的参考手册完成大多数 Atlast 编程任务。FORTH-83 与 Atlast 的主要区别如下:

  • 32 位整数:在 1990 年推出另一种受限于 16 位整数的语言是不可想象的。我们正快速进入一个时代:绝大多数 C 语言环境已达成共识将 int 类型定为 32 位,应用软件也将迅速适配这一标准。因此 Atlast 中所有整数均为 32 位,不提供 short 类型。需注意这并不与 16 位 int 的 C 环境冲突——例如 Atlast 能完美兼容 MS-DOS 的 Turbo C 和 OS/2 的 Microsoft C,因为所有整数均显式声明为 long 类型。

  • 任意长度标识符:Atlast 消除了 Forth 程序员在内存效率与标识符唯一性之间的权衡困境。标识符长度仅受内置的记号缓冲区大小的限制(默认 128 字符),且所有字符均有效。这一改变使 Atlast 更符合现代语言设计理念。借助底层的 C 运行时环境,ATLAST 的符号名称不放在 Forth 堆内,而放在动态分配的缓冲区中。这简化了堆大小调整(同时改变了某些涉及系统底层结构的细节),不过 Forth 能实现的功能在 Atlast 中均可实现,只是方式略有不同。

  • 浮点支持:提供浮点常量、变量、运算符、输入输出工具及丰富数学函数作为原语(不需要时可在编译时关闭)。与 C 兼容,默认浮点类型为 64 位双精度数。Atlast 对浮点格式的唯一假设是其大小为整型的两倍。未实现 Forth 的有理数功能。

  • 增强的字符串支持:Atlast 的字符串支持层级远高于 Forth。Atlast 采用通用且显式的方式提供字符串字面量,转义字符的语法与 C 语言相同,同时提供与 C 高度一致的字符串处理函数集(STRCPYSTRCATSTRLEN 等)。循环分配的临时字符串缓冲区机制为交互式输入提供更灵活的字符串操作。字符串仍遵循 C 与 Forth 共用的指针-缓冲区模型,字符串密集型程序的运行速度与 C 或 Forth 版本相当。

  • 调试工具:Atlast 可在编译时根据应用集成需求和产品开发阶段配置不同级别的错误检查与调试支持。开发测试阶段可启用:逐原语跟踪执行的 TRACE 功能、错误时打印活动词栈的 WALKBACK、对求值栈和返回栈的精确溢出检测、以及近乎防弹的指针检查(捕获对堆外区域的存取)。即便在 MS-DOS 等无保护环境中,能绕过检查造成破坏的错误也极为罕见。结合 Atlast 的交互特性,形成了友好的调试环境。所有运行时检查均可适时关闭以降低开销。

  • 遵循 C/Unix 文件输入输出规范:Forth 诞生于标准化操作系统时代之前,早期其本身就是许多小微型机的操作系统。如今 Unix 文件系统接口已成为事实工业标准,Atlast 遵循这一模型:FILE 变量对应 C 的文件描述符,FOPENFCLOSEFREADFSEEK 等原语用法与 C 一致。Atlast 提供行级 I/O 支持,可以像 AutoCAD 一样自动识别各类行尾格式的 ASCII 文件。

  • 深度嵌入支持:与 Forth 不同,Atlast 的设计初衷是隐形地嵌入到应用程序中。除了为可编程性和扩展性提供通用框架外,应用程序仍保持其原有外观,而非呈现 Atlast 或 Forth 的特征。因此,Atlast 并不像 Forth 系统的主循环那样“掌控全局”,而是作为从属角色,由应用程序在适当时机调用。实现这一目标需要反转典型 Forth 系统的控制结构,并提供一套完整的 C 语言可调用接口,供应用程序与 Atlast 交互。此外,系统还提供了有助于根据宿主程序精确需求定制 Atlast 的原语。开发者可以监控内存使用情况,记录哪些原语被使用或闲置,并配置出完美契合宿主程序需求及运行环境的定制版 Atlast。

关于后续内容的说明

本文后续将通过大量 Atlast 代码示例进行说明。具备 Forth 基础的读者结合文末的原语定义应能理解示例内容。若初次接触 Forth,示例可能看似天书。不必担忧——掌握要领后(或参考优秀 Forth 书籍,如《Mastering Forth》),自会豁然开朗。

在此之前,请勿因示例却步。您可先略读这些代码,权当已理解其意。您仍能领略该 Atlast 的特质、其与应用程序的整合方式以及可实现的功能。我曾幻想能让大脑和手指通宵工作,次日清晨便能在计算机上看到一本独立成册的完整 Atlast 参考手册。可惜我缺乏夜间批量处理的能力,目前也无暇在黄金时段完成这项工程。现决定以这种非常规的残缺形式提供文档,旨在向能理解要义的读者传递核心内容,而非等到我能完成上百页——实则大部分重复 Forth 参考手册内容——的完整文档后再行发布。

交互式 Atlast

尽管 Atlast 的设计初衷是嵌入应用程序中,但为了学习它本身、试验小程序或将其用作桌面计算器,拥有一个独立的交互式版本非常方便。Atlast 源码包中包含一个主程序 atlmain.c,可与 Atlast 连接生成此类工具。该可执行文件在 Unix 下名为 atlast,在 MS-DOS 下为 atlast.exe,并启用了所有错误检查功能以辅助程序开发。

要体验 Atlast,请运行交互程序:

atlast

当 Atlast 处于解释状态时,您会看到提示符:

->

例如,您可以加载 Atlast 并尝试 π 的不同有理数近似值:

% atlast
-> 22.0 7.0 f/ f.
3.14286 -> 377.0 120.0 f/ f.
3.14167 -> ^D
%

注意:Atlast 输出后不会自动换行,如需换行请使用 CR 原语。与其手动打印每个数并与 π 对比,我们可以定义 π 的常量值和一个新词(或函数),用于计算与 π 的误差并打印残差。示例如下:

% atlast
-> 1.0 atan 4.0 f* 2constant pi
-> : pierr
:>   pi f- fabs f. cr
:> ;
-> 3.0 pierr
.141593
-> 22.0 7.0 f/ pierr
0.00126449
-> 355.0 113.0 f/ pierr
2.66764e-07
-> ^D 
%  

我们还可以将文件中的程序加载到交互式 Atlast 中。假设要研究莱布尼茨 1673 年提出的著名级数(该级数以极其缓慢的速度收敛于 π):

π/4 = 1 − 1/3 + 1/5 − 1/7 + 1/9 − …

用文本编辑器创建包含以下内容的文件:

\   Series approximations of Pi

\   Leibniz: pi/4 = 1 - 1/3 + 1/5 - 1/7 ...

: leibniz      ( n -- fpi )
    1.0 1.0
    4 pick 1 do
        2.0 f+  \ denom += 2
        2dup
        i 1 and if
            fnegate
        then
        1.0 2swap f/
        2rot f+
        2swap
    loop
    2drop
    rot drop
    4.0 f*
;

\   Reference value of Pi

1.0 atan 4.0 f* 2constant pi

\ Calculate and print error

: pierr
    pi f- fabs f. cr
;

若这段代码看似天书,别担心!回想您初次接触 Lisp 或 C 程序时的感受。若想在学习语言前解析部分结构,可参考手册末尾的 Atlast 原语定义,记住 Atlast 是逆波兰表达式的栈语言,且 “\” 是行注释符,“(” 会忽略后续文本直到 “)”。

将文件保存为 leibniz.atl 后,可通过命令加载到交互式 Atlast:

atlast -ileibniz

Atlast 会编译文件中的程序,报告错误,若无错误则进入交互解释模式。leibniz 根据栈顶数字执行指定次数的迭代,并将 π 的级数逼近结果留在栈顶。

测试示例:

% atlast -ileibniz
10 leibniz f.
3.04184 -> 100 leibniz f.
3.13159 -> 1000 leibniz f.
3.14059 -> 10000 leibniz f.
3.14149 ->

可见收敛速度确实缓慢。由于支持即时定义新编译词,我们可以临时编写一个打印每 10,000 次迭代结果及误差的程序:

-> : itest 0 do i 1+ 10000 * dup .
:> leibniz 2dup f. pierr  loop ;
-> 5 itest
10000 3.14149 0.0001 
20000 3.14154 5e-05 
30000 3.14156 3.33333e-05 
40000 3.14157 2.5e-05 
50000 3.14157 2e-05 
-> ^D
%

(即使不理解代码)可以看到我们无缝混合了已经编译的代码、解释执行代码,以及即时定义并编译的函数。

通过命令行指定文件名可直接以批处理模式运行 Atlast 程序。例如在 leibniz.atl 末尾添加:

\   Run iteration vs. error report

: itest
    0 do 
        i 1+ 10000 * dup . leibniz
        2dup f. pierr
    loop
;

10 itest  

另存为 leibbat.atl 后,批处理执行结果如下:

% atlast leibbat 
10000 3.14149 0.0001 
20000 3.14154 5e-05 
30000 3.14156 3.33333e-05 
40000 3.14157 2.5e-05 
50000 3.14157 2e-05 
60000 3.14158 1.66667e-05 
70000 3.14158 1.42857e-05 
80000 3.14158 1.25e-05 
90000 3.14158 1.11111e-05 
100000 3.14158 1e-05 
% 

(显然这不是计算 π 的高效方法!若需精确计算 π,可尝试以下田村—金田快速 π 算法:)

\   Tamura-Kanada fast Pi algorithm

2variable a
2variable b
2variable c
2variable y

: tamura-kanada ( n -- fpi )
    1.0 a 2!
    1.0 2.0 sqrt f/ b 2!
    0.25 c 2!
    1.0
    rot 1 do
        a 2@ 2dup y 2!
        b 2@ f+ 2.0 f/ a 2!
        b 2@ y 2@ f* sqrt b 2!
        c 2@ 2over a 2@ y 2@ f-
        2dup f* f* f- c 2! 2.0 f*
    loop
    2drop
    a 2@ b 2@ f+ 2dup f* 4.0 c 2@ f* f/
;

调试

作为一门交互式语言,Atlast 自然提供了调试支持。您可以通过启用 TRACE 功能逐词追踪程序执行过程。要开启追踪,请输入以下命令:

1 trace

如果您已加载如下阶乘函数定义:

: factorial
        dup 0= if
           drop 1
        else
           dup 1- factorial *
        then
;

并在追踪模式下执行,您将看到如下输出:

% atlast -ifact
-> 1 trace
-> 3 factorial .

Trace: FACTORIAL 
Trace: DUP 
Trace: 0= 
Trace: ?BRANCH 
Trace: DUP 
Trace: 1- 
Trace: FACTORIAL 
Trace: DUP 
Trace: 0= 
Trace: ?BRANCH 
Trace: DUP 
Trace: 1- 
Trace: FACTORIAL 
Trace: DUP 
Trace: 0= 
Trace: ?BRANCH 
Trace: DUP 
Trace: 1- 
Trace: FACTORIAL 
Trace: DUP 
Trace: 0= 
Trace: ?BRANCH 
Trace: DROP 
Trace: (LIT) 1 
Trace: BRANCH 
Trace: EXIT 
Trace: * 
Trace: EXIT 
Trace: * 
Trace: EXIT 
Trace: * 
Trace: EXIT 
Trace: . 6 -> ^D
%

可以用“0 trace”关闭追踪功能。

当错误发生时,系统通常会打印回溯信息,列出从错误发生点开始的活跃词,沿着嵌套层级直至最外层的解释层级。若配置了 WALKBACK 包,默认会打印回溯信息。用“0 walkback”禁用该功能。以下是错误回溯报告的示例:

% atlast -ileibniz
-> leibniz
Stack underflow.
Walkback:
   ROT
   LEIBNIZ
->

集成 Atlast

与大多数语言不同,Atlast 并非作为主程序构建,而是一个子例程。您可以在应用程序中随时随地调用它,根据需要提供或多或少的可编程性。在深入探讨应用程序与 Atlast 的接口细节前,通过示例展示一个能访问前述所有 Atlast 功能的简单程序很有意义。以下主程序与 Atlast 目标模块连接后,即构成一个功能完整的交互式 Atlast 解释器。它虽缺少交互式 Atlast 的增强功能(如控制台中断处理、批处理模式、加载定义文件、编译状态提示等),但通过输入重定向提交的任何能被交互式 Atlast 运行的程序,该程序均可执行。

#include <stdio.h>
#include "atlast.h"
int main()
{
    char t[132];
    atl_init();
    while (printf("-> "),
           fgets(t, 132, stdin) != NULL)
        atl_eval(t);
    return 0;
}

配置 atlast.c

集成 Atlast 的第一步是构建适合与应用程序链接的 atlast.c 版本。为此需选择 Atlast 的构建模式,这些模式通常通过 C 编译器命令行提供的编译时定义指定。除非单独配置 Atlast 子包,否则将构建全功能版本。此时只需关注以下编译时 C 预处理器宏的设置:

  • ALIGNMENT
    若双精度浮点数需按 8 字节边界对齐内存,则定义 ALIGNMENT。未定义时,Atlast 默认 4 字节对齐足够。(atldef.h 中的条件代码会尝试在需要对齐的处理器上定义ALIGNMENT,但可能遗漏某些机型。)

  • COPYRIGHT
    若需将 Atlast 的公有领域声明嵌入二进制程序,则定义 COPYRIGHT。否则不定义以节省空间。

  • EXPORT
    若仅将 Atlast 作为宏引擎调用且无需访问其内部数据结构,则保持 EXPORT 未定义。若需添加应用专用原语(多数情况如此),则定义 EXPORT 并在所有需访问的模块中包含atldef.h 文件。此时栈、返回栈和堆指针将转为外部可见,Atlast 内部符号名将重命名为以 atl_ 开头的特殊名称以避免冲突,同时启用额外接口代码使原语能完全访问Atlast运行时环境。

  • MEMSTAT
    若需启用运行时内存使用监控(通过 MEMSTAT 原语或 atl_memstat() 函数访问),则定义 MEMSTAT

  • NOMEMCHECK
    定义 NOMEMCHECK 可禁用所有运行时栈、堆和指针检查。这将显著提升执行速度,但仅应在确保所有错误已排除的封闭应用中启用。启用 NOMEMCHECK 后,Atlast 程序的安全性将不高于普通 C 程序。

  • READONLYSTRINGS
    启用 WORDSUSED 包时,Atlast 会追踪程序中使用的原语和用户定义词,以便确定所需包及测试覆盖率。此功能通过修改词定义标志实现,对内置原语需修改 C 常量字符串。若C实现不允许此操作,则定义 READONLYSTRINGS 将预定义词复制到可修改的动态分配缓冲区中。注意此功能仅在启用 WORDSUSED 包时生效。

在 MS-DOS 或 16 位 OS/2 上构建 Atlast 时,必须使用大模式(32 位数据地址)。Atlast 将所有整数视为 32 位并假定数据指针长度至少为此值。尝试用 16 位数据地址构建将引发违反设计假设的编译错误。

初始化:atl_init()

在应用程序调用其他 Atlast 功能前,必须调用 atl_init() 初始化动态存储并创建用于求值的数据结构。

使用默认内存配置初始化 Atlast 只需调用:

atl_init();

此时将创建栈、返回栈、堆和初始词典,Atlast 进入可执行状态。通过在调用 atl_init() 前设置以下变量(定义于 atlast.h 中)可调整内存分配大小:

  • atl_stklen 求值(数据)栈长度,以 4 字节栈项为单位。默认 100。

  • atl_rstklen 返回栈长度,以 4 字节返回栈指针项为单位。默认 100。

  • atl_heaplen 堆长度,以 4 字节栈项为单位。默认 1000。

  • atl_ltempstr 临时字符串缓冲区长度,用于解释模式下输入的字符串及某些原语创建的字符串。默认 256。

  • atl_ntempstr 临时字符串数量。临时字符串循环使用,若未保存旧结果时超出此数量,最早的结果将被覆盖。默认 4。

应用程序可通过 PROLOGUE 包允许加载的 Atlast 程序覆盖默认内存分配设置。深度嵌入式应用(如烧录至 ROM 的程序)可将 Atlast 动态存储区分配给预定义内存区域而非 malloc() 申请。若在调用 atl_init() 前将某区域基地址指针设为非零值,则使用指定地址且不分配缓冲区。使用此功能时请仔细阅读 atlast.catl_init() 的代码,确保提供的内存区域符合各长度单元要求。特别注意系统状态字、临时字符串缓冲区和堆会被合并为连续内存区域。

求值:atl_eval()

要对包含 Atlast 程序文本的字符串进行求值,调用:

stat = atl_eval(string);

其中 string 是待求值的字符串,stat 为整数,表示求值器的状态。状态码的助记符定义于 atlast.h 中,其含义如下:

状态码 意义
ATL_SNORM 无错误
ATL_STACKOVER 栈溢出
ATL_STACKUNDER 栈下溢
ATL_RSTACKOVER 返回栈溢出
ATL_RSTACKUNDER 返回栈下溢
ATL_HEAPOVER 堆溢出
ATL_BADPOINTER 无效堆指针
ATL_UNDEFINED 未定义词
ATL_FORGETPROT 尝试 FORGET 受保护符号
ATL_NOTINDEF 只能在编译模式下使用的词在定义之外出现
ATL_RUNSTRING 字符串没有结束符
ATL_RUNCOMM 文件中的注释没有结束符
ATL_BREAK 接收到异步中断信号
ATL_DIVZERO 尝试除以零

除这些状态码外,调用 atl_eval() 的程序可通过检查外部变量确定 Atlast 的当前状态。若正在等待以“)”终止的多行注释,atl_comment 非零。若当前有待处理的词定义(冒号定义),变量 state(仅在定义了 EXPORT 且包含 atldef.h 时可访问)非零。

加载文件:atl_load()

要加载包含 Atlast 程序文本的整个文件,调用:

stat = atl_load(file);

其中 file 为 C 文件描述符(类型 FILE * ),指向已打开且位于待加载程序首字节前的文件。程序被读取后,stat 为该文件加载并执行 Atlast 程序的结果状态。状态码与上述 atl_eval() 函数相同。atl_load() 函数可读取 AutoCAD 能够识别的任何行尾约定的文本文件;任何 Atlast 实现均可加载这些格式的 ASCII 文件。若宿主系统要求在文件打开时标识二进制文件,需通过 atl_load() 加载的 Atlast 程序文件(即使只包含 ASCII 文本)也应以二进制模式打开。二进制模式能正确解析 AutoCAD 接受的所有行尾分隔符。

atl_load() 函数使用 atl_mark() 保存加载前的运行时状态。若发生错误,函数尝试通过执行 atl_unwind() 恢复原状。若加载的文件包含修改堆上既有对象的解释模式代码,加载过程中出错时这些更改不会被撤销。

标记:atl_mark()

应用程序可能需执行一系列可能导致运行时求值错误的 Atlast 操作。此时,程序通常希望撤销出错程序进行的定义。为在运行高风险 Atlast 程序前标记当前位置,使用:

atl_statemark mk;
atl_mark(&mk);

栈、返回栈、堆和词典的当前位置将保存于 atl_statemark 结构中。后续的 atl_unwind() 调用会将各动态存储区域回滚至 atl_mark 标记的位置。

撤销更改:atl_unwind()

要回滚栈、返回栈、堆分配和词典的所有更改至通过 atl_mark() 保存在 atl_statemark 类型对象中的状态,调用:

//atl_statemark mk;
atl_unwind(&mk);

所有存储区域的分配指针会被重置为调用 atl_mark() 时的位置,但通过指针在 atl_mark() 之后对堆变量进行的存储更改不会撤销。

异步中断:atl_break()

Atlast 的交互式应用必须允许用户跳出无限循环或其他意外触发的长时间计算。若系统支持响应用户中断请求,Atlast 可通过 atl_break() 机制终止其控制下的程序执行。

若编译时定义了 BREAK,则启用 atl_break() 函数及异步中断支持。当应用收到异步中断时,应调用 atl_break() 通知当前运行的 Atlast 程序中断信号。若信号触发时无 Atlast 程序运行,调用无害。应用中处理中断的例程应始终调用 atl_break(),而非尝试判断 Atlast 是否活跃。若中断信号触发时有 Atlast 程序正在执行,宿主应用无论是通过 atl_eval()atl_load() 还是 atl_exec() 调用 Atlast 程序,都将通过返回的 ATL_BREAK 状态获知异常终止。

atl_break() 函数仅设置一个由 Atlast 解释器内循环检查的标志,并不实际终止执行。因此可安全地在任意时刻调用,甚至可在硬件中断服务例程中调用。

显示内存状态:atl_memstat()

在优化集成 Atlast 的应用并准备发布的最后阶段,可能需要调整内存分配参数以消除浪费空间,同时为发布后的用户扩展预留合理余量。明智设置参数需了解应用的基础内存使用情况。若编译 atlast.c 时定义了 MEMSTAT,可通过执行 Atlast 程序内的 MEMSTAT 原语或应用中在适当时机调用 atl_memstat() 函数获取该信息。两种方式均会向标准输出流写入类似下例的内存使用报告:

                 Memory Usage Summary
    
                  Current  Maximum  Items   Percent
    Memory Area    usage    used  allocated in use 
     Stack            0        9     100       0
     Return stack     0        4     100       0
     Heap           227      227    1000      22

注意:使用下面的函数前,必须用 EXPORT 定义编译 atlast.c 及调用它们的模块,并在调用文件中包含头文件 atldef.h

查找词:atl_lookup()

应用可通过以下调用按解释器相同的搜索顺序查询 Atlast 词典中的词:

dictword *dw;
char *name;

dw = atl_lookup(name);

由于 Atlast 匹配名称时不区分字母大小写,name 可包含任意大小写组合。若词已定义,则返回其词典条目。dictword 结构定义于 atldef.h 中。若词未定义,则返回 NULL。一个词可能存在多重定义;此时仅返回最近的定义(活跃定义)。仅凭 atl_lookup() 无法定位被隐藏的定义。

访问词的主体部分:atl_body()

Atlast 词定义由多个组件构成,包括其名称和实现该词的 C 编码方法。对于与 Atlast 交互的应用程序而言,最值得关注的是词的主体部分。对于变量或常量,主体部分即存储其值的空间。要获取由 atl_lookup() 返回或通过 atl_vardef() 创建的词典项(见下文)的主体部分的地址(体地址),可使用 atl_body()。其调用方式如下:

dictword *dw;
stackitem *si;

si = atl_body(dw);

该调用将词典项 dw 的体地址存入变量 si。若需在 Atlast 词的主体部分中存储非默认类型 stackitem(其定义为 long)的数据,需将指针转换为正确的指针类型。下文atl_vardef() 的示例展示了如何使用 atl_body() 创建并初始化浮点变量。

定义变量:atl_vardef()

共享变量是实现宿主应用与 Atlast 间交互的便捷方式。通过使应用的状态对 Atlast 程序可见且可修改,程序既能获取所需信息,又能指导应用程序行为。共享变量是由应用程序定义的 Atlast 变量,其地址通过词典被 Atlast 知晓,同时通过创建时返回的指针被应用程序掌握。创建共享变量的调用如下:

dictword *var;

var = atl_vardef(name, size);

其中 name 为指向变量名称的字符指针,size 为指定变量字节大小的整数。注意:创建标准 Atlast 整型变量时 size 应为 4;浮点变量则为 8 字节。变量存储空间分配在 Atlast 堆上。若堆空间不足则返回 NULL,否则返回变量词典条目的地址。需注意:词典条目并非变量值的存储地址,获取后者需调用前述 atl_body()

例如,以下代码创建了一个存储 π 的粗略近似值的浮点变量:

dictword *pi;

pi = atl_vardef("Pi", sizeof(double));
if (pi == NULL) {
    printf("Can't atl_vardef PI.\n");
} else {
    *((double *) atl_body(pi)) =
        3.141596235;
}

随后可通过运行以下 Atlast 程序打印该值:

pi 2@ f.

执行词:atl_exec()

若已获取 Atlast 词定义的词典条目地址,应用程序可通过以下方式执行它:

dictword *dw;
int stat;

stat = atl_exec(dw);

返回的状态码 statatl_eval() 相同。atl_eval()atl_exec() 的区别微妙但关键——它可能显著影响应用性能。若已知 Atlast 词名称,可通过两种方式执行:向atl_eval() 传递包含名称的字符串,或将词典地址保存至变量后直接用 atl_exec() 执行。两种操作结果相同,但使用 atl_eval() 时,Atlast 需扫描字符串、解析字符串内容为表示词的记号、查词典后执行。若已知词典条目地址,使用 atl_exec() 可跳过这些耗时步骤。

巧妙结合 atl_lookup()atl_exec() 能极大增强应用程序的灵活性。若应用程序执行明确任务,可在主循环前通过 atl_lookup() 检查用户是否定义了特定系列的词。若存在定义,则将词典地址保存至应用代码的指针中。随后在执行的每个关键节点,应用程序只需检测该节点关联的词是否被定义,若存在则用 atl_exec() 执行。若默认处理逻辑已通过atl_primdef()(见下文)定义为 Atlast 原语,Atlast 程序可轻松在“挂钩点”检查数据、执行定制逻辑,或通过调用已定义的原语继承默认处理行为。用户未定义特殊处理时,应用程序仅需进行一次 NULL 指针比对。相比开放架构的优势,这一开销微不足道。

atl_exec() 调用的定义传递参数有两种方式:存入 atl_vardef() 创建的共享变量,或在执行前压入栈中(这是更推荐的方式)。关于从 C 语言访问栈的细节,请参阅下文atl_primdef() 的讨论。

定义原语:atl_primdef()

Atlast 的强大之处主要源于其能轻松将 C 语言编写的原语添加到语言中。一旦集成,这些原语可以与现有的循环、条件执行等功能结合使用。Atlast 经过精心设计,使得添加原语变得简单且安全——完全不像在 AutoLISP 中添加函数那样充满风险。尽管如此,扩展任何语言都需要了解其内存架构和控制结构。因此,请仔细听讲,跟随示例操作,很快你就能像专业人士一样添加原语了。

Atlast 原语是一个 C 函数。执行原语时,该函数被调用,并可以执行任意操作。原语可以简单到丢弃栈顶元素,也可以复杂到从三维几何模型生成光线追踪位图。大多数原语通过栈相互通信,部分原语还会访问堆中存储的变量。极少数原语会操作返回栈上的数据( Atlast 用返回栈跟踪执行的嵌套)。用户定义的原语很少需要访问返回栈。atldef.h 中的定义简化了对这些内存区域的访问。让我们逐一来看。

访问栈

栈指针变量名为 stk,始终指向下一个可用的栈项(数据类型为 long)。原语很少直接引用 stk,因为使用隐藏了栈索引复杂性的定义通常更方便。以下是 Atlast 提供的栈访问工具:

  • Sl(n)
    在访问栈上任何项之前,必须确认栈中至少有需要访问的那么多项,否则需报告栈下溢。在原语开头使用语句“Sl(n);”,其中 n 为原语中将要使用的栈项数。例如,若使用栈顶两项 S0S1,则写“Sl(2);”。务必使用该定义而非直接检查栈限制——若后续构建应用时关闭栈检查,Sl() 语句将不生成代码,自动优化原语速度。

  • So(n)
    在压入新项前,必须确保栈不会溢出。若可能溢出,需报告栈溢出。在原语开头使用“So(n);”,n 为待压入的新项数。例如,压入一个整型项则写“So(1);”。使用该定义而非直接检查栈限制的原因同上——关闭栈检查时 So() 不生成代码。

  • S0S5
    S0S5 提供对栈顶 6 个整型项的直接访问。S0 为栈顶项,S1 为次顶项,以此类推。这些定义可用于赋值语句左右两侧。

  • Pop
    作为语句使用。“Pop;”丢弃栈顶项。

  • Pop2
    作为语句使用。“Pop2;”丢弃栈顶两项。

  • Npop(n)
    丢弃栈顶 n 项。

  • Push
    用于赋值左侧时,将右侧值存入下一个空闲栈项并递增栈指针。

  • Realsize
    对使用浮点数的原语,Realsize 表示一个浮点数占用的栈项数。若原语需栈上两个浮点参数并保留它们,同时新增一个浮点结果,应以“Sl(2*Realsize); So(Realsize);”开头。

  • REAL0REAL2
    这些定义提供对栈顶三个浮点数的只读访问,栈项自动转换为 double 类型。必须以此方式访问浮点值——某些计算机要求 double 按 8 字节对齐,REALn 定义会根据机器需求自动对齐变量。

  • SREAL0(f), SREAL1(f)
    作为函数使用时,这些定义将浮点参数存入栈顶(SREAL0)或次顶(SREAL1)浮点项。由于需补偿机器对齐限制,REALn 定义不可用于赋值左侧,应改用这些函数。

  • Realpop
    弹出栈顶浮点值,等效于 Npop(Realsize)

  • Realpop2
    弹出栈顶两个浮点值,等效于 Npop(2*Realsize)

他说这很简单!” 请耐心些——实际使用远比解释起来更简单(也更紧凑)。若难以忍受,可直接跳至示例原语定义自行体会。好了,欢迎回来。你为 Atlast 添加的 95% 的原语可能仅涉及栈访问。堆和返回栈访问极少见(可能预示设计不佳)。如需操作,方法如下。

访问堆内存

堆是用于分配静态对象的内存池。大部分堆空间由 Atlast 的定义词分配,例如 VARIABLECONSTANT 以及用于定义新可执行词的冒号(:),这些定义词本身也存储在堆中。Atlast 能够直接创建新数据类型的定义词是其最强大的特性之一,这减少了用户原语操作堆的需求。堆通过一组与栈操作类似的定义进行访问。堆指针本身命名为 hptr,但很少被显式引用。

  • Ho(n)
    在堆上存储新数据前,必须确认该操作不会导致堆超过其分配的最大容量。这种情况称为堆溢出,Ho(n) 函数会检查此情况并在溢出发生时终止程序执行。参数 n 表示您计划分配的堆空间量,以栈项为单位(每项 4 字节)。若需按字节数分配,必须将其向上取整至 4 的倍数。可移植的实现方式是使用表达式:((x+(sizeof(stackitem)−1))/sizeof(stackitem)),其中 x 为所需堆空间的字节数。若关闭堆栈检查以追求最高性能,Ho(n) 不会生成任何代码。
  • Hpc(ptr)
    堆存储通常通过栈传递的指针访问。由于栈包含多种数据类型,意外将非指针值作为堆地址使用可能导致灾难性后果。在使用任何值作为堆指针前,应调用 Hpc(ptr) 进行验证(ptr 为待检查指针)。若指针不在堆范围内,将报告错误指针并终止执行。若关闭堆栈检查,Hpc(ptr) 不会生成任何代码。
  • Hstore
    用于赋值语句左侧,将右侧的长整型值存入下一个可用堆单元,并推进堆分配指针。

访问返回栈

返回栈记录定义间的调用关系、跟踪循环控制索引,并存储解释器内部的其他数据。随意操作返回栈通常极为危险。此处说明主要是为了保持文档完整性及为 atlast.c 中维护返回栈的代码提供文档,而非鼓励使用返回栈。返回栈指针变量名为 rstk,始终指向下一个可用返回栈项。返回栈项类型为 **dictword(理解了吗?),该类型也被 typedefrstackitem

原语很少直接引用 rstk,因为使用封装了返回栈索引复杂度的定义更为便捷。以下工具提供返回栈访问功能:

  • Rsl(n)
    访问返回栈项前,必须确认栈中至少有 n 个可用项。否则需报告返回栈下溢错误。在原语起始处使用语句“Rsl(n);”,其中 n 为需引用的返回栈项数。例如引用顶层两项 R0R1 时,应使用 Rsl(2)。务必使用该定义而非直接检查返回栈限制——若后续构建应用程序时关闭栈检查,Rsl() 语句不会生成代码,自动实现原语最高速运行。

  • Rso(n)
    向返回栈压入新项前,必须确认添加后不会超出分配空间。否则需报告返回栈溢出错误。在原语起始处使用语句“Rso(n);”,其中 n 为待压入的新项数。例如压入一项时使用Rso(1)。务必使用该定义而非直接检查返回栈限制——若后续构建应用程序时关闭栈检查,Rso() 语句不会生成代码,自动实现原语最高速运行。

  • R0R2
    定义R0R1R2 提供对返回栈顶层三项的直接访问。R0 为栈顶项,R1 为次顶项,R2 为第三项。这些定义可用于赋值语句的任意一侧。

  • Rpop
    作为语句使用。“Rpop;”移除返回栈顶项。

  • Rpush
    用于赋值语句左侧,将右侧值存入下一个空闲返回栈项,并递增返回栈指针。

实现原语

每个原语都由声明为 “static void” 的 C 函数实现。头文件 atldef.h 将“prim”定义为该类型,以更明确地标识实现原语的函数。

以一个简单原语为例,我们添加获取 Unix 格式日期时间的能力,并从 Unix 日期字中提取小时、分钟和秒。我们将向 Atlast 添加两个新原语:TIME,它在栈顶留下自 1970 年 1 月 1 日午夜以来的秒数;以及 HHMMSS,输入为 TIME 返回的值,在栈顶三个位置留下该时间表示的小时、分钟和秒,其中秒位于最顶部。

以下是实现 TIME 原语的 C 函数:

prim ptime()
{
    So(1);
    Push = time(NULL);
}

由于要在栈上放置一个新项,因此调用 So(1) 检查栈溢出。完成后,我们只需在赋值左侧使用 Push 来存储 Unix 兼容的 time() 函数返回的长整型时间值(大多数非 Unix C 库也支持此函数)。

HHMMSS 原语稍微复杂一些。它使用 Unix 兼容的 localtime() 函数,该函数输入一个指向包含 time() 返回格式的时间值的指针,返回一个指向内部静态结构的指针,该结构的字段给出该时间表示的日、月、年、小时、分钟、秒等。原语的定义如下:

prim phhmmss()
{
    struct tm *lt;

    Sl(1);
    So(2);
    lt = localtime(&S0);
    S0 = lt->tm_hour;
    Push = lt->tm_min;
    Push = lt->tm_sec;
}

此原语期望栈上有一个参数(时间值),因此它以 Sl(1) 开始以验证其存在。它将用小时替换该值,并为分钟和秒向栈添加两个新项,因此接下来使用 So(2) 确保这些添加不会导致栈溢出。现在它可以开始工作。它调用 localtime(),传递第一个栈项(时间词)的地址,然后将小时存储回该词,并使用 Push 两次添加分钟和秒。

通过将编码完成的函数列在原语定义表中并调用 atl_primdef() 函数向 Atlast 注册该表,原语正式添加到 Atlast 中。两个新原语的定义表如下:

static struct primfcn timep[] = {
    {"0TIME",   ptime},
    {"0HHMMSS", phhmmss},
    {NULL,      (codeptr) 0}
};

primfcn 结构在 atldef.h 中声明。您可以在表中列出任意数量的原语,表的末尾以 NULL 代替原语名称的条目标记。对于每个原语,需创建一个包含两个部分的条目:第一个部分为一字符串,若原语为普通词则以“0”开头,若为编译时立即词则以“1”开头,其余部分为全大写的原语名称;第二个部分是实现该原语的函数名称。通过调用 atl_primdef() 并传递表地址来定义表中的原语,如下所示:

atl_primdef(timep);

(给 MS-DOS 用户的注意事项:为节省内存,Atlast 会使用您在原语表中声明的实际静态字符串作为其创建的词典条目的一部分。由于 Atlast 词典将包含指向这些编译时字符串的指针,因此切勿将原语表的数据放置在可能被交换出的覆盖区中,否则当 Atlast 后续尝试搜索词典时可能无法访问。如果您的程序未使用覆盖数据段的技巧,则无需担心此问题。)

可以在调用 atl_init() 之后的任何时间调用 atl_primdef(),并且可以使用不同的primfcn 表多次调用它。如果 primfcn 表中的名称与内置 Atlast 原语或先前通过atl_primdef() 调用定义的原语名称重复,则先前的定义将被隐藏且无法访问。

安装这些新原语后,现在可以从 Atlast 交互式地试用它们。

% atlast
-> time .
634539503 -> time .
634539505 -> time .
634539508 -> time .s
Stack: 634539512 -> hhmmss
-> .s
Stack: 20 58 32 -> clear time hhmmss .s
Stack: 20 58 44 -> clear
-> time hhmmss .s
Stack: 20 58 52 -> ^D
%

一切似乎都按预期运行。我们的新原语起作用了!

最后,让我们看一个更复杂的涉及浮点数的原语。再次回到 π 的莱布尼茨级数,这里是一个用于计算它的原语函数的 C 语言定义。该函数与我们之前在 Atlast 中实现的函数兼容:它期望堆栈顶部有项数,并将 π 的近似值作为浮点值返回到堆栈顶部的两个项中。

prim pleibniz()
{
    long nterms;
    double sum = 0.0,
           numer = 1.0,
           denom = 1.0;

    Sl(1);
    nterms = S0;
    Pop;

    So(Realsize);
    Push = 0;
    Push = 0;
    while (nterms-- > 0) {
        sum += numer / denom;
        numer = -numer;
        denom += 2.0;
    }
    SREAL0(sum * 4.0);
}

该函数首先使用 Sl(1) 验证项数参数存在于栈上。它加载该参数(S0)并将其保存在循环计数 nterms 中。然后使用 Pop 从栈中丢弃它。接下来,So(Realsize) 验证在压入实数结果时栈不会溢出(回想一下,Realsize 是每个浮点结果占用的栈项数——始终为 2,但使用该定义使代码更易读)。我们随即利用 Realsize 为 2 的特性,通过两次 Push 操作为结果分配栈空间并将其清零。完成后,函数进入循环,对请求的级数项数进行求和。最后,使用 SREAL0() 将结果存储到栈顶部的浮点值中:即我们通过两次 Push 创建的那个值。

该原语通过以下序列在 Atlast 中声明并注册:

static struct primfcn pip[] = {
    {"0LEIBNIZ", pleibniz},
    {NULL, (codeptr) 0}
};
atl_primdef(pip);

通过 C 语言实现的原语,我们可以探索这个糟糕级数的极限。例如,这里用它来打印前五十万项后的误差:

% atlast
-> 2variable pi
-> 1.0 atan 4.0 f* pi 2!
-> pi 2@ f. c 
3.14159
-> 500000 leibniz pi 2@ f- f. cr
-2e-06
-> ^D
%

从这些示例原语的简洁性和直接性可以看出,向 Atlast 添加原语并不复杂或困难。从 Atlast 执行原语函数而非从 C 程序调用它,其开销仅涉及几条指令。若需实现与 Atlast 以更复杂方式交互的原语,最佳信息来源是 atlast.c 的源代码;找到一个参数和结果与你计划添加的原语类似的标准原语,查阅其实现函数。这应能消除关于栈和堆操作细节的任何困惑。

包配置

除了全局配置参数外,您可以通过创建名为 custom.h 的自定义配置文件,并使用 -DCUSTOM 编译器标志编译 atlast.c,精确选择构建应用程序版本时要包含的 Atlast 组件。自定义配置文件的格式如下:

#define INDIVIDUALLY
#define Package_1
#define Package_2
              
#define Package_n

Package_i 定义用于选择您希望在应用程序中包含的 Atlast 子包。各个子包将在以下段落中描述。作为 WORDSUSED 包的一部分提供的 WORDSUSEDWORDSUNUSED 原语,可让您确定 Atlast 程序中使用了哪些原语,从而确定执行该程序需要哪些包。

  • ARRAY
    提供任意数据类型的 n 维数组声明以及此类数组的运行时下标计算。
    原语:ARRAY

  • BREAK
    通过 atl_break() 函数启用异步中断处理。禁用此包可节省非常少量的内存,并将执行速度提高约 10%。
    原语:无。

  • COMPILERW
    启用用于定义新编译器词的原语。
    原语:[COMPILE]LITERALCOMPILE<MARK<RESOLVE>MARK>RESOLVE

  • CONIO
    启用显示交互式输出的原语。在不提供用户交互的应用程序中可以禁用这些原语。
    原语:.?CR.S.".(TYPEWORDS

  • DEFFIELDS
    启用用于操作词典项的低级原语。这些原语很少使用,除非有要用 Atlast 语言本身编写复杂语言扩展的雄心壮志。
    原语:FIND>NAME>LINKBODY>NAME>LINK>N>LINKL>NAMENAME>S!S>NAME!

  • DOUBLE
    启用双字操作。这些操作可用于任何栈数据,但在浮点代码中大量使用,因为浮点数占用 2 个栈项。
    原语:2DUP2DROP2SWAP2OVER2ROT2VARIABLE2CONSTANT2!2@

  • FILEIO
    启用类似 C 语言的文件操作原语。如果您的应用不需要访问文件,可以禁用此包。
    原语:FILEFOPENFCLOSEFDELETEFGETSFPUTSFREADFWRITEFGETCFPUTCFTELLFSEEKFLOAD
    此外,还定义了 FILE 变量 STDINSTDOUTSTDERR,自动绑定到同名的 Unix I/O 流。

  • MATH
    启用数学函数。只有在 REAL 也启用时才能启用 MATH
    原语:ACOSASINATANATAN2COSEXPLOGPOWSINSQRTTAN

  • MEMMESSAGE
    控制是否在发生运行时错误(如栈溢出和下溢、错误指针等)时打印消息。禁用这些消息不会节省时间或大量内存:它适用于只将错误状态返回给 atl_eval()atl_exec() 的调用者就足够了的深度嵌入式应用程序。
    原语:无。

  • PROLOGUE
    允许调整分配给栈、返回栈、堆及临时字符串缓冲区的内存大小。启用该包后,Atlast 程序文本可覆盖默认配置。若启用此包,解释器会在首行可执行 Atlast 代码前识别以下特殊格式的声明(称作序言):

    \ *area size
    

    为了允许处理序言,请勿显式调用 atl_init(),该函数会在序言解析后由atl_eval() 自动触发。序言中支持以下区域定义:

    区域 意义
    STACK 栈的尺寸(long 类型的栈项个数)
    RSTACK 返回栈的尺寸(返回栈指针项的个数)
    HEAP 堆的尺寸(long 类型的项目个数)
    TEMPSTRL 临时字符串缓冲区的长度(字符个数)
    TEMPSTRN 临时字符串缓冲区的数量
  • REAL
    启用浮点运算功能。建议同时启用 DOUBLE 包以增强功能。
    原语:(FLIT)F+F-F*F/FMINFMAXFNEGATEFABSF=F<>F>F<F>=F<=F.FLOATFIX

  • SHORTCUTA
    启用快捷整数算术运算。
    原语:1+2+1-2-2*2/

  • SHORTCUTC
    启用快捷整数比较运算。
    原语:0=0<>0<0>

  • STRING
    启用字符串操作功能。
    原语:(STRLIT)STRINGSTRCPYS!STRCATS+STRLENSTRCMPSTRCHARSUBSTRCOMPARESTRFORMSTRINTSTRREAL
    若同时启用 REAL 包,还可使用 FSTRFORM 原语。

  • SYSTEM
    支持向操作系统提交字符串命令以执行。仅当构建 Atlast 的 C 语言实现提供 system() 函数时可用。
    原语:SYSTEM

  • TRACE
    启用逐词追踪程序执行过程的功能。
    原语:TRACE

  • WALKBACK
    启用出错时打印回溯信息的功能。
    原语:WALKBACK

  • WORDSUSED
    启用程序用词分析功能,提供已使用/未使用词汇的统计。该功能可在 Atlast 应用开发阶段确定必需与非必需的功能包。
    原语:WORDSUSEDWORDSUNUSED

基准测试

为了大致了解 Atlast 在处理计算密集型任务时的性能表现,我将其与 C 语言和 AutoLISP 进行了两项基准测试,这两项测试均涉及平方根计算。

第一项基准测试 CSQRT 采用 AutoCAD 的 hmath.c 模块中使用的牛顿-拉夫逊迭代算法计算 2 的平方根,该算法也用于 AutoLISP 示例程序 sqr.lsp。这项测试代表了极端计算密集型代码——这实际上是对宏语言的误用,此类计算通常应转移到用 C 编写的原生函数中。不过了解最坏情况下的表现仍具参考价值。

第二项基准测试 SSQRTCSQRT 相同,区别在于调用的是系统数学库的 sqrt() 函数而非测试语言自实现的函数。由于三种语言都调用相同的底层系统函数,该测试展示了在比典型宏语言应用更受计算限制、但语言开销低于 100% 的环境中的相对性能。所有测试均在 Sun 3/260 工作站(运行 SunOS 4.0.3 系统)上完成,测试程序清单附于本文末尾。Atlast 的计时数据来自使用“-O4 -f68881”编译选项并禁用堆栈检查的版本;C 程序同样采用“-O4 -f68881”编译;AutoLISP 测试则在 Z.0.65 非生产(NONPRODUCTION)版本(使用“-O -f68881”构建)上运行。下表所有时间数据均已标准化,以 C 语言执行时间为基准值 1。

C 语言 Atlast AutoLISP
CSQRT 1.00 7.41 67.08
SSQRT 1.00 1.00 1.52

总结与结论

万物皆应可编程!我得出结论:以封闭方式编写几乎任何程序都是错误的,这会导致在其生命周期中耗费无数时间进行“增强”。后续的调整、“增加功能”和“修复”往往会使产品变得臃肿难懂,最终导致无法学习、无法维护乃至无法使用。

更好的做法是前期投入精力创建足够灵活的产品,让用户能根据即时需求自由调整。如果产品采用可移植的开放形式实现可编程性,用户扩展就能被交换、比较、由开发者审核,并最终融入产品主线。

拥有成千上万创造性用户以开发者未曾预料的方式拓展产品边界——实际上是在无偿为厂商工作,远比面对成千上万沮丧用户提交愿望清单,迫使厂商雇佣人力付费满足需求要好得多。开放架构与可编程性不仅惠及用户,不仅从技术和市场角度提升产品品质,更能为厂商带来直接经济优势——这种优势恰恰对应着封闭产品厂商的同等劣势。

反对可编程性的主要论点是开发开放产品需要额外投入。Atlast 提供了一种构建开放产品的方法,耗时与开发封闭产品相当甚至更少。正如理智的 C 程序员不会在标准库中存在完善的带缓冲的文件 I/O 包时自行编写,当现成的宏语言工具具备近乎原生 C 代码的速度(除非极端滥用)、全功能启用仅占 51K 空间、通过重编译单个文件即可移植到任何支持 C 的机器、并能 15 分钟内集成到基础应用中时,为何还要重复发明轮子?

我是在提议所有应用都变成 Forth 风格吗?当然不是——就像 PostScript 打印机输出不会呈现 PostScript 代码,80386 处理器上运行的应用也不会展现汇编语言。Atlast 是中间语言,仅由产品实现和扩展者接触。即便如此,通过合理定义词汇,Atlast 也能如同变色龙般呈现任何你想要的形态,甚至在解释器原始层面也是如此。

我屡次面临这样的设计困境:明知需要可编程性,却缺乏时间、内存或勇气直面问题正确解决。最终只能制造临时方案,长期背负技术债务。这就像老派程序员因缺乏动态内存分配器或链表包而陷入噩梦的高级版本。可编程性虽是计算的“魔法烟雾”,但我们不该被“机器中的幽灵”吓退,也不应吝啬赋予用户这种能力。

别把 Atlast 视为 Forth。甚至别把它当作语言。最佳理解方式是将 Atlast 看作提供可编程能力的库例程,就像其他库提供文件访问、窗口管理或图形功能那样。“预制可编程”的概念确实古怪——我花了两年才真正理解并驯服这个理念。仔细思考,动手实践,你或许能找到更好的应用构建方式。

开放更好。Atlast 让你用比编写封闭程序更短的时间构建开放程序。继承 Atlast 开放架构的程序,将在整个产品线及所有支持硬件平台间共享统一、简洁、高效的用户扩展机制。其潜在效益不可估量。

John Walker
加利福尼亚州缪尔海滩
1990 年 1 月 22 日 — 2 月 11 日
4072 行代码

初始版本:1990 年 2 月
AMIX 1.0 版:1992 年 9 月
网页版 1.0:1995 年 8 月
网页版 1.1:2002 年 7 月
网页版 1.2:2007 年 10 月

按字母排列的 Atlast 原语

略,参见原版

基准测试用程序

略,参见原版