| |
七行,画出一朵三瓣玫瑰。
这是一门语言的完整程序。语料只有 17 个词,但它图灵完备——jz 加回跳足以表达任意控制流。
打开一个 html 文件就能用:左边写程序,按 Ctrl+Enter 重跑,画布上出现图案。单文件、离线、零依赖。
17 个词
| 词 | 作用 |
|---|---|
数字 / p0…p5 / i | 压入常量、参数滑杆、当前轮数 |
+ - / mod neg | 二元算术;neg 取负 |
> < sign | 比较(返回 0/1);sign 取符号 |
sin cos | 输入是圈不是弧度:0.25 → 90° |
rand | 伪随机 0…1,种子固定 ⇒ 可复现 |
fwd back | 沿朝向前进/后退(画线) |
turn right | 转向:turn 0.25 = 左转 90° |
jump | 画笔瞬移到 (x, y),不画线 |
N st / N ld | 存入 / 取出 N 号寄存器(N = 0…7) |
pen color width | 抬落笔、色相 0…1、笔宽 |
jmp N / jz N | 跳到第 N 行;jz 在栈顶为 0 时跳 |
pause / halt | 交还控制权(动画靠它)/ 停机 |
栈是唯一的数据通道,所以约定是后缀式:写 0.25 turn,不写 turn 0.25。
跳转用代码行号。跳到注释行或空行时会自动顺延到后面第一条真指令。
程序会被反复执行——这就是动画的来源,i 是已经跑过的轮数。
设计上真正需要决定的三件事
写这门语言的过程中,有三处不做决定就写不下去。
1. 循环的进度谁提供?
不是 while,而是「程序反复执行 + i 计数」。
好处是写参数方程时不用自己维护计数器。代价很实在:每轮的步长必须一样。用 i 递增角度画不出闭合曲线——每轮走的角不同,永远回不到起点。
这个坑是我自己写示例时踩到的,注释留在了「三瓣」示例里。
2. 循环里的状态放哪?
最初只有栈。而栈每轮必须清空,否则运行时会一圈圈涨。
结果:累加器根本写不出来。
所以加了寄存器:5 0 st 存、0 ld 取。
栈保持"每轮归零"的干净语义,跨轮状态一律走寄存器。分析器因此能给出很强的保证:程序停止时栈必须是空的。
3. 怎么保证这些约定不被写坏?
靠 analyze():对编译出的指令做前向数据流分析。
一条指令被不同路径以不同栈深到达,就重新入队(上限 6 个栈深,再多就是无限增长)。
判据是程序停止时栈深为 0。这一条覆盖三种坑:栈被弹穿、分支不均衡、循环里偷偷堆值。
被拒绝时给的是可读的诊断:
| |
编译期还有一道 peephole:N fwd 折成一条带参指令(fwd#N),既让指令数少一半,也消掉了"两个字面量在分支里叠在栈上"的歧义。
验证
| |
- 确定性:同一种子逐位一致,不同种子不同图 ⇒ 任何观察都可复现
- 敌意输入:空程序、只有注释、除零、取模零、寄存器槽越界、
1e9大数 —— 都不崩 - 性能:约 2400 万条指令/秒(单线程);界面默认每秒执行 600 条
- 示例是机械生成的:生成器先编译、做栈分析、真跑 8000 步,全部通过才写回
loom.html
最后这条是被逼出来的做法——在此之前我手工改示例错了五次。
验证抓到的 8 个真实缺陷
| # | 缺陷 | 症状 |
|---|---|---|
| 1 | 跳转目标被折成 ["lit", idx] | jz 拿到 0 而不是目标行,条件跳转全部失效 |
| 2 | 跳转占位指令多占一个槽位 | 目标整体错位,i 永不递增 |
| 3 | 跳转参数是字符串 "0" | Number.isFinite("0") 为 false ⇒ 回边从未被分析 |
| 4 | 末端判定读的是执行前的栈深 | 1 fwd 这类正常程序被误判不平衡 |
| 5 | 分析器只记"首达"栈深 | 每轮偷偷堆值的程序被放行 |
| 6 | 缺"每轮入口栈深一致"不变量 | 同上——运行 1500 步后栈涨到 1500 |
| 7 | 示例里 i 0.1 color 的栈残留 | 每轮留一个数,栈无限增长 |
| 8 | 自动取景用"全部历史外接框" | 随机游走把视野撑到无穷,画面全黑 |
第 8 条不是语言问题,是可视化策略问题。用历史包络去取景,对无界图形必然是错的;改成跟随画笔当前位置之后才正常。
第 3 条尤其典型:"0" 和 0 在 JavaScript 里看起来都像 0,但 Number.isFinite("0") 是 false。于是回边从来没被分析过,静态检查等于不存在——而界面一切正常。
它现在能画什么
它只画线。但 17 个词加上反复执行,能出来的东西比想象的多:蕨类、格栅、漂移的涡、以及那朵玫瑰。
参数滑杆 p0…p5 让你不改代码就能调形状——这也是它作为一个"玩的东西"成立的原因。
还能往哪走
- 图形输出:现在只画线。加
fill/stroke路径模式就能做面 - 多画笔:
pen已经是状态机,加若干"画笔槽"就能一次画多层 - 编辑器:行号已经在了,差行内报错高亮;
analyze()已经能给出准确行号,接口是现成的
17 个词听起来很少。
但语言的表达能力从来不是词表决定的,是组合规则决定的。
