Sign in

    moonbit_static_analysis

    Three-inspection (structural / type / behavior) static analysis for MoonBit programs, every toolchain file kind (.mbt / .mbtx / .mbti / .mbt.md / .mbtp) and state-machine tables

    static-analysis
    linter
    state-machine
    abstract-interpretation
    file-kinds
    mbti
    literate-markdown
    formal-verification
    Download zip
    Author
    Version
    0.4.7
    License
    MIT
    Last updated
    6 hours ago
    Downloads
    82

    Dependencies

    #moonbit_static_analysis

    中文 | English

    riantr/moonbit_static_analysis — 三鉴(结构/类型/行为)静态分析流水线,一个基础设施,两种用途:

    1. 程序代码修订:分析快速进化中的 MoonBit 语言的程序(未定义名、未用绑定、类型错配、死分支、不可达代码);
    2. 静态状态修订:为多层状态机提供通用机器表审计(src/statecheck)——被测对象调用本模块,把机器表作为纯数据喂进来。参考消费方是 riantr/pyroduct(主体/群体/社会/进化层状态机族),其 audit 包用真实机器表调用本模块做黑盒测试。

    一条流水线贯穿两者:结构走查 → 类型/符号 → 抽象解释 → 统一报告。

    #0.4.7 要点

    .mbti 接口审计学会了跨文件解析类型。 生成的接口文件大量引用声明在同包其它文件 (或 extern 目录)里的类型——&Show 参数、raise SnapshotError 返回、impl Debug forBenchError 的目标——此前 @moonfiles.iface 只看单文件,core 上 13 个真实 .mbti 因此被误报「未知类型」。本版为接口审计建立模块级接口符号表:对扫描目标的每个 .mbti 逐文件收集声明与 pub using 再导出后合并,解析顺序为本文件声明 → 内建名 → 类型参数 → 任一已扫接口(仅类型)。core 上 .mbti 缺陷 13 → 0,actionable 49 → 36。

    顺带修掉:pub using @pkg {type T} 形状的再导出此前一条也没被收集(24 个 type + 20 个 trait 的 prelude 段因此不可见),现在进表。

    实测(moonbitlang/core,1029 文件,同一台机器):

    文件解析actionable未定义名
    0.4.610293204917*
    0.4.710293203617

    * 勘误:0.4.6 行的未定义名发布时误记为 0,与该版自己的扫描产物不符——0.4.6 的 扫描里同样有这 17 条,本次核对原始产物后按实测更正;.mbti 修复没有触碰它们。

    剩余 36 条的构成(下一层的靶子):17 条未定义名 = 11 条跨文件函数引用(hex / base64 的 v128 bench 文件调用同包其它文件定义的 sample_bytes / encode_scalar)+ 3 条 引用子集外声明的名字(E_MAX / b / N)+ 3 条其余形态(is / e);其余 19 条 是未用绑定等其它缺陷。

    自检(扫本仓库自身源码)保持 0。测试 231 → 233。

    #安装 / 快速上手

    moon add riantr/moonbit_static_analysis@0.4.7

    // 用途一:修订一段 MoonBit(当前子集)程序
    let result : @pipeline.PipelineResult = @pipeline.run(source, "main.mbt")
    println(@pipeline.render_result(result))

    // 用途二:审计一台状态机(机器表以纯数据进来)
    let spec : @statecheck.MachineSpec = { name: "主体", states: [...], ... }
    println(@statecheck.render(spec))

    #三鉴(结构/类型/行为)(流水线核心)

    三鉴按"后者消费前者的表"组装:

    鉴包职责产出
    结构src/walk赋值即绑定、未用/未定义/参数被改、常量折叠剪枝绑定表 fn_bindings
    类型src/types注解即契约、推断、赋值/实参/条件检查(声明取自绑定表)签名表 sigs
    行为src/interp格上抽象解释、按签名调度、虚栈(方法表取自签名表)行为发现
    合并src/pipeline同一缺陷的多鉴回声 → 一条报告,lens 并集render_result

    四个组装点:绑定表→符号表(组装点1)、签名表→方法表(组装点2)、常量折叠的格化(组装点3)、合并去重(组装点4)。

    #用途一:程序代码修订(快速进化中的 MoonBit 语言)

    moon run src/cli # demo:6 个样例 × 三鉴

    === undefined.mbt === 2:10 - error: undefined variable 'missing' (UndefinedName) [structural+type+behavior] in main() at undefined.mbt:4 in g at undefined.mbt:1 Summary: 1 finding(s) (before merge: structural 1, type 2, behavior 1)

    一条缺陷三鉴都看见 → 一条报告,标签是并集;合并前后计数都保留(合并无损)。死分支被剪两次(结构层 const_eval + 行为层 ABool 格值),不存在的 typo_fn 零报告。

    #用途二:静态状态修订(机器表 → 三鉴)

    src/statecheck 接受任意状态机的纯数据规格 MachineSpec(状态 / 初始 / 终点 / 迁移 / 触发-槽位 / Block 理由 / 修习历程),三鉴读机器表就像读程序:

    鉴机器侧语义检查内容
    结构状态即绑定("赋值即绑定"推广到机器表)任何状态都必须被至少一条迁移绑定(出或入);只出不进 → never entered;从初始位置的可达性闭包;终点必须可达(机器必须能完成)
    类型驱动槽契约("注解即契约")每个触发必须归位已知槽、无空槽;Block 必须带理由——无路必须说出口,不能沉默
    行为轨迹抽象执行(虚栈)修习历程每一步都必须有触发承载;断链的轨迹步带入口帧→出错帧的虚栈

    let spec : @statecheck.MachineSpec = { name: "主体", states: [...], ... }
    let findings : Array[@report.Report] = @statecheck.audit(spec)
    let text : String = @statecheck.render(spec) // 合并后的文本报告

    pyroduct 是被测对象,不是依赖:本模块自身不依赖 pyroduct;方向是 pyroduct(其 audit 包)用真实机器表构造 MachineSpec 调用本模块。依赖单向:机器 → 分析器。

    pyroduct 侧的真实自审计(moon run cmd/main -- audit)。它 pin 的是 0.2.0,所以 下面输出里出现的 0.2.0 指那个 pin,不是上面 moon add 的安装版本:

    规格 | 计数 ---|--- 状态 | 034 迁移 | 053 触发→槽 | 049 → 8 无路组合(带理由) | 228 修习历程 | 031 站 已知设计(4 条,出处层:设计使然——0.2.0 审计已验证): - state '无忆' is never entered: it appears only as a transition source - state '无筹' is never entered: it appears only as a transition source - state '无忆' is unreachable from the initial position '立位' - state '无筹' is unreachable from the initial position '立位' 未预期发现:**0 条**

    无忆/无筹(原文:失忆/浑噩)是主体机器(34 位·53 迁·8 槽)里真实存在的两个"只出不进且不可达" 位置——pyroduct 自己的 192 个测试之外,用另一套三鉴语言独立复核出机器的论文边界 (「记不起来的过去」与「尚未到来的未来」)。"未预期发现 0 条"是回归绊线:三鉴在真实表上 任何新增报警都会让 pyroduct 的测试失败,强制复审。

    moon run src/cli 末尾那台 pyroduct 形状示例机器 是本模块内嵌的 8 状态玩具机 (statecheck.pyroduct_flavored()),不是 pyroduct 的真表;它带 182:12 这类 行:列,那是 state_span() 按状态名哈希出来的稳定伪 span(同一名字永远同一坐标), 不是任何源文件的位置——别拿它去 pyroduct 里对行号。

    #把扫描结果渲染成状态图(public API)

    @sa.mermaid_states(verdicts, summary, prov, per_file, max_files) 把一次扫描渲染成 mermaid stateDiagram-v2——和 CLI 打印的是同一批发现,只是形状从"给人扫一眼"换成 "给程序读"。

    每个文件的框的主干就是那些计数所依赖的信任链:lexed → structural → type → behavioral。 前端没读完的文件停在那条线上,越界之后的发现计数但不画。每条发现是自己的状态, 挂在报告它的那一鉴下面,带一条 note:WHY:(哪条规则、依据什么证据触发)与 HOW: (该改什么),取自 @core.Family::advice。

    有两个 family 带着读者动手之前必须知道的警告,而且都写在 note 正文里: .mbti / .mbtp / .mbt.md 上的 ParseError 通常是前端规则而不是解析边界; UndefinedName 有两个已知盲区(#doc(hidden) 的 API 不出现在任何 .mbti 里; @pkg.fn(1, 2) 这种顶层跨包引用不会被折叠成可调用名)。

    per_file 与 max_files 限制图的规模,0 表示不限制。被截掉的部分一定会在图里写出来 ——写在头部和该文件的 note 上,所以被截断的图不会被读成"干净的图"。summary 永远描述 整个扫描,而画出来的可能只是子集。文件按可行动数降序取,不是按原始发现数:在 moonbitlang/core 上,原始发现最多的那个文件是一个 402 条发现的测试文件,而那 402 条全是 notice、零缺陷——按原始数排,它会排在这张以"先看该修什么"为职责的图的第一位。

    let root = /* 已解析到的源码根目录 */
    let prov = @sa.provenance_for("my/module@1.2.3", root)
    let text = @sa.mermaid_states(verdicts, @sa.aggregate(verdicts), prov, 8, 25)
    println(scan_artifact_name(prov, root))

    输出是合法 mermaid:已用 mermaid@10.9.8 对 moonbitlang/core 的真实扫描(1029 文件)做过 解析 + 渲染双重验证。渲染器围绕两条实测出来的文法约束写成:

    • 迁移标签是第一个 : 之后的全部内容,所以标签里不能再有 :——行号和消息因此放在 带引号的 state 标签里,绝不放箭头上
    • MoonBit 没有运算符续行,源码里以 + 开头的行是语法错误——所以文档是一段一段 write_string 拼出来的,原因就是这个

    src/sa/mermaid_test.mbt 每次运行都把生成的文本按这套文法重读一遍——每一行都必须是 mermaid 接受的形态、每个节点 id 必须是标识符、每条迁移的端点必须是已声明的状态——并且 它自己被证明能抓住五个它本该抓住的文档。

    #给产物命名:版本与扫描时间写进文件名

    一次扫描结果是对某一棵树、由某套工具链、在某一时刻的断言。三件事都放进文件名—— @sa.provenance_for(target, root) 负责收集,@sa.scan_artifact_name(prov, root) 负责排布:

    moonbit_static_analysis_0.4.7_0.1.20260920_20261009T155222Z.mmd moonbitlang-core_0.1.20260920+7d59c7ec9_0.1.20260920_20261009T051925Z.mmd tree_unknown_0.1.20260920_20261009T051511Z.mmd # 版本没找到

    格式是 <包名>_<版本号>_<moonbit版本>_<时间戳>.mmd。四个字段各回答一个问题:

    字段回答来源
    package读的是什么坐标,或解析后的目录名
    version它的哪个修订目标自己的 moon.mod(moon.mod.json 也读)
    moonbit是哪套工具链读的子进程跑 moon version
    stamp什么时候@async.now(),UTC

    moonbit 是唯一没法从树里读回来的字段,也是决定两次扫描能不能比较的那个。moon.mod 只记 模块自己的版本和依赖,从不记编译它的工具链;也没有 MOONBITS_* 环境变量可问(实测:env 里 过滤 MOON 是空的)。所以只能去问工具链本身。这一点是实测的而不是照着文档念: moon version 打出

    moon 0.1.20260920 (914d7da 2026-09-20)

    取第二个字段。它是一次子进程调用,所以只在真的要写图时跑一次;失败就写 unknown, 而不是写错。

    对本工具尤其重要:仅仅因为把方法调用建进 AST,这个 .mbt 子集在同一份没变过的 core 检出上就从 158 个文件涨到 173 个。结论随工具链走,所以不写明工具链的扫描结果没法跟后来的 结果比较。

    包名是坐标的模块名;路径则是解析后的目录名——扫 . 是最常见的用法,照着调用方写的 字符串命名会得到 _.mmd。

    版本优先取坐标里的 @version,没有就读目标自己的 moon.mod(moon.mod.json 也读—— 两种写法在现场都存在)。时间戳是 UTC,来自 @async.now(),这一点是实测的而不是照着 文档注释念:wasm 跑出 1791522911114,而约 1.1 秒前读到的墙上时钟是 1791522910008, 差值正好是两次调用之间的延迟。这里没有时区数据库,所以时间戳取 UTC 并用结尾的 Z 说明, 而不是一年错两次小时。

    本轮更正。 上一版这一节写着「CLI 根本没法打时间戳,因为唯一的时钟在 internal 包后面」。那是错的:moonbitlang/async 把它公开重导出了 (pub using @event_loop {now}),而且 provenance_for 从 0.4.5 起就在调 @async.now()。CLI 之所以写 unknown,只是因为它被塞了一个手写的空时间戳,而没有去调 那个本来就能用的函数。

    这件事刻意不做的三件事:

    • 不省略它没能确定的东西。 版本、工具链或时钟缺失时写字面量 unknown。省掉那一段会 得到一个"看起来完整"的名字,而且四个字段时更糟:会错位——读 core_0.1.2_20261009 的人会把时间戳当成工具链版本。
    • 不产出 dotfile。 以 . 开头的名字在 Linux/macOS 上 ls 根本列不出来,而没人 列得出来的产物等于没人找得回来。
    • 不把文件名当记录。 同样的三个字段会以 %% 注释重复写在文档正文里,因为文件 是会被改名的,改完名之后它的名字就不再是证据了。

    #从 CLI 直接写出一张图:--mmd / --mmd-auto

    渲染器本来只是库函数,现在不写代码也能用上:

    moonx riantr/moonbit_static_analysis@latest <target> --mmd out/scan.mmd moonx riantr/moonbit_static_analysis@latest <target> --mmd-auto

    两者都额外写出图,并在最后一行打印 MMD<TAB><路径>;stdout 上的文本报告一字未改, 仍然是权威输出。--mmd-auto 用上面那套命名,写进工作目录。

    这里有三个决定,都不是随手定的:

    • 要显式开,永远不是默认。 这张图天生有损:mermaid_states 按 actionable 数排序、 到自己的上限就停,并在正文里写明 %% THIS DRAWING IS A SUBSET。把图当唯一输出等于把 这句话藏起来,而且会悄悄弄坏所有 grep SUMMARY 的脚本。
    • --mmd 必须带路径,自动命名另给一个 --mmd-auto。 可选值在这里是歧义的: <target> --mmd <path> 里 --mmd 后面那个裸词同样可能是第二个目标,两种读法无法区分。 与其猜、然后以 "unexpected extra argument" 失败,不如让这个值必填。
    • 产物绝不落进被扫描的树里。 本模块写明的约束是只读目标的文件;往别人的源码仓库 里丢文件既违背这个承诺,也会弄脏一棵用户未必拥有的树。

    四个字段全是真的。在 moonbitlang/core 完整 1029 文件的扫描上实测:

    MMD _mbcore_0.1.20260920+7d59c7ec9_0.1.20260920_20261009T132547Z.mmd

    _mbcore 是解析后的目录名,0.1.20260920+7d59c7ec9 是目标 moon.mod 里的自己的版本, 0.1.20260920 是 moon version 报的工具链,20261009T132547Z 是真实的 UTC 时间戳—— 对照本地墙上时钟 20261009T212552,正好差预期的 +8。若 moon version 那个子进程失败, 该字段写 unknown,其余字段不受影响。

    写失败会打印 ERROR --mmd: cannot write <path>,stdout 其余部分不受影响——write_file 不会创建父目录,所以 --mmd no/such/dir/x.mmd 是干净失败,而不是留下半个文件。

    @sa.stamp_utc(ms) -> String 是纯函数、可单独使用:把 epoch 毫秒转成 YYYYMMDDTHHMMSSZ,民用日期用 Hinnant 的 civil_from_days 算,测试把答案钉在 "不会有争议"的日期上——epoch 本身、闰日、年界月界、一次真实运行报出的那个确切时刻, 以及 400 年世纪规则的两侧(2000 是闰年,2100 不是)。

    #包结构

    src/core Span/Severity/Lens/Family(merge_group 契约)/Frame + Family::advice src/report Report 结构与渲染(lens 并集标签、vst 框架) src/sa 扫描入口 + aggregate() + mermaid_states()(对外 library) src/lexer MoonBit 词法前端 src/parser MoonBit 语法 → ast(含 .mbtx 脚本的 import 块) src/ast MoonBit 抽象语法 src/walk 结构鉴:绑定表 + 结构发现 src/types 类型鉴:Ty/Ty?/Sig 推断 src/interp 行为鉴:AbsVal 格 + 调度 + 虚栈 src/pipeline 组装:跑三鉴 + merge_group 合并 src/moonfiles 其余文件种类:.mbt.md 逐块三鉴 / .mbti 接口审计 / .mbtp 证明 lint src/statecheck 用途二:通用机器表审计(MachineSpec 纯数据桥) src/samples demo 样例(内嵌 MoonBit 源) src/cli 可执行入口(程序 demo + 文件种类 demo + 示例机器审计)

    #文件种类(MoonBit 工具链的其它后缀)

    完整分类表见 EXTENSIONS.md,语义依据 docs.moonbitlang.com/en/latest(最终依据), 并与 mooncakes 的 moonbitlang/parser@0.4.3 / moonbitlang/lexer@0.4.2 对照过:

    后缀本项目静态分析
    .mbt三鉴程序分析
    .mbtx三鉴程序分析 + 导入块审计(条目文法 "path" [@alias] [*];重复路径报 FParse;导入清单随报告回显)
    .mbti接口审计:畸形行 / 重复签名 / 未知类型引用。行文法与 moon info 实际输出一致,生成文件不会报畸形行——但它的未知类型检查完全没有跨文件感知,在生成文件上仍会误报(实测 moonbitlang/core 上 93 条,而那棵树 moon check --target all 是 0 错误通过的)。这类发现按「未验证」对待,不要当缺陷报给用户
    .mbt.mdliterate:只分析工具链确实编译的围栏 —— mbt check / mbt test 及其 moonbit 写法;第二个词是 check 或 test 才使块成为活代码。行号对齐 .md 真实行。裸 mbt、裸 moonbit 与 nocheck 是展示块,跳过——按工具链实测,见 EXTENSIONS.md
    .mbtp证明文件逻辑侧 lint(体内字符串常量、!/↔ 禁形、跨包调用、lemma 缺 proof_ensure)——不替代 moon prove
    moon.mod / moon.pkg / workspace记录在案,不做静态分析(配置不是代码——官方也把两者放在 parser 的 moon_config 子包里,与 syntax / mbti_parser 并列而独立;理由见 EXTENSIONS.md「配置文件的边界」)

    .mbti 审计的行文法是照着真实生成物对齐的:moon info 实际会输出 import {} 块、 #deprecated / #alias(...) / #callsite(...) 属性行、const、impl … for T、 suberror、带 pub / async / extern / 类型参数 / 具名参数(input_offset? : Int)/ raise / String? 的 fn 签名。早期版本会把其中每一种都当成"畸形行"报出来, 也就是对每一个真实生成的接口文件都误报。

    #验证

    moon check --target all --deny-warn # 0 错 0 警(js / native / wasm / wasm-gc) moon test --deny-warn # wasm 192/192;js 与 wasm-gc 139/139 moon run src/cli # 程序 demo + 文件种类 demo + pyroduct 形状示例机器审计

    各 target 的数量不同是刻意的:src/sa 与 src/jsoncli 声明了 supported_targets = "+wasm+native",js 与 wasm-gc 会跳过这两个包而不是失败。 因此"四个 target 各 N"这种写法本身就是错的;只有 wasm 的数字覆盖渲染器与扫描器。

    --deny-warn 是有意的:官方包配置页要求「In CI, add --deny-warn to moon check, moon test, or the equivalent command to treat enabled warnings as fatal errors」。 少了它,「0 警」这句话没有任何东西强制 —— 冒出警告 CI 照样绿。.github/workflows/gate.yml 里的 CI 门禁因此带 --deny-warn;代价是工具链日后新增警告会让 CI 变红,这正是它的用途。

    CI 只跑 --target js;上面「四个 target」是本地跑的结论,CI 不覆盖 native/wasm。

    #形式验证(moon prove)

    src/core 是纯内核(无 Array 携带的结构体、无字符串),以 "proof-enabled": true 开启 MoonBit 2026 实验形式验证;src/core/core_proof.mbtp 是逻辑侧:谓词(is_error / type_error_family / lower_irreflexive_ok / lower_transitive_ok / lower_antisymmetric_ok / rank_order_ok)+ 19 条引理(rank 顺序三常数、lower 三律、rank_order_agrees、 severity 分类:5 个 Error 族 + 7 个 Warning 族逐条 + 全量 taxonomy)。

    moon prove src/core --why3-config .why3.conf # 生成 19 个 VC 并交 cvc5/alt-ergo

    验证状态(why3 降级产物 _build/verif/src/core/*.smt2 逐目标 cvc5 机检):

    • 21/21 目标全部 VALID(unsat):19 条引理 + span_at/zero_span 两个自动安全 VC。
    • 已知工具链限制(均为上游问题,非本项目代码):
      1. #proof_pure 函数体为结构体字面量时(pos/span_at/zero_span)降级为不透明逻辑符号 —— 函数体被丢弃,span 形状律在逻辑侧不可证(已由测试钉住,见 core_proof.mbtp 注释)。
      2. 捆绑 why3server(Windows 构建)丢失求解器 stdout —— why3 报 "High failure"、输出仅 .;裸 why3 ... prove CLI 同样复现。逐目标机检绕过: why3 -o <dir> -P cvc5 <mlw> 导出 SMT 后对每个文件跑 cvc5。

    本地复现所需 shim(不进发布 zip):.why3.conf(datadir/libdir 指向 .moon/share/why3 与 .moon/lib/why3,显式 [prover] 节)与 cvc5wrap.ps1(滤掉 why3 文件尾部的 get-info :reason-unknown —— cvc5 1.0.9 对确定性回答会报错,why3 1.7.2 解析失败)。

    #发布

    本模块通过以下渠道发布(同一份源码,三处同步):

    • mooncakes.io:moon publish(发布后 riantr/moonbit_static_analysis@0.4.7 可被任何 MoonBit 模块以 import 依赖;src/cli 附带 SKILL.md,上架 skills.mooncakes.io)。
    • Gitee / GitHub:git push 双推;tag 与 moon.mod 版本号保持一致。

    #分析其他项目(不引入本项目)

    本工具可以扫任意 MoonBit 仓库,而那个仓库既不依赖本模块、也不会被改动 —— 源码只在扫描时被读取:

    moonx riantr/moonbit_static_analysis@latest riantr/moonbit_doubleML@latest

    第一个参数是要扫的目标:registry 坐标(author/module,可带 @version / @latest)或一个本地路径;省略则扫当前目录。坐标会经 moon fetch 落到 .repos/ 再遍历。--exclude <目录> 可重复,用来跳过以普通名字存放的 vendored 源码(点目录规则抓不到它们)。

    #输出说的是「我读懂了什么」,不是「我数出多少条」

    SCANNED 107 files under ...\ML\CI CLEAN .mbti ...\CI/pof/pkg.generated.mbti PARTIAL .mbt defects=2 notices=1 read up to line 36, 57 after it not shown ...\CI/pof/aggregator.mbt 12:3 - error: ... (UndefinedName) [behavior] PARSED 32/107 files understood (29.9%); 75 more read only up to their first parse error SUMMARY files=107 parsed=32 actionable=57 notices=211 unreliable=15759 withheld=6769 total=16027 (actionable+notices+unreliable = total; withheld ⊆ unreliable)

    • PARSED 是头号指标。 工具链的前端只覆盖 MoonBit 的一个子集,读不懂的部分 报出来的「发现」量的是工具的盲区,不是你的代码。
    • 半读懂的文件仍然值得读。 解析器记录它真正读不下去的第一行;那之前的 发现照常报(PARTIAL),之后的计入 unreliable 且从不展示——它们来自一棵 已经错了的树,摆出来等于把这个工具最初的毛病又犯一遍。
    • 跳过声明不算读不懂。 「不分析 test 块」不会移动这条边界,因为块之后的 代码照样读得通。
    • actionable 不含告知。 「这种声明不分析」不是你的代码有缺陷——不然自检 会报「发现 92 个问题」而真实缺陷是 0。
    • 未解析文件按原因分组而不是一行一个,带示例路径。
    • actionable + notices + unreliable = total,这一行打印里直接断言成立。withheld 也印在旁边,但是 unreliable 的子集(无前置发现的 PARTIAL 文件的尾部发现),不计入等式。

    跨文件类型符号解析(0.4.3 新增):单个 .mbt 用到别的包里的类型(如 @argparse 的 Command)原本会报 undefined name 'Command'——单文件分析 看不到外部声明。--extern-iface-dir <dir>(可重复)把指定目录里所有 .mbti 的声明类型 + 值签名灌进跨文件符号表;结构走查和类型镜在报 FUndefinedName 之前先查这张表。默认还会读目标的 moon.mod 自动 moon fetch 直接依赖(--no-auto-fetch-deps 关掉)。类型命中返回 TUnknown、值命中 返回 TUnknownFn(新增的 Ty 变体:确定是个值、但签名没读到,所以 调用方 Command(...) 既不误报 FOpMismatch,也不会拿一个编出来的 arity 去判「参数过多」——.mbti 只给名字、从不给参数表,编出来的 arity 是猜测,检查会把它当事实报出去)。表里没有的名字照常报错—— 表只抑制、不发明。查表前会先剥掉 @pkg. 限定:.mbti 里声明是裸名 pub fn T::m,使用处必须写成 @pkg.T::m,两侧拼写不一致时精确匹配 永远打不中,而那正是唯一常见的跨包写法。

    已实测(改可信区间前后,同一批树):

    目标文件解析actionable(旧 → 新)
    riantr/moonbit_doubleML@latest → 0.113.018073(40.5%)0 → 123
    本仓库自身(.repos/0.3.5)3922(56.4%)0 → 7
    moonbit_linalg_gpu84(50.0%)1 → 5
    ML/CI(--exclude _qa_verify)10732(29.9%)11 → 57

    与 moon check 的关系要说清楚:在 .mbt 上,本工具不与工具链的编译器 竞争——moon check 有真正的解析器和约 90 条内建告警,覆盖率恒为 100%, 实测 2.3 秒 / 128 条,且每条都带 file:line:col、源码片段与修复建议。本工具的 .mbt 前端只读一个子集:在 moonbitlang/core 上,882 个 .mbt 解析了 173 个(19.6%),而 .mbti 与 .mbt.md 是 100%。原先最大的单一原因 方法调用在 AST 里根本没有位置——ECall 的被调名是 String,所以 m.set(k, v) 无处安放,解析就停在那里——本轮已修(新增 EMethodCall 节点, 三个 lens 都接上),该阻塞从 34 降到 0。这才是诚实的头条数字,也是 PARSED 要印在第一行的原因。本工具能说「编译器做不到」的地方是 .mbti / .mbt.md / .mbtp 三类文件与跨包语义。

    早先版本在同一份 ML/CI 代码上报过 529 条「未使用局部」,而编译器报 8 条。 那不是两种意见,而是没解析的 struct 函数体里那些字段被当成了没被读的绑定。 现在它报不出来了——被前端截断的函数会整函数退出未使用局部 / 未使用参数 / 参数被改写这三族审计,因为「从未被读」是关于整个函数的结论,半个函数 支撑不了这个结论。仅此一项修复就把 moonbitlang/core 从 270 条 actionable 降到 147 条,把本仓库从 11 条降到 0 条。

    再往下读,同一条原则还适用于模块帧,也适用于一类解析器从没学过怎么解析的 名字:前端没读完的文件,模块帧是可证不完整的,所以那里「找不到可见绑定」 什么也不能证明;而 Type::member 路径(@debug.Repr::opaque_(v))是成员引用 不是绑定,跟字段名、方法名本来的处理一致。再把 extern "c" fn 当成声明建模 (原先整条跳过,名字就丢了,于是每个 extern 函数的调用点都报未绑定),补上数值 字面量与 enum 构造器,moonbitlang/core 从 270 条降到 49 条,undefined name 从 64 条降到 17 条(发布 0.4.6 时误记为 0,与该版扫描产物不符,后按实测勘正), 本仓库仍是 0;0.4.7 再把 .mbti 审计的未知类型检查接到模块级接口符号表上, 其余 13 条误报清零,降到 36。

    moonbitlang/core文件解析actionable未定义名
    0.4.5102930527064
    0.4.610293204917
    0.4.710293203617

    根包是薄转发层,真正的实现在 src/sa(library)。设成 library 是因为「main 包 import 另一个 main 包」已被工具链标记为将来会报错;src/sa 与根包都声明 supported_targets = "+wasm+native",因为文件 IO 与子进程来自 moonbitlang/async,只有 wasm / native 后端有可用的 async 运行时(js 没有,wasm-gc 缺 run_async_main),其余后端会跳过这两个包而不是失败。

    #DeepSeek Harness 插件

    src/jsoncli 是 JSON 桥接入口(Node 下运行,一条 JSON 请求进、一条 JSON 应答出): {"kind":"program","source":...} 走程序三鉴,{"kind":"file","filename":...} 按扩展名 分派到对应文件种类的分析(.mbt / .mbtx / .mbt.md / .mbti / .mbtp), {"kind":"machine","spec":...} 走机器表审计。 它被打包为 DeepSeek Harness 插件 @riantr/moonbit-static-analysis-dsh(工作区目录 dsh-plugin-moonbit-static-analysis/),向 agent 暴露 moonbit_analyze / moonbit_analyze_file / moonbit_audit / moonbit_gates 四个工具——插件只是生成器与 格式化器,分析语义全部留在 MoonBit 侧、随模块一起版本化与跑门禁。 安装方式见该目录 README(plugin_manager 的 install_bundle)。

    Source Files