PostgreSQL 的 Commodore 64 BASIC
摘要
PL/CBMBASIC 是一个 PostgreSQL 过程语言扩展,集成了原始的 Commodore 64 BASIC V2 解释器,允许用户编写带有行号的 BASIC 数据库函数。它用 C 语言模拟 6502 解释器,提供对 BASIC 命名约束的验证,并通过读取模拟变量表来支持 OUT 参数。
暂无内容
查看缓存全文
缓存时间: 2026/07/03 11:13
# LOAD "PL/CBMBASIC",8,1:用于 PostgreSQL 的 Commodore 64 BASIC
来源:https://thombrown.blogspot.com/2026/07/load-plcbmbasic81-commodore-64-basic.html
如果你到了某个年纪,`38911 BASIC BYTES FREE` 这几个字会对你造成任何心理治疗都无法抹去的印记。你会记得那片蓝屏。你会记得从杂志上抄了三页代码列表,然后遇到 `?SYNTAX ERROR IN 2340`,却不知道三页里哪一页有拼写错误。你会记得磁盘驱动器是设备 8,而且它比大陆漂移还慢。我有一些消息要告诉你:现在这一切都能在 PostgreSQL 内部运行了。
PL/CBMBASIC 是一种过程语言扩展,它在 Commodore 64 BASIC V2 上执行函数体。不是仿制品,不是致敬表演——而是真正的 Microsoft/Commodore 1982 年解释器,通过 Michael Steil 的 `cbmbasic`(https://github.com/mist64/cbmbasic)项目实现,该项目将 6502 ROM 静态重编译为 C 语言。这个 C 代码被直接编译进扩展的共享库中,因此解释器就活在你的后端进程里。每次函数调用都是一次内存中的电源循环:清零 64KB RAM 数组,重置 CPU 寄存器,然后在 $E394 处重新进入 ROM。整个过程大约花费 15 到 20 微秒,这比硬件当年快大约一千倍,而且快到可以在大表上逐行调用而不必感到愧疚。
```sql
CREATE EXTENSION plcbmbasic;
CREATE FUNCTION hello(who text) RETURNS text AS $$
10 PRINT "HELLO, ";WHO$;"!"
$$ LANGUAGE plcbmbasic;
SELECT hello('WORLD');
-- HELLO, WORLD!
```
是的,那些是行号。是的,它们是强制的。用户代码从第 10 行开始,就像大自然规定的那样,因为 0 到 9 行被保留:扩展会在你的代码运行前,将函数参数以普通 BASIC 赋值语句的形式注入进去。一个名为 `who` 的 `text` 参数会变成 `WHO$`,一个名为 `lives` 的 `smallint` 会变成真正的 16 位 `LIVES%`,而其他所有数值类型都会落入一个 40 位的 CBM 浮点数中,包括它那九个完整的有意义数字。
## 验证器有自己的意见,因为 BASIC V2 就有意见
任何一个在 C64 上编程超过一小时的人都会发现,你不能使用名为 `TOTAL` 的变量。词法分析器会在任何位置压缩关键字,包括在标识符内部,所以 `TOTAL` 包含了 `TO` 而变成了垃圾。`SCORE` 包含了 `OR`。`BUDGET` 包含了 `GET`。名字的前两个字符才有效,所以 `USERNAME` 和 `USERID`……实际上它们没问题,`US$` 和 `US` 是不同的变量,但 `ALPHA` 和 `ALPS` 会静默地变成同一个字符串。而且 `TI` 和 `ST` 被系统占用了。
该扩展附带了一个验证器,因此 PostgreSQL 现在会在 `CREATE FUNCTION` 时给出这些意见,而不是让你在运行时重新发现它们:
```
ERROR: parameter name "total" contains the BASIC keyword TO
HINT: This is why nobody could ever have a variable called TOTAL on the Commodore 64.
```
有些创伤值得比我们第一次遇到时更好的错误信息。
## OUT 参数,通过遍历变量表
当 BASIC 程序结束时,它的变量仍然停留在模拟的 64KB RAM 中。因此对于 OUT 和 INOUT 参数,处理程序做了唯一合理的事情:遍历 BASIC 自己的简单变量表——位于 $2D/$2E 的 VARTAB 和 $2F/$30 的 ARYTAB 之间的 7 字节条目,解码类型编码的名称字节,然后将 5 字节浮点数、16 位整数和字符串描述符转换回 SQL 值。
```sql
CREATE FUNCTION divmod(num int, den int, OUT quot int, OUT rmd int) AS $$
10 QUOT=INT(NUM/DEN)
20 RMD=NUM-QUOT*DEN
$$ LANGUAGE plcbmbasic;
SELECT * FROM divmod(47, 5);
-- quot | rmd
-- ------+-----
-- 9 | 2
```
PEEK 另一个进程的内存来获取结果并不是我预期很快能在 PostgreSQL 文档中看到的模式,但在这里感觉非常合适。
## 数据库就是设备 8
这是我最满意的部分。在 Commodore 64 上,你的数据存放在磁盘驱动器(设备 8)上,你用 `OPEN`、`INPUT#`、`GET#`、`PRINT#`、`CLOSE` 和 `ST` 状态变量与它通信。所以,在 PL/CBM-BASIC 中,设备 8 就是数据库。你 OPEN 的“文件名”是一个 SQL 语句,通过你的事务内部的 SPI 执行:
```sql
CREATE FUNCTION top_scores() RETURNS text AS $$
10 OPEN 1,8,0,"SELECT NAME, SCORE FROM HISCORES ORDER BY SCORE DESC"
20 INPUT#1,N$,S
30 IF ST<>0 AND N$="" THEN 60
40 PRINT N$;" ";S
50 IF ST=0 THEN 20
60 CLOSE 1
$$ LANGUAGE plcbmbasic;
```
列值以每行 CR 结尾的形式流回,而 `ST` 会在最后一个字节时捕获 EOF 位(64),就像它从 1541 驱动器读取时一样。你在 1985 年编写的读-直到-完成的循环可以直接运行,包括需要小心空文件的那一部分——这就是第 30 行在做的事。
更棒的是,辅助地址 15 是驱动器的命令通道——你之前会用 `PRINT#` 向它发送 DOS 命令,并从它那里读回 `00, OK,00,00`。现在这个也管用:
```basic
10 OPEN 15,8,15
20 PRINT#15,"DELETE FROM HISCORES WHERE SCORE < 1000"
30 INPUT#15,EN,EM$,RC,ES
```
状态记录是 `0,OK,,0`,以向原始版本致敬。每个 `PRINT#` 会追加内容,而终止的 CR 会执行,因此你可以分段发送超过 BASIC 255 字符字符串限制的语句。INSERT、UPDATE、DDL,随你便。这是一个仅限超级用户使用的不安全语言,所以你本来就能做可怕的事情;现在你可以用 `FOR` 循环和没有小写字母的方式来实现它们。
运行时错误会通过解释器自己的错误向量($0300)捕获——与 Simons' BASIC 使用的相同插件机制——因此错误会以正确的 PostgreSQL 错误形式呈现,并携带真正的 ROM 错误代码:
```
ERROR: BASIC error: ?DIVISION BY ZERO ERROR IN 20
```
而且因为 KERNAL 的 STOP 例程(RUN/STOP 键扫描,在每条语句前轮询)被修补为调用 `CHECK_FOR_INTERRUPTS()`,所以那个不朽的 `10 GOTO 10` 会在 `statement_timeout` 时干净地终止。RUN/STOP 键终于可靠地工作了。只花了 44 年。
## 它快吗?定义“快”
我针对相同的函数对 PL/Python 进行了基准测试。PL/Python 胜出,正如你对一个保持温暖解释器而不是每次函数调用都重启 Commodore 64 的语言所期望的那样:在简单调用上快大约 14 到 19 倍,对于从函数内部查询 100 行的负载,差距缩小到大约 6 倍,因为双方都需要支付相同的 SPI 成本。但是 C64 的底线是那 15 微秒的电源循环,这与非内联 SQL 函数的成本相当,而解释器每秒处理约一百万条 BASIC 语句。这个 ROM 最初所在的机器每秒只能处理大约一千条。一个 1982 年的 8 位解释器,在每次调用都执行完全上电复位的情况下,能接近 CPython 的 1.5 个数量级之内,这已经比任何人有权期望的要好得多了。
## 细节说明
里面是 1982 年的世界,扩展也没有假装不是。所有内容都是大写的,包括你的字符串字面量,因为 C64 的默认字符集几乎不包含小写字母。字符串最多 255 个字符。浮点数有九位有效数字。没有 NULL,因为 NULL 那时还没被发明出来;它表现为空字符串,如果你在意的话可以在查询中使用 COALESCE。`INPUT` 会引发错误,因为你的数据库没有连接键盘。`POKE` 和 `SYS` 可以针对模拟的 64KB 生效(如果你喜欢冒险的话),而野生 POKE 能做的最坏情况就是让机器混乱直到下一次调用的电源循环。
代码位于 `github.com/darkixion/pl-cbmbasic`(https://github.com/darkixion/pl-cbmbasic),附带回归测试套件和包含其余细节说明的 README。需要超级用户权限,生产部署……会是个选择,怀旧是必须的。
```
READY.
█
```
相似文章
Boriel BASIC
Boriel BASIC 是一款现代开源的 BASIC 编译器 SDK,主要为 ZX Spectrum 设计,提供增强功能、整数类型以及内联汇编支持,适用于复古游戏开发。
Postgres by Example
一份使用带注释的SQL示例的PostgreSQL实践入门,涵盖从基础到高级主题。
GPT2-BASIC: 基于BASIC的便携式机器智能
GPT2-BASIC 在 FreeBASIC 中为 DOS 系统实现了 GPT 风格的 Transformer 推理引擎,使用定点算术和本地模型工件,在老旧硬件上运行 AI 辅助任务。
cl-bbs: 用Common Lisp重写的类schemeBBS文本公告板
cl-bbs 是一个用 Common Lisp 编写的高性能匿名文本公告板引擎,忠实复刻了原始 SchemeBBS 的风格。它提供格式化支持、图片预览以及零 JavaScript 渲染等功能。
微软的6502 BASIC现已成为开源软件(2025)
微软正式开源了1976年的原始6502 BASIC源代码,该代码曾驱动早期的Commodore和Apple II计算机,这标志着历史保存的一个里程碑。