Three-inspection (structural / type / behavior) static analysis for MoonBit programs, every toolchain file kind (.mbt / .mbtx / .mbti / .mbt.md / .mbtp) and state-machine tables
moon add riantr/moonbit_static_analysis@0.4.33import {
"riantr/moonbit_static_analysis@0.4.33",
}moonx riantr/moonbit_static_analysis@latest riantr/moonbit_doubleML@latest// Use 1: revise a MoonBit (current-subset) program
let result : @pipeline.PipelineResult =
@pipeline.run(source, "main.mbt", @core.IfaceSymTab::empty())
println(@pipeline.render_result(result))
// Use 2: audit a state machine (machine tables come in as plain data)
let spec : @statecheck.MachineSpec = { name: "主体", states: [...], ... }
println(@statecheck.render(spec))moon run src/cli # 6 samples × 3 inspections, the file-kind demos, a sample machine| Inspection | Package | Responsibility | Output |
|---|---|---|---|
| Structural | src/walk | assignment-as-binding, unused/undefined/param-reassigned, constant-folding pruning | binding tables fn_bindings |
| Type | src/types | annotations-as-contracts, inference, assignment/argument/condition checks (declarations come from the binding tables) | signature tables sigs |
| Behavior | src/interp | abstract interpretation over a lattice, signature-dispatched methods, virtual stacks (the method table comes from the signature tables) | behavior findings |
| Merge | src/pipeline | one defect's echoes across inspections → one report, union of lenses | render_result |
=== 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)| Inspection | Machine-side semantics | What it checks |
|---|---|---|
| Structural | states are bindings ("assignment-as-binding" lifted to machine tables) | every state must be bound by at least one transition (in or out); out-only → never entered; reachability closure from the initial position; the terminal must be reachable — the machine must be able to finish |
| Type | the drive-slot contract ("annotations-as-contracts") | every trigger must be placed into a known slot, no empty slots; a Block must carry a reason — a road-less step must say so out loud, never silently |
| Behavior | the course as abstract execution (virtual stacks) | every course step must be carried by some transition; a stranded step carries a virtual stack from entry frame → error frame |
let spec : @statecheck.MachineSpec = { name: "主体", states: [...], ... }
let findings : Array[@report.Report] = @statecheck.audit(spec)
let text : String = @statecheck.render(spec) // merged text report规格 | 计数
---|---
状态 | 034
迁移 | 053
触发→槽 | 049 → 8
无路组合(带理由) | 228
修习历程 | 031 站
未预期发现:**0 条**The pyroduct-shaped sample machine printed at the end of moon run src/cli is this module's own 8-state toy (statecheck.pyroduct_flavored()), not pyroduct's real tables. It carries coordinates like 182:12; those are stable pseudo-spans hashed from the state name, not positions in any source file.
| function | signature |
|---|---|
| run | (src : String, file : String, extern_iface : @core.IfaceSymTab) -> PipelineResult |
| run_functions | (src : String, file : String, extern_iface : @core.IfaceSymTab) -> PipelineResult |
| render_result | (r : PipelineResult) -> String |
| merge_findings | (all : Array[@report.Report]) -> Array[@report.Report] |
| merge_group | (g : Array[@report.Report]) -> @report.Report |
| function | signature |
|---|---|
| audit | (spec : MachineSpec) -> Array[@report.Report] |
| render | (spec : MachineSpec) -> String |
| pyroduct_flavored | () -> MachineSpec — the 8-state sample machine |
| function | signature |
|---|---|
| mermaid_states | (verdicts : Array[Verdict], summary : Summary, prov : ScanProvenance, per_file : Int32, max_files : Int32) -> String |
| provenance_for | async (target : String, root : String) -> ScanProvenance |
| scan_artifact_name | (p : ScanProvenance, root : String) -> String |
| aggregate | (verdicts : Array[Verdict]) -> Summary |
| stamp_utc | (ms : Int64) -> String |
| moon_mod_version | (src : String) -> String |
moonbit_static_analysis_0.4.33_0.1.20260920_20261009T155222Z.mmd
moonbitlang-core_0.1.20260920+7d59c7ec9_0.1.20260920_20261009T051925Z.mmd
tree_unknown_0.1.20260920_20261009T051511Z.mmd # version not determined| field | question | source |
|---|---|---|
| package | what was read | the coordinate, or the resolved directory |
| version | which revision of it | the target's moon.mod (moon.mod.json too) |
| moonbit | which toolchain read it | moon version, run as a subprocess |
| stamp | when | @async.now(), UTC |
| function | signature |
|---|---|
| Report::render | (self : Report) -> String |
| render_reports | (reports : Array[Report]) -> String |
moonx riantr/moonbit_static_analysis@latest <target> [flags]| flag | meaning |
|---|---|
| <target> | registry coordinate (author/module[@version]) or a path. Omit it to scan the current directory. |
| --exclude <dir> | a directory name to skip. Repeatable. It exists because a dot-directory rule cannot catch everything: a project may keep vendored registry snapshots under an ordinary name. |
| --extern-iface-dir <dir> | a directory to scan for .mbti files whose declared types/values are treated as valid in every .mbt file under analysis. Repeatable. The natural target is .repos/ after moon fetch of the dependencies you want indexed. The target's own tree is always indexed. |
| --no-auto-fetch-deps | by default the analyzer reads the target's moon.mod and moon fetches each direct import. Pass this to skip the auto-fetch. |
| --mmd <path> | also write the scan as a mermaid stateDiagram-v2 to <path>, and print MMD<TAB><path> on the last line. Opt-in on purpose: stdout stays the authoritative text report. |
| --mmd-auto | the same, but pick the name and write it to the working directory — never inside the analysed tree. |
| --per-function | enter the behavioral lens at each function rather than at the module. |
| --findings-shown-per-file N | how many findings are printed per file (default 40). A printing limit, not an analysis limit: the SUMMARY counts come from every finding, so raising it reveals findings that were already counted and changes no summary number. |
The CLI cannot exit non-zero. MoonBit exposes no process-exit entry point in the packages available here, so a target that cannot be located prints ERROR and still exits 0 — a caller must read the output, not the exit code.
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)| notice | n | notice | n | |
|---|---|---|---|---|
| test declaration | 1175 | anonymous function literal | 10 | |
| match expression | 299 | const declaration | 6 | |
| if expression | 67 | short lambda | 2 | |
| struct declaration | 58 | try expression | 1 | |
| derive declaration | 52 | |||
| enum declaration | 24 |
This view is not yet sound, and the flag is opt-in for that reason. It is measured across all seven pinned corpora by _verify/census_perfunction.ps1, and the release gate pins the number. Today it reports 58 actionable findings, where the module view reports 33. Not one of the 58 is a defect in the analysed code.
family n what the lens claimed A 12 a hex float literal (0x1.0p1023) is not read, so its exponent part is reported as an undefined name B 5 a BigInt literal's N suffix (0N) is not read C 6 == between two arrays D 3 Int + Char and List + List E 3 * on a struct F 9 a name the analyzer cannot see — a sibling-file helper, a type parameter's method, an internal API absent from the .mbti index, a module qualifier G 11 unreachable code — the lens's own claim about the code H 2 a local assigned and never read — the lens's own claim about the code I 1 abs on a Double J 1 * on two function values K 3 a local call with a named argument L 2 the documented #doc(hidden) limit "Accepted" is measured, not assumed. moon check --target all on moonbitlang/core reports exactly one error, in json/README.mbt.md, and it is not one of the twenty-four files that carry a finding. That is only half of it: a file with no diagnostic is indistinguishable from a file nobody opened, so _verify/control_perfunction_census.ps1 appends a deliberate type error to each of those files in a copy and requires the error back — 24 of 24.The two in family L are the documented limit rather than a defect: a cross-package #doc(hidden) function cannot be indexed from the dependency's source — only from its .mbti — so @locale.LocalePreferences::resolve is still reported as an undefined function in proton 0.3.4. Answering them would mean growing the analyzer into a source indexer, which is a different project. Use the view to see the lens, not to gate a build on it.(This note said 116 → 2 until 0.4.32, and the "2" was the census of six corpora — moonbit_core was not in it, and it carries more findings than all six put together. The number was true of the corpora that were measured and false of the ones that were not. The history is in CHANGELOG.md.)
| suffix | static analysis here |
|---|---|
| .mbt | three-inspection program analysis |
| .mbtx | three-inspection program analysis + import-block audit |
| .mbti | interface audit: malformed lines / duplicate signatures / unknown type references. The line grammar matches what moon info actually emits, so a generated file raises no malformed line. The unknown-type check consults the module-wide interface table and measures 0 on moonbitlang/core. A residual finding can still name a type declared outside the scan — treat it as unverified, not as a defect. |
| .mbt.md | literate: only fences the toolchain actually compiles — mbt check / mbt test and the moonbit spellings. A bare mbt, a bare moonbit and nocheck are display blocks and are skipped. |
| .mbtp | logic-side proof lint (string constants in bodies, banned !/↔ forms, cross-package calls, lemma without proof_ensure) — not a replacement for moon prove |
| moon.mod / moon.pkg / workspace | recorded only, no static analysis — configuration is not code |
src/core Span/Severity/Lens/Family(merge_group contract)/Frame + Family::advice
src/report Report struct and rendering (union lens tags, vst frames)
src/sa scan entry + aggregate() + mermaid_states() (public, library)
src/lexer MoonBit lexer frontend
src/parser MoonBit syntax → ast
src/ast MoonBit abstract syntax
src/walk structural inspection: binding tables + structural findings
src/types type inspection: Ty/Ty?/Sig inference
src/interp behavior inspection: AbsVal lattice + dispatch + virtual stacks
src/pipeline assembly: run the three inspections + merge_group merge
src/moonfiles the other file kinds: .mbt.md per-block three-inspection / .mbti
interface audit / .mbtp proof lint
src/statecheck use 2: generic machine-table audit (plain-data MachineSpec bridge)
src/samples demo samples (embedded MoonBit source)
src/cli executable entry (program demo + file-kind demo + sample machine audit)
src/jsoncli the DeepSeek Harness JSON bridgemoon check --target all --deny-warn # 0 errors, 0 warnings (js / native / wasm / wasm-gc)
moon test --deny-warn # 1163/1163 on wasm; 1083/1083 on js and wasm-gc
moon run src/cli # program demo + file-kind demo + sample machine auditpowershell -NoProfile -ExecutionPolicy Bypass -File _verify\release.ps1The command does not run green today, and the reason is measured. moon prove src/core --why3-config .why3.conf fails with unbound type symbol 'map'. src/core grew IfaceSymTab on 2026-10-08 — a struct whose fields are Map[String, Bool] — and the toolchain's proof prelude has no Map theory, so the generated Why3 file declares map int bool and neither moon prove nor why3 can load it. The 19 lemmas never touch IfaceSymTab: it is the package that is proved, and the package carries the type. The fix is to move IfaceSymTab out of the proof subject, and that changes this package's public API (@core.IfaceSymTab is exported), so it is a version-line decision rather than a patch.Two #proof_pure annotations were also false: on path_tail and unqualified, whose bodies build a StringBuilder, which the prover rejects (error 4207). One false annotation stops the whole file from proving, and both of those arrived on 2026-10-09. They are removed, and _verify/check_prove_contracts.ps1 now fails the release if any #proof_pure body is one the prover rejects.
powershell -NoProfile -ExecutionPolicy Bypass -File _verify\release.ps1Install
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