Fun语言源码深度分析报告 - 元宝
← 目录
作者: 一了 <
[email protected]
> 日期: 2026-10-16 一句话总结: AI 做的 Fun 语言源码深度分析报告, 联名作者: 一了, 元宝。
# Fun语言源码深度分析报告 - 元宝 > 分析对象:`https://github.com/funlang/fun`,分支 `main`,版本 9.0 > 分析方式:拉取完整源码后逐层静态阅读(非 README 转述)。文中所有行号以仓库 `src/` 为基准。 > 分析日期:2026-10-16 --- ## 一、结论先行 Fun 不是玩具,但也**不是一门"通用替代型"脚本语言**。它的真实定位是: > 一个用 Pascal 写的、**极紧凑的树遍历解释器内核**(自写代码约 1.27 万行),外挂一条「从数据结构一路下沉到机器码」的能力阶梯,并把 59 个标准库模块用 Fun 自己重写了一遍。 三个最值得注意的判断: 1. **内核极小、但边界极宽**。真正手写的内核只有 ~12.6k 行 Pascal,却同时覆盖了词法/语法分析、对象系统、闭包、正则(PCRE)、FFI、运行时 C 编译、JIT、内联汇编。代价是每一块都做得"够用即止",而非工业级完备。 2. **所谓 JIT 不是"把 Fun 编译成机器码"**。JIT 只对**你写在脚本里的 C 片段或汇编片段**生效;Fun 代码本身永远是树遍历解释执行,且没有常量折叠、没有类型特化——仓库自带基准显示简单算术 78–143 ns/次,同一计算比内联汇编慢约 **613 倍**(详见 §10)。这是最容易误读的一点,README 的措辞容易让人以为有语言级 JIT。 3. **能力阶梯的代价是安全边界**。仓库自带 `SAFETY.zh.md`,把 `getapi`/`@toCallback`/TCC/汇编/任意内存读写标记为 NAT/MEM 类危险能力,并明确写着:拦截开关 `-safe` **尚未实现**。也就是说,当前版本跑 Fun 脚本 ≈ 跑任意本机代码,没有沙箱。 --- ## 二、仓库画像(实测数据) | 项 | 数值 | 说明 | |---|---|---| | 仓库大小 | ~2.4 MB(源码树,未含构建产物) | 第三方库刻意不入库 | | 主语言(内核) | Pascal(Delphi 2006/2009 + Free Pascal 双编译器) | `src/` | | 自有手写内核 | **12,653 行**(.pas/.inc,排除生成表与第三方) | core+parse+lib+regex+utils | | 语法生成表 | 5,945 行(`ytab/ltab/yact/yacc.inc`) | TP Yacc/Lex 预生成的 LALR 表,直接 include | | PCRE Pascal 移植 | 1,894 行 | `src/regex/pcre/` | | 第三方 KOL 等 | 92,457 行 | `src/3rd/`,仅 Windows GUI 路径使用 | | 标准库(.fun) | **12,164 行 / 86 个文件** | `fun/lib/`,纯 Fun 自写 | | 示例与测试 | `fun/demo`(46)、`fun/test`(58 脚本 + 91 份快照) | 快照对比式回归 | | Star / Fork / Issue | 19 / 1 / 3(2026-08-30 建库) | **开源仅一个多月,社区几乎为零** | | 提交 | 244 次,2 位贡献者 | 实质是单人作品开源 | 语言分布有个冷知识:GitHub 把它标成 "Standard ML",因为 `.fun` 后缀被 linguist 误判(SML 的关键字正是 `fun`)。 --- ## 三、执行流水线 ``` 源码文本 → lex(ltab.inc 生成的 DFA 表 + lex.inc) → yacc LALR 归约(ytab.inc/yact.inc),动作直接调用 CParser.BuildXXX → 直接构造 AST(CNode 树,无独立 IR、无字节码) → CRun.run(env) 树遍历解释执行 ``` 关键观察:**没有字节码、没有编译期优化、没有 AST→IR 这一步**。语法动作的语义动作(如 `builder.BuildIf`、`CreateExp`)边归约边建树;执行时 `CRuns.run` 顺序遍历子节点,`CExp.calc` 递归求值。这决定了性能上限,也是"小"的来源。 --- ## 四、内核设计解剖 ### 4.1 值模型:把 Pascal `Variant` 玩到极致 ```pascal // src/core/base.pas:28 CValue = System.Variant; PValue = ^CValue; CData = System.TVarData; ``` - 所有 Fun 值就是 Pascal `Variant`;代码大量直接操作 `TVarData`(`PData(val)^.VType / VInteger / VPointer`),绕过 RTL 做零拷贝读写。 - 自定义两个变体类型 `VarObject` / `VarObjNew`(`fun.pas:76-82`,FPC 下取 `$201/$202`、Delphi 下 `2010/2011`)承载对象指针;`VarObjNew` 表示"接管引用",`clear()` 时会 `_Release`。 - **整数默认是 int32**,即使在 64 位目标上:`fun.pas:34` 明写 `int = int32; // NOT int64 !!!`。指针宽度另用 `intptr/uintptr`(`FunPtrWide` 宏区分)。这带来一整类 64 位移植坑——代码里到处是补丁式注释(`base.pas:272-321` 的 `asPtr`/`rawPtr` 就专门处理"64 位下地址不能从 32 位字段读")。 - 浮点用 `Double`,时间用 `TDateTime`;`ToFunTime` 甚至在 FPC 分支里手写 ISO 时间解析,因为 FPC 的 `VarToDateTime` 受宿主 locale 影响(`base.pas:193-247`)。 ### 4.2 内存管理:原子引用计数,不是 tracing GC ```pascal // src/core/base.pas:413-421 function CBase._AddRef : fun.int; begin Result := lockInc(FRefCount); end; function CBase._Release: fun.int; begin Result := lockDec(FRefCount); end; ``` `CBase` 手写 `IInterface`,用**原子自增/自减**做引用计数;`add()/del()` 是全局入口。含义: - 无分代、无标记清除,**循环引用不会被回收**(闭包捕获 + 集合互相嵌套是很容易写出环的)。 - 官方宣传口径里的 "GC",在实现层面就是这套引用计数 + Variant 值语义。 - 源码里能看到明确的"泄漏补丁"痕迹,例如 `CId.enil`(`core.pas:256-264`)注释说明:原来每次求值 `CExp.new(nil)` 会泄漏一个节点,2 万次循环涨 1.8MB RSS,改成预分配单个 nil 单元。 ### 4.3 AST 节点即存储:变量是树上的槽 这是整个设计里最有意思的一点:`CExp`(表达式节点)**同时是 AST 节点和运行时值的容器**: ```pascal // src/core/core.pas:190-214 CExp = class(CNode) protected p_val: PValue; // 值指针 v_val: CValue; // 内联值 ... ``` 变量(`CVar`)、集合成员、对象属性都是 `CExp` 实例,赋值就是往节点里写 Variant。于是: - 不需要独立的 Environment/Activation Record 结构,**符号表就是 AST 的一部分**; - 递归调用靠 `CFunStack` / `CVarStack`(`base.pas:129-157`)把同名变量按调用深度压栈/出栈,而不是新建帧; - 副作用是**赋值天然是引用语义**:`new []` 出来的集合赋给另一个变量不会拷贝,`SAFETY.zh.md` 明确记了作者决定"不做 COW"。 ### 4.4 作用域与模块 - 符号查找 `CNode.UID`(`core.pas:493`)沿 parent 链上溯;`CRuns.ID` 用 `CHash`(**Trie 实现**,`base.pas:48-105`,`Max_Char=39`)做名字索引。 - 模块:`use 'file.fun'` 生成 `CModu`(`core.pas:142-154`),带 `runed` 标志保证**只加载运行一次**,`next` 串成链表做去重。 - `use ':file'` 是特殊语法——**在解析期把文件内容内联进脚本**(`lib-asm-pro.fun` 用它吃 1MB 的 `asm-list.fd`;后来改成 `.gz` + zlib 解压,注释里明确写了这条优化)。 ### 4.5 对象系统:class 就是"会造对象的函数",继承靠构造期拷贝 ```pascal // src/core/core.pas:786-810(简化) if isClass then begin o := newObj; // 建空对象 doCopy(o.items, exps); // 递归把父类 + 自己的成员拷进对象 evar.assign(o); // result = 对象 pushObj(o); ... doCall(env, exps); ... popObj; ``` - `class` 编译成 `isClass=true` 的 `CFun`;`upClass` 指向父类。 - 继承**不是原型链查找,而是构造时把父类成员全部复制进对象**(`CFun.doCopy` 递归 `upClass.doCopy`,`core.pas:847-854`)。 - 方法/属性/getter/setter 统一由 `.` 派发:`CId2.find` → `env.call(exp, id, exps, val, ct)`,`ct` 区分 `CDoMethod` / `CPropertySet`(`core.pas:1484-1490`, `host.pas:187`)。 - `this` / `base` / `@`(即 `result`):`CFun.parse` 里把 `evar` 同时挂到 `vars[_Result]` 和 `vars[_AT]`(`core.pas:882-900`),所以 `@` 和 `result` 是同一个变量槽。 ### 4.6 集合:一个类型同时当数组和字典 `CSet` = `CExps`(`CHist` = Trie 哈希 + 有序列表)双结构:既能 `s[0]`、`s['key']`,也能 `@each`、`@count`。内建 `@` 方法只有 7 个,全部注册在 `lib/libset.pas:451-457`: ``` @count / @length / @has / @each / @add / @clone / @toJson ``` 其它一切(map/filter/reduce/merge/diff)都在 Fun 层 `lib-set.fun`(170 行)里补。这是 Fun 的一贯手法:**内核只留最小原语,上层用 Fun 自己堆**。 ### 4.7 控制流与异常 `flow.pas` 只有 450 行,`CIf/CCase/CLoop/CForTo/CForIn/CNext/CExit/CTry/CRaise` 全部是 `CBloc` 子类,`run(env)` 里直接递归跑子树。`CNext/CExit` 复用 `CGoto`(带 `target` 语句块引用)实现带标签的跳出。 --- ## 五、解析器与语法 - 文法真身在 `src/parse/bnf/yacc.y`(TP Yacc 语法)+ `lex.l`(词法),产物 `ytab.inc/ltab.inc/yact.inc` 已预生成并入库——**改文法需要重跑 TP Yacc/Lex**,这是参与贡献的一道门槛。 - `src/parse/bnf/fun.ebnf` 是给人看的全量 EBNF(**GBK 编码**,直接打开会乱码)。 - 运算符优先级集中在 `yacc.y:57-70`:`opPipe` < `opLambda` < `or` < `and` < 比较 < `&`连接 < `+-` < 位 < `*/` < `^`。 - 三个有辨识度的语法设计: - `?` 是**打印运算符**(`?.` 带换行、`?,` 带空格、`?:` 带 Tab); - `x?` 是**可选成员**(找不到就给 nil 而不报错),实现在 `CId.find` 的 `opt` 分支(`core.pas:1405-1411`); - `.` 后跟符号串(`点符 ::= '.' [~!#%^&*-+=\|<>/?]+`)是**自定义运算符**,如 `$ .# 3`。 - **关键字别名层**:`ParseAlias`(`parse.pas:178`)在词法期把别名换成规范关键字,`-key:key_cn.ini` 就能用中文关键字(`函数=fun`、`如果=if`……)。语言本身是关键字中立的,别名包只是本地化层。注意 `fun/key_cn.ini` 同样是 GBK。 --- ## 六、能力阶梯:真正的技术含量所在 ### 6.1 FFI:两个完全不同的后端 签名串约定 `'iiii:i'`(参数类型字符 + `:` + 返回类型),`i`=int、`l/n`=64 位、`s/a/w`=字符串、`f/d`=浮点、`p`=指针、`c`=回调、`v`=void、`C`=cdecl 标记。 - **Windows 32 位(winapi.pas)**:手搓。倒序 `for` 遍历参数,按类型把值塞进 `ret`,然后 **内联汇编 `push ret`** 压栈,最后 `CWinAPI(hfun)()` 直接调用(`winapi.pas:167-260`)。回调则是 `VirtualAlloc` 出可执行内存,手写机器码 thunk:`B8 xx`(mov eax, obj)+ `E8 rel`(call)+ `C2 n`(ret n)(`winapi.pas:266-300`)。 - **Linux/macOS/win64(lffi.pas,基于 libffi)**:正规得多——`ffi_prep_cif` + `ffi_call`,回调用 `ffi_closure`。还内置了**运行时 ABI 探测**:libffi ≥3.4 的 `FFI_UNIX64=2`,老版本是 1,代码逐个试(`lffi.pas:100-118`);Darwin arm64 可变参数走 `ffi_prep_cif_var`;Windows 上还会给裸 API 名自动补 `A` 后缀。 两个后端的存在直接解释了 CHANGELOG 里大量"只在某个平台显形"的 bug。 ### 6.2 运行时 C 编译(lib-tcc.fun) 60 行 Fun 代码:`getapi` 出 `tcc_new/tcc_compile_string/tcc_relocate/tcc_get_symbol`,把字符串 C 源码编译、重定位、取符号、再包成可调用对象。注意它给源码前缀拼了 `void _start(){}` 并用 `-nostdlib -nostdinc -O2`,是一套很务实的"最小可用"配置。 ### 6.3 JIT:名不副实但很有用 ```fun // fun/lib/lib-jit.fun fun NewJit(code, args, names, name) if code =~ /^#!c\b/i then result = ccompile(code, flag: args, name: name); // TCC 路径 else result = Assembly(code: code, args: args, names: names).Load(); // 汇编路径 ``` **它只做"源码形态分发",不做 Fun→机器码编译。** Fun 代码本身仍由树遍历解释。真正的价值在于:C/asm 产物能通过 `@toCallback` 回调进 Fun 函数,实现"热点用 C 写、编排用 Fun 写"的同进程混编。 ### 6.4 内联汇编:自带一个 x86 汇编器 `lib-asm-pro.fun`(113 行)+ 数据表 `asm-list.fd`(1 MB,压缩后 `asm-list.fd.2.gz` 93 KB):把 `mov eax, <test>` 这类助记符 **查表翻译成机器码字节**,其中 `<name>` 占位符替换为回调地址。 工程细节相当扎实,明显是踩过坑的: - 64 位下 `@toCallback` 的 libffi closure 地址**高于 4GB**,表的 32 位 `mov r,%8` 装不下,于是运行时注入 `%16` 形式的 `mov r64, imm64`(`lib-asm-pro.fun:41-49`); - Linux/macOS 用 `mmap` 取可执行内存而非 `VirtualAlloc`;macOS arm64 必须 `MAP_JIT` + `pthread_jit_write_protect_np` 逐线程切换 W^X(`lib-asm.fun:19-47`); - aarch64 需要 `__clear_cache` 刷指令缓存,x86 不需要(`_asm_flush` 钩子); - `ArchOk()` 会在错误的 CPU 上拒绝执行,避免 SIGILL。 `lib-asm-a64.fun`(641 行)是 aarch64 后端。可以说,**"能在一门脚本里手写 x86 汇编并直接跑"是 Fun 最不可替代的能力**。 --- ## 七、FD 与自举:设计上最漂亮的一环 FD 是 Fun 自有的类 Markdown 数据格式(缩进 + 空格 + `-`,兼容 JSON 结构)。它的实现是**真的自举**: 1. `fun/lib/fd.bnf.fd` —— 用 FD 写的 FD 文法; 2. `lib-bnf.fd`(`bnf.fd`)—— 用 FD 写的"BNF 的 BNF",且自带 `@regex` 字段(BNF 编译出的正则); 3. `lib-bnf.fun`(114 行)—— 把 BNF 文法展开成带命名组的正则; 4. `lib-fd.fun`(113 行)—— 用正则匹配 + 缩进栈语义动作,200 行内完成 FD 解析。 ```fun // fun/lib/lib-fd.fun 核心:语义动作就是一个缩进栈 Key: (o,r) { ... while stak.peek().ind >= rt.ind do stak.pop(); end do; ... } ``` 同时内核里另有一份 Pascal 手写 FD/JSON 解析(`lib/libfd.inc` 328 行)供 `getJson(fd:true)` 高性能路径使用,标准库里还有 zlib/SSE 压缩分支。**同一份格式,三套实现(Pascal 内核、Fun 自举、压缩变体)**,这是它"数据驱动 + 自描述"哲学的兑现。 顺带一提,Fun 的官方文档体系也是这个思路:demo 文件既是文档、又是测试、又是教程。 --- ## 八、标准库与跨平台约定 59 个模块、12k 行纯 Fun 代码,覆盖 JSON/YAML/XML/Base64/MD5/CStruct、集合与算法、字符串/正则/Unicode、文件/OS/进程/时间、网络(winsock/ajax/jsonrpc)、数据库(ORM 系列、ADO)、UI(甚至有一个用 Fun 写的记事本 `notepad--.fun`)、异步。 平台后端约定清晰:**Windows 用 `lib-x.fun`,Unix 用 `lib-x-unx.fun`(旧的 `-lnx` 变薄封装),共享逻辑放 `lib-x-base.fun`**。例如 `lib-asm` 在 Linux 走 libc `mmap`、macOS 走 `libSystem`、Windows 走 `VirtualAlloc`,全部在 Fun 层用 `getapi` 切换——没有一行 C 胶水。 --- ## 九、工程实践:意外地认真 - **构建矩阵**:9 个目标(Win32/Win64/WinCE/Linux i386/x86_64/aarch64/macOS x86_64/aarch64),脚本集中在 `src/prj/fun/`,Windows 侧路径由 `setenv.bat` 统一。macOS 与 Linux-arm64 走 Zig 0.13 提供 clang/lld,无需目标 SDK。 - **第三方依赖刻意不入库**(PCRE 8、TCC 0.9.27、libffi),缺失时**降级并打印提示而非中断构建**——这个决策在 CI 上被反复验证。 - **CI 五平台全绿**(Linux/macOS/Windows × x64 + Linux/macOS arm64),快照回归:58 个测试脚本对比 `expected/` 下 91 份快照,平台差异用 `regression-skip-<platform>.txt` 声明,`--update` 写平台专属快照。 - **`.gitattributes` 精确到每个扩展名的 eol**,并把测试快照标为 `verbatim` 逐字节比对(CHANGELOG 里记过一次"文本模式复制把 GBK 字节重编码导致永远对不上"的坑)。 - 注释密度:内核约 8–18%,`lffi.pas` 达 17.5%,且大量注释在解释"为什么"(ABI 版本差异、locale、堆归属),不是复述代码。 --- ## 十、性能:仓库里其实有基准数字 更正一处:仓库并非"没有任何基准数字"。`fun/demos/benchmarks/` 下有两个文件把**实跑输出直接贴在末尾的 `#* ... #` 注释块**里,是可用的量化参考。 计时口径已核实:`lib-time.fun` 的 `tick(-2)` 走 `QueryPerformanceCounter`(Linux 走 `clock_gettime`,`libase.pas:613-620`),`get()` 换算结果为**毫秒**。测试统一跑 **10⁶ 次循环**,所以「ms 数 ≈ 每次迭代的 ns 数」,读起来很直观。 ### 10.1 逐操作开销(`test-speed.fun`,1e6 次迭代) | 操作 | 耗时(ms) | ≈ ns/次 | |---|---|---| | 空循环体(`for i=1 to n`,**1e8 次**) | 642 | **6.4** ⚠️ | | `var a=1` | 47 | 47 | | `1+2`(常量) | 78 | 78 | | `1.1+2.1`(常量浮点) | 94 | 94 | | `i1+i2`(变量) | 141 | 141 | | `d1/d2` | 156 | 156 | | `"1"&"2"`(连接) | 267 | 267 | | `"1"+"2"` | 329 | 329 | | `f()` 空匿名函数 | 94 | 94 | | `f2(1,2)` 两参调用 | 157 | 157 | | `1>2` 比较 | 172 | 172 | | `n.f()` 对象方法 | 298 | 298 | | `n .+ 2` 自定义运算符 | 375 | 375 | ### 10.2 四个可以从数据里读出的结论 1. **整型与浮点几乎同价**(78 vs 94 ns)。差值只有 ~16 ns,仅够覆盖 Variant 类型提升——**没有任何按类型分派的 specialization**,所有算术都走同一条 Variant 通路。这是"值模型就是 Variant"的直接代价。 2. **完全没有常量折叠**。`1+2`(78) 与 `i1+i2`(141) 相差 63 ns,而这 63 ns 纯粹是两次变量读取(**≈31 ns/次**:Trie 哈希查找 + Variant 拷贝 + 引用计数)。语法树里写的就是运行时做的,`1+2` 每轮都会真的算一遍。 3. **空循环 6.4 ns:数字本身很漂亮,但不能当基线——它更重要的身份是对照组**。 `flow.pas:283-305` 的 `CForTo.run` 是**特化快路径**:上界 `emax` 只求值一次并存成裸 `fun.int`,计数器用 `inc(vi, si)` 自增,每轮只往预构造好的 `TVarData` 里写整数。拆开看一轮的全部工作只有 7 件:`vv.VInteger := vi` → `evar.assign()`(就是 `p_val^ := val`,一次 16 字节 Variant 拷贝,**没有引用计数**,因为没走 `clear()`)→ `all.run(env)`(`CRuns.run` 对空块是 `for i := 0 to -1`,零迭代)→ `isExit` 读 → `env.trace()`(仅 `lastNode := n` 一次指针写)→ `inc` → while 条件。 6.4 ns ≈ 32 cycles(@5GHz),摊到 7 个操作约 **4.5 cycles/操作**——热路径上**没有原子操作、没有 Variant 类型分派、没有深层虚调用**。对非 JIT 的树遍历解释器,这是相当干净的数字,FPC 生成的代码质量也确实是够的。 但真正有价值的是拿它做对照:**同样是"往一个变量里写个整数",特化路径 6.4 ns,通用路径(`var a=1`)47 ns —— 差 7.3 倍。** 这 7.3 倍就是纯粹的"树遍历 + 通用分派税":`CRuns.run` → `CRun.run` 虚调用 → `CExp.calc` → `CExp.assign` 这条链,每多一层就多一份成本。所以 6.4 ns 不能外推到一般语句,真正的语句基线要按 **47 ns** 算;而 6.4 ns 的意义在于它给出了**这条链的理论下限**。(另一处嫌疑已排除:循环自身的 `evar` 与函数体内的 `var` 都会生成 `CVarRE`,其 `assign` 先调 `stack.Push()`;但 `CFunStack.PushVar`(`base.pas:847-854`)带 `avar.level < level-start` 守卫——循环里不再发生函数调用、`level` 恒定,每个变量最多压 `level-start` 次就永久短路,摊到 1e6 次循环可忽略——故这 40 ns 确实来自分派,不是变量模型。) 4. **调用开销比算术贵,对象派发再翻一倍**。函数调用于 94–157 ns,对象方法 298 ns(≈2 倍),自定义运算符 375 ns(≈2.5 倍)——与 §4.5 里"方法/getter/setter 统一走 `.` 派发 + 构造期拷贝成员"的实现完全对得上。 ### 10.2b 一个分层成本模型(把上面的数字串起来) 把基线逐层剥离,可以得到相当自洽的成本分解: | 层 | 推导 | 成本 | |---|---|---| | 循环骨架 + 整数变量写(**特化**) | `test-speed` 空循环 | **6.4 ns** | | 通用语句基线 | `var a=1` | **47 ns**(增量 +40.6) | | 一次 Variant 二元运算 | `1+2`(78) − `var a=1`(47) | **31 ns** | | 一次变量读取 | (`i1+i2`141 − `1+2`78) ÷ 2 | **31.5 ns** | **变量读取 ≈ Variant 加法 ≈ 31 ns。** 这个相等未必是巧合,但**别把它读成"瓶颈只有一个"**——上表其实分出了两个性质完全不同的成本。 > **精度提示**:`test-speed` 一共 18 个不同取值。直接排序作差,最小非零差是 **1 ns**(78/79、125/126、140/141、156/157、266/267 这五对就是 ±1 抖动);把这五对抖动合并后,相邻档位间距落在 **14–16 ns**,均值约 **15.6 ns**。也就是说这份数据的**有效分辨率约 15 ns**,上面 31 ns 的减法只有约 2 个量子、40.6 ns 约 2.6 个——**量级可信,精确拆分不可信**。 ### 10.2c 地板与天花板:两层成本,性质完全不同 | | **分派税(天花板)** | **Variant 税(地板)** | |---|---|---| | 在哪儿付 | **每条语句**付一次 | **每个值操作**付一次 | | 实测 | 6.4 → 47 ns,差 **40 ns** | 一次加法 **31 ns** | | 随什么增长 | 代码行数 / 语句数 | 计算量 | | 能优化掉吗 | **能**——纯工程问题 | **当前架构里不能**——除非加特化层或改值模型 | | 证据 | `CForTo` 特化后就是 6.4 ns | 原生整数加法本应 <1 ns,这里 31 ns | 关键的区分在于**有没有下限**: - **Variant 是地板(对这套解释器架构而言)。** 一次整数加法在硬件上是亚纳秒级的,这里要 31 ns(约两个数量级)。这 31 ns 不来自分派,来自"值就是 Variant"这件事本身——16 字节容器 + 运行时类型判定 + RTL 辅助调用。**没有特化的解释器**(包括朴素字节码 VM)削不掉它;能削它的是内联缓存 / 类型特化——CPython 3.11+ 就是在保留统一对象模型和引用计数的前提下,靠特化把 int 加法压到几纳秒——或干脆改值模型(NaN-boxing / tagged union)。注意特化**不必改语言的值模型**,但在这套"AST 即存储、CExp 直接持有 Variant"的内核上,那基本等于重写执行器。 - **分派是天花板。** 40 ns 的每语句开销是可以靠工程手段削的——`CForTo` 就是活证据:同样"往变量里写个整数",特化后 6.4 ns,通用路径 47 ns,**7.3 倍纯属遍历与虚分派**。 这正好解释了 Fun 的那个设计判断:**为什么它选择"往 C/汇编下沉",而不是"优化解释器"**。计算密集型代码撞的是地板,工程优化救不了;而 `CForTo` 那 6.4 ns 恰好证明——**只要绕开 Variant,Pascal 写的骨架本身并不慢**。 于是"Fun 慢不慢"这个问题要看你在哪一侧:**做数据胶水(JSON/FD 变换、编排、调原生库)时你是天花板主导,每语句 47 ns ≈ 2100 万语句/秒,够用;做计算时你撞地板,每操作 31 ns,在这套没有特化的解释器里靠调参逃不掉(要逃得加特化层,那是重写执行器)——这时候成本最低的动作是换一层(C / 汇编),也就是能力阶梯。** 语言把自己定位成"从 JSON 到机器码的胶水层",而不是"跑得快的脚本",与这套成本结构是自洽的。 ### 10.3 能力阶梯的实际收益(`test-lib-asm-fun-9-2.fun`) 这个文件把同一件事(求 1..n 的和)用两种写法各跑一遍,且输出的是**全精度浮点数**(143.192199853098 / 23.3613999065171 ms),比 `test-speed.fun` 那些整数值可信得多: | 写法 | 规模 | 实测 | 折算每次 | |---|---|---|---| | **Fun 原生** `for i=1 to 1e6: s += i` | 1e6 | **143.19 ms** | 143 ns | | **内联汇编** `@sum1ton` | 1e8 | **23.36 ms** | 0.234 ns | 两处交叉验证:`test-speed` 的 `i1+i2` = 141 ns 与这里的 143.19 ns 吻合在 1.5% 内,说明两份数据大概率来自同一台机器,可互相印证。 **把规模拉平:Fun 原生跑 1e8 次约需 14.3 秒,汇编 23.4 毫秒 —— 差距约 613 倍。**(两个数字来自同机同日输出,613 是实测端到端比值而非外推;下面已核对 23.4 ms 对应 ~4.3×10⁹ 次/秒,在 5 GHz 级核心上属正常范围,不构成"样本可疑"的理由。) (顺带一提,`@sum1ton` 的定义就在 `fun/lib/asm-list.fd` 第 3 行:`33C033D203C183D2004975f8`,即 `xor eax,eax / xor edx,edx / add eax,ecx / adc edx,0 / dec ecx / jnz -8` —— 6 条指令的紧凑循环。23.36 ms 跑完 1e8 次意味着 4.3×10⁹ 次/秒,只有在 5GHz 级 CPU 上才做得到,说明这台机器相当快;**跨机器比较时,"613 倍"这个比值比任何绝对数字都更稳**。) ### 10.4 怎么定位这个性能 - 简单算术吞吐 ≈ **7–13 M ops/s**(视是否走变量),字符串连接掉到 ~3 M ops/s,方法调用 ~3.4 M ops/s。 - 量级上属于**典型树遍历解释器**:比 CPython 3.x 同题慢 2–4 倍(后者有专门的二元运算特化与栈式字节码),与早期 Tcl/Ruby 1.8 大致同一档。此项为跨语言估算,非实测。 - 因此 README 里那句"从 JSON 到机器码"应该这么理解:**Fun 自己的代码始终在慢的那一侧,能力阶梯的意义是让你把热点搬到快的那一侧**——代价是这段热点得用 C 或汇编手写,且只在 x86(Windows 32 位 ABI)上有现成表。 > 数据可信度提示:均为作者机器上的单次结果,未标注 CPU/系统,无方差;`test-speed` 的整数值呈现 ~15.6 ns 的量化台阶(相邻档位间距,可能是取整或计时分辨率),故 **30 ns 以内的差异应视为噪声**,只读趋势不读绝对位。 --- ## 十一、客观评价 ### 亮点 1. **内核与体积的比值惊人**:1.27 万行换来了 JSON/FD + 对象 + 闭包 + 正则 + FFI + C 编译 + 汇编的完整栈。 2. **能力阶梯是真的打通的**:同一脚本内高层数据处理与裸机器码混写,且能双向回调,市面上极少有第二家。 3. **标准库全用自己写**:既是能力自证,也让"改语言"和"改库"用同一套心智。 4. **跨平台降级设计**:可选能力缺失不阻断构建,CI 探针报告实际能力。 5. **自举与自描述**(FD/BNF)在工程上真的落地了,不是 PPT。 ### 风险与短板 1. **无安全边界**:`getapi`/TCC/汇编/任意地址读写全部开放,`-safe` 拦截**尚未实现**(`SAFETY.zh.md` 自述)。运行第三方 Fun 脚本 = 授予本机代码执行权。 2. **性能上限来自值模型 +"无特化"执行器的组合,而非解释器骨架本身**:无字节码、无语言级 JIT、无常量折叠与类型特化。仓库自带基准(§10)显示两条独立的成本线——每语句分派税 ~40 ns(可优化)与每值操作 Variant 税 ~31 ns(在这套"AST 即存储 + Variant"的执行器里是结构性开销;要动它得加内联缓存/类型特化,或换值模型);同一计算比内联汇编慢约 613 倍。能力阶梯的收益是真的,但它不改变 Fun 自身代码的性能档位。 3. **引用计数不回收环**,长生命周期的互引用结构会泄漏;作者已明确不做 COW,别名语义容易踩坑。 4. **整数 32 位 + 双编译器(Delphi/FPC)**带来持续的移植成本:CHANGELOG 里满是"只在 Windows/FPC 显形"的差异修复(locale 时间、代码页输出、PCRE 堆归属、libffi ABI 版本……)。 5. **中文资产是 GBK 编码**(`fun.ebnf`、`fd.fd`、`key_cn.ini`、部分快照),在现代工具链里是持续的摩擦源。 6. **文法改动门槛高**:需要重跑 TP Yacc/Lex 生成 `.inc`,新人贡献链路不顺。 7. **社区近乎为零**:19 star、1 fork、2 贡献者,开源一个多月。生态、issue 响应、第三方模块都谈不上有保障。 8. **许可证是双许可**:MIT + 商业条款(改内核闭源分发 / 嵌入 `fun.dll` 到闭源商业产品需商业授权)。做产品前必须过一遍法务。 --- ## 十二、适用 / 不适用 **适合** - 需要嵌入式脚本、且脚本要能摸到本机 API / 驱动 / 芯片的 Windows 桌面或工业软件; - 想把"数据编排"和"热点用 C/汇编"塞进同一个文件、同一进程的工具作者; - 研究"小内核 + 大能力"语言实现、Pascal 现代实践、FFI/JIT 工程细节的学习者; - 已有 Delphi/Pascal 资产,想找一门能复用 Pascal 模块的动态语言。 **不适合** - 需要性能可预期的高吞吐服务(数值/Web 后端); - 需要跑不可信第三方脚本的场景(无沙箱); - 需要成熟生态、包管理、活跃社区的生产项目; - 对闭源商业集成敏感、又不愿谈商业授权的团队。 **上手路径(推荐顺序)** 1. `src/parse/bnf/yacc.y` —— 一页看懂全部语法; 2. `src/core/base.pas` → `core.pas` —— 值模型与节点即存储; 3. `src/lib/host.pas`(`CLib` 责任链,187 行)→ `libase.pas` 尾部(内置函数注册)→ `lffi.pas` —— 看懂"原生能力是怎么接进来的"; 4. `fun/lib/lib-bnf.fun` + `lib-fd.fun` —— 自举的精华; 5. `fun/demos/benchmarks/` —— 先看 `test-speed.fun` 和 `test-lib-asm-fun-9-2.fun` 末尾的实跑输出(本报告 §10),再从 `test-jit-cb.fun` 看回调贯通、`fractal-benchmark-{,asm,c}.fun` 看三档性能阶梯。 --- *报告基于 `main` 分支源码逐文件阅读;未能在本环境中编译运行(缺少 FPC 工具链),故实现性结论来自代码与文档推断。§10 的性能数字取自仓库中随源码提交的实跑输出(`test-speed.fun`、`test-lib-asm-fun-9-2.fun` 末尾注释块),非本次实测。*