Three-inspection (structural / type / behavior) static analysis for MoonBit programs, every toolchain file kind (.mbt / .mbtx / .mbti / .mbt.md / .mbtp) and state-machine tables
Dependencies
| 文件 | 解析 | actionable | 未定义名 | |
|---|---|---|---|---|
| 0.4.6 | 1029 | 320 | 49 | 17* |
| 0.4.7 | 1029 | 320 | 36 | 17 |
moonx riantr/moonbit_static_analysis@latest riantr/pyroduct@latest| 修复 | 是什么 | 条数 |
|---|---|---|
| Repr 进内建表 | core/debug 里 Repr 枚举的构造器,隐式可见——而不在 core/builtin 接口里(表里其它名字全是从那儿抄的) | 16 |
| 插值里的 import 别名 | @ 不是标识符字符,所以 \{@sm.all_states()} 记录别名时丢了 @,walk 里 !has_prefix("@") 那道闸根本看不见。这是 0.4.8 自己引入的回归 | 6 |
| extend 是声明 | 当成语句读时,extend 变成了一个名字 | 8 |
| C 风格三段式 for | MoonBit 的另一种 for;把它的头部当绑定变量读,就会要求 in | 覆盖率 |
| 探针 | 结果 |
|---|---|
| 顶层可达,真实那行 | 报 Byte::default |
| 去掉顶层调用 | 完全静默 |
| Array::make 单独调用 | 被报 |
| Byte::default 单独调用 | 被报 |
| 外层也不存在 + 内层 Byte::default | 只报内层 |
| 写法 | 结果 |
|---|---|
| Array::make(4, 0) | 静默(名字在表里) |
| Array::mkae(4, 0) | 被报(名字不在表里) |
fn every_mode() -> Array[Mode] {
[Select, Rect, Polygon, Keypoint, EditBinding]
}| 语料 | 0.4.16 | 0.4.17 |
|---|---|---|
| riantr/snn_mbt 0.155.0 | 6 | 6(全真阳性,未动) |
| riantr/moonbit_doubleML 0.139.0 | 0 | 0 |
| riantr/moonbit_labeler 0.2.13 | 5 | 3 |
| riantr/moonbit_image 0.3.7 | 0 | 0 |
| riantr/pyroduct 0.1.29 | 0 | 0 |
| 合计 | 11 | 9 |
| 位置 | 发现 | 判据 |
|---|---|---|
| neuron_identity.mbt | 参数 dt 从未被读 | 读源码确认函数体里没有 dt |
| dsac.mbt ×2 + 3 个 examples/*/main.mbt | for a in 0..<n 的绑定变量 a/s 从未被读 | 同上;探针确认 moon check 对未读的 for 绑定变量也告警 |
| 成因 | 条数 | 位置 |
|---|---|---|
| 跨包 const 被当成函数 | 4 | IfaceSymTab.values 的 Bool 一直写 true、没人读 |
| 带花括号的 suberror 体被当成表达式 | 4 | 一律走「跳到行尾」的 skip |
// snn_mbt/istdp.mbt
r: 3.0F * hz, // hz 是 @snn_kw 的 const hz : Float限定名一度被过度报告(Layer::ReLU、fn hz),修完之后它们变成了 报告不足。
let pixels : Array[Byte] = Array::make(total, Byte::default())| 语料 | 0.4.13 | 0.4.14 | 0.4.15 | 0.4.16 |
|---|---|---|---|---|
| snn_mbt | 33 | 19 | 9 | 6(全真) |
| moonbit_doubleML | 28 | 3 | 1 | 0 |
| moonbit_labeler | 6 | 5 | 5 | 5(加 --extern-iface-dir 为 2) |
| moonbit_image | 4 | 4 | 4 | 0 |
| pyroduct | 0 | 0 | 0 | 0 |
| 合计 | 71 | 31 | 19 | 11 |
| 成因 | 条数 | 判据 |
|---|---|---|
| extern "C" 的参数被判「未使用」 | 6 | 探针:extern 不告警,同文件的普通 fn 告警 |
| Bool == Bool 被判「不支持的操作数」 | 2 | 探针:0 error 0 warning |
| 单元(无载荷)枚举变体读不到 | 4 | 探针:0 error;同 enum 的带载荷变体本来就正常 |
enum Layer {
Conv2d(Conv2dParam) // 能读
ReLU // 报 undefined name
}| 写法 | moon check |
|---|---|
| extern "C" fn expf(x : Float) -> Float = "expf" | 静默 |
| fn p7(z : Float) -> Float { } | 告警 |
| fn p6(y : Float) -> Float { 1.0 } | 告警 |
| 语料 | 0.4.13 | 0.4.14 | 0.4.15 |
|---|---|---|---|
| snn_mbt | 33 | 19 | 9 |
| moonbit_doubleML | 28 | 3 | 1 |
| moonbit_labeler | 6 | 5 | 5 |
| moonbit_image | 4 | 4 | 4 |
| pyroduct | 0 | 0 | 0 |
| 合计 | 71 | 31 | 19 |
| 成因 | 条数 | 判据 |
|---|---|---|
| F 后缀浮点字面量 0.0F 未建模 → undefined name 'F' | 28 | moon run 确认合法;7F 才是语法错 |
| / 被当成浮点除法 → cannot assign 'float' into '[int]' | 14 | 7 / 2 = 3,不是 3.5 |
| 指数记法 1.0e-12 未词法化 → undefined name 'e' | 14 | moon run 确认合法 |
| suberror 体被二次扫描 → undefined variable 'String' | 4 | 同一行已报 ParseError |
1.5 -> 1 | 0.25 -> 0 | 3.14159 -> 3| 语料 | 文件 | 可解析 | actionable |
|---|---|---|---|
| riantr/snn_mbt 0.155.0 | 673 | 234 → 603 | 33 → 19 |
| riantr/moonbit_doubleML 0.139.0 | 205 | 145 → 165 | 28 → 3 |
| riantr/moonbit_labeler 0.2.13 | 37 | 16 → 18 | 6 → 5 |
| riantr/moonbit_image 0.3.7 | 18 | 1 | 4 |
| riantr/pyroduct 0.1.29(不回退) | 119 | 103 → 104 | 0 |
| 合计 actionable | 71 → 31 |
| 成因 | 条数 | 位置 |
|---|---|---|
| color[u]:把 Map 当成 list 报下标类型错 | 2 | src/paths.mbt 135/152 |
| self 只在插值里被读 → 报参数未用 | 1 | src/slot.mbt 127 |
| _ 前缀的未用绑定/参数(分析器与编译器规则相反) | 2 | src/algebra.mbt 118、audit/mbti.mbt 69 |
| 真错误 | 误报 | |
|---|---|---|
| 0.4.12 | 3 / 3 | 1(Map) |
| 0.4.13 | 3 / 3 | 0 |
no visible binding for 'is_coordinate'
no visible binding for 'split_coord'let stem = if is_coordinate(p.target) { // 226 行——if 表达式
...
if is_coordinate(target) { // 411 行——if 语句moonx riantr/moonbit_static_analysis@latest riantr/moonbit_static_analysis@latest --mmd-autoMMD riantr-moonbit_static_analysis_0.4.8_0.1.20260920_20261010T235302Z.mmd
%% version=0.4.8| 轮 | 已解析 | actionable | 那一轮修了什么 |
|---|---|---|---|
| 起点(0.4.7) | 46/46 | 104 | —— |
| 第二轮后 | 46/46 | 8 | 被跳过的表达式、walk_target 穿透写、过期 .mbti |
| 第三轮后 | 46/46 | 0 | 字符串插值里的名字不可见 |
moon add riantr/moonbit_static_analysis@0.4.17// 用途一:修订一段 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 |
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)| 鉴 | 机器侧语义 | 检查内容 |
|---|---|---|
| 结构 | 状态即绑定("赋值即绑定"推广到机器表) | 任何状态都必须被至少一条迁移绑定(出或入);只出不进 → never entered;从初始位置的可达性闭包;终点必须可达(机器必须能完成) |
| 类型 | 驱动槽契约("注解即契约") | 每个触发必须归位已知槽、无空槽;Block 必须带理由——无路必须说出口,不能沉默 |
| 行为 | 轨迹抽象执行(虚栈) | 修习历程每一步都必须有触发承载;断链的轨迹步带入口帧→出错帧的虚栈 |
let spec : @statecheck.MachineSpec = { name: "主体", states: [...], ... }
let findings : Array[@report.Report] = @statecheck.audit(spec)
let text : String = @statecheck.render(spec) // 合并后的文本报告规格 | 计数
---|---
状态 | 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 条**moon run src/cli 末尾那台 pyroduct 形状示例机器 是本模块内嵌的 8 状态玩具机 (statecheck.pyroduct_flavored()),不是 pyroduct 的真表;它带 182:12 这类 行:列,那是 state_span() 按状态名哈希出来的稳定伪 span(同一名字永远同一坐标), 不是任何源文件的位置——别拿它去 pyroduct 里对行号。
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))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 # 版本没找到| 字段 | 回答 | 来源 |
|---|---|---|
| package | 读的是什么 | 坐标,或解析后的目录名 |
| version | 它的哪个修订 | 目标自己的 moon.mod(moon.mod.json 也读) |
| moonbit | 是哪套工具链读的 | 子进程跑 moon version |
| stamp | 什么时候 | @async.now(),UTC |
moon 0.1.20260920 (914d7da 2026-09-20)本轮更正。 上一版这一节写着「CLI 根本没法打时间戳,因为唯一的时钟在 internal 包后面」。那是错的:moonbitlang/async 把它公开重导出了 (pub using @event_loop {now}),而且 provenance_for 从 0.4.5 起就在调 @async.now()。CLI 之所以写 unknown,只是因为它被塞了一个手写的空时间戳,而没有去调 那个本来就能用的函数。
moonx riantr/moonbit_static_analysis@latest <target> --mmd out/scan.mmd
moonx riantr/moonbit_static_analysis@latest <target> --mmd-autoMMD _mbcore_0.1.20260920+7d59c7ec9_0.1.20260920_20261009T132547Z.mmdsrc/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 + 示例机器审计)| 后缀 | 本项目静态分析 |
|---|---|
| .mbt | 三鉴程序分析 |
| .mbtx | 三鉴程序分析 + 导入块审计(条目文法 "path" [@alias] [*];重复路径报 FParse;导入清单随报告回显) |
| .mbti | 接口审计:畸形行 / 重复签名 / 未知类型引用。行文法与 moon info 实际输出一致,生成文件不会报畸形行——但它的未知类型检查完全没有跨文件感知,在生成文件上仍会误报(实测 moonbitlang/core 上 93 条,而那棵树 moon check --target all 是 0 错误通过的)。这类发现按「未验证」对待,不要当缺陷报给用户 |
| .mbt.md | literate:只分析工具链确实编译的围栏 —— 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「配置文件的边界」) |
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 形状示例机器审计CI 只跑 --target js;上面「四个 target」是本地跑的结论,CI 不覆盖 native/wasm。
moon prove src/core --why3-config .why3.conf # 生成 19 个 VC 并交 cvc5/alt-ergo| 渠道 | 地址 |
|---|---|
| Gitee(主仓) | https://gitee.com/ren-yongxiang/moonbit_static_analysis.git |
| GitHub(镜像) | https://github.com/riantr/moonbit_static_analysis |
| mooncakes.io(包注册表) | https://mooncakes.io/docs/riantr/moonbit_static_analysis |
moonx riantr/moonbit_static_analysis@latest riantr/moonbit_doubleML@latestSCANNED 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)| 目标 | 文件 | 解析 | actionable(旧 → 新) |
|---|---|---|---|
| riantr/moonbit_doubleML@latest → 0.113.0 | 180 | 73(40.5%) | 0 → 123 |
| 本仓库自身(.repos/0.3.5) | 39 | 22(56.4%) | 0 → 7 |
| moonbit_linalg_gpu | 8 | 4(50.0%) | 1 → 5 |
| ML/CI(--exclude _qa_verify) | 107 | 32(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.5 1029 305 270 64 0.4.6 1029 320 49 17 0.4.7 1029 320 36 17
Install
Download zipThree-inspection (structural / type / behavior) static analysis for MoonBit programs, every toolchain file kind (.mbt / .mbtx / .mbti / .mbt.md / .mbtp) and state-machine tables
Dependencies