行业资讯

加入亿拓客·流量大师 撬动财富之门!!!

Fun语言源码深度分析报告 - 元宝

wang 2026-10-17 行业资讯
Fun语言源码深度分析报告 - 元宝
作者: 一了 <1liao3@funlang.org> 日期: 2026-10-16  一句话总结: AI 做的 Fun 语言源码深度分析报告, 联名作者: 一了, 元宝。   

Fun语言源码深度分析报告 - 元宝

分析对象:https://github.com/funlang/fun,分支 main,版本 9.0 分析方式:拉取完整源码后逐层静态阅读(非 README 转述)。文中所有行号以仓库 src/ 为基准。 分析日期:2026-10-16


一、结论先行

Fun 不是玩具,但也不是一门"通用替代型"脚本语言。它的真实定位是:

一个用 Pascal 写的、极紧凑的树遍历解释器内核(自写代码约 1.27 万行),外挂一条「从数据结构一路下沉到机器码」的能力阶梯,并把 61 个标准库模块用 Fun 自己重写了一遍。

三个最值得注意的判断:

  1. 内核极小、但边界极宽
    。真正手写的内核只有 ~12.7k 行 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,692 行
(.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,778 行 / 88 个文件fun/lib/
,纯 Fun 自写
示例与测试
fun/demo
(49)、fun/test(59 脚本 + 93 份快照)
快照对比式回归
Star / Fork / Issue
19 / 1 / 3(2026-08-30 建库)
开源仅一个多月,社区几乎为零
提交
258 次,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 玩到极致

// 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

// 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 节点和运行时值的容器:

// 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 解压,注释里明确写了这条优化)。
  • 平台中性名
    :use 'lib-proc.fun' 不必写平台后缀——CParser.PlatformFile(parse.pas:1032-1060)先按目标试平台专属变体(macOS -darwin、Linux -lnx),没有再退共享的 -unx,命中即用;只有 Unix 上都没命中(或在 Windows 上,这段探测整段 { .# 3。
  • 关键字别名层
    :ParseAlias(parse.pas:179)在词法期把别名换成规范关键字,-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/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/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 文件既是文档、又是测试、又是教程。


    八、标准库与跨平台约定

    61 个模块、12.8k 行纯 Fun 代码,覆盖 JSON/YAML/XML/Base64/MD5/CStruct、集合与算法、字符串/正则/Unicode、文件/OS/进程/时间、网络(winsock/ajax/jsonrpc,这一轮又加了 HTTP 服务端 http-server 与阻塞式客户端 http-client,服务端目前 Linux/macOS 可用)、数据库(ORM 系列、ADO)、UI(甚至有一个用 Fun 写的记事本 notepad--.fun)、异步。

    平台后端约定清晰:Windows 用 lib-x.fun,Unix 用 lib-x-unx.fun(旧的 -lnx 变薄封装),共享逻辑放 lib-x-base.fun;中性名 use 'lib-x.fun' 现在会自动探平台专属变体(macOS -darwin、Linux -lnx,再退 -unx)。例如 lib-asm 在 Linux 走 libc mmap、macOS 走 libSystem、Windows 走 VirtualAlloc;lib-proc 这一轮补了 macOS 后端(libproc + mach VM,202 行),全部在 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),快照回归:59 个测试脚本对比 expected/ 下 93 份快照,平台差异用 regression-skip-<platform>.txt 声明,--update 写平台专属快照;这一轮 Linux arm64 的 lib-asm-a64.fun 快照也从 skip 名单移出,真在 CI 上跑。
    • .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=147 ns
    (增量 +40.6)
    一次 Variant 二元运算
    1+2
    (78) − var a=1(47)
    31 ns
    一次变量读取
    (i1+i2141 − 1+278) ÷ 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 看三档性能阶梯。

    本报告首发于 2026-10-16,同日随上游 cf2d6a3 刷新了一轮:标准库计数、网络章节与平台后端说明(新增 HTTP 服务端/客户端、lib-proc 的 macOS 后端、平台中性文件名解析)。报告基于 main 分支源码逐文件阅读;未能在本环境中编译运行(缺少 FPC 工具链),故实现性结论来自代码与文档推断。§10 的性能数字取自仓库中随源码提交的实跑输出(test-speed.fun、test-lib-asm-fun-9-2.fun 末尾注释块),非本次实测。

猜你喜欢

发表评论

发表评论: