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.32// Use 1: revise a MoonBit (current-subset) program
let result : @pipeline.PipelineResult = @pipeline.run(source, "main.mbt")
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))| 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 (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 |
moon run src/cli # demo: 6 samples × 3 inspections=== 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 站
已知设计(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 条**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 by state_span() (the same name always yields the same coordinate), not positions in any source file — don't go looking for that line in pyroduct.
let root = /* the resolved source 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 # 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 |
moon 0.1.20260920 (914d7da 2026-09-20)Corrected in this round. The previous version of this section claimed the CLI could not stamp a time at all, because "the only clock is behind an internal package". That was wrong: moonbitlang/async re-exports it publicly as pub using @event_loop {now}, and provenance_for had been calling @async.now() since 0.4.5. The CLI was stamping unknown only because it had been given a hand-rolled empty stamp instead of calling the function that already worked.
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 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)moon 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 + pyroduct-shaped sample machine auditCI runs --target js only. The "four targets" line above is a local result; CI does not cover native or wasm.
powershell -NoProfile -ExecutionPolicy Bypass -File _verify\release.ps1| suffix | static analysis here |
|---|---|
| .mbt | three-inspection program analysis |
| .mbtx | three-inspection program analysis + import-block audit (entry grammar "path" [@alias] [*]; duplicate paths report FParse; the import list is echoed in the report) |
| .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 used to raise false positives on generated files — it had no cross-file awareness (measured: 93 on moonbitlang/core, a tree moon check --target all accepts with 0 errors); it consults the module-wide interface table now, and measures 0 on that same tree. 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 second word of check or test is what makes a block live code. Line numbers align to the .md file's real lines. A bare mbt, a bare moonbit and nocheck are display blocks and are skipped — measured against the toolchain, see EXTENSIONS.md |
| .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 — upstream puts both in the parser's separate moon_config subpackage, alongside syntax and mbti_parser rather than inside them; see the config-boundary section of EXTENSIONS.md) |
moon prove src/core --why3-config .why3.conf # emits 19 VCs for cvc5/alt-ergo| Channel | Address |
|---|---|
| Gitee (primary) | https://gitee.com/ren-yongxiang/moonbit_static_analysis.git |
| GitHub (mirror) | https://github.com/riantr/moonbit_static_analysis |
| mooncakes.io (package registry) | 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)This view is not yet sound, and the flag is opt-in for that reason. The per-function view 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 (all 33 of them in moonbit_core). 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. Under --target js five of them would report nothing at all, because MoonBit's per-file targets options do not compile them for that target; a file no target compiles is a file the checker never saw.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 (facade_locale.mbt:70, core/app_command_host.mbt:114). 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.(This note said 116 and "what remains is one class" until 0.4.30 fixed the sibling-file class, and 7 and "four smaller classes" after that, and then 2 until 0.4.32 measured the census over seven corpora instead of six. The census that produced the two covered six: moonbit_core was not in it, and it carries more findings than all six put together. The 0.4.31 number was true of the corpora that were measured and false of the ones that were not — and a published number that is not gated is a number that can go missing silently, so the census is now tracked, pinned, and fails on a finding it cannot classify.)
| target | files | parsed | actionable (before → after) |
|---|---|---|---|
| riantr/moonbit_doubleML@latest → 0.113.0 | 180 | 73 (40.5%) | 0 → 123 |
| this repository (.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 |
Where this stands against moon check. On .mbt this tool does NOT compete with the toolchain's compiler: moon check has a real parser and ~90 built-in warnings at 100% coverage — measured 2.3 s for 128 diagnostics, each with file:line:col, the source line and a fix. This tool's .mbt frontend reads a subset, and on moonbitlang/core it parses 173 of 882 .mbt files (19.6%); .mbti and .mbt.md are 100%. The single largest reason used to be that method calls could not be represented in the AST at all — ECall takes a String, so m.set(k, v) had nowhere to go and the parse stopped there. That is fixed (a dedicated EMethodCall node, wired into all three lenses); the blocker went from 34 files to 0. That is the honest headline number, and it is why PARSED is printed first. What this tool can do that the compiler cannot is the three file kinds it does not audit at all — .mbti, .mbt.md, .mbtp — plus cross-package semantics.An earlier version of this tool reported 529 "unused locals" on that ML/CI source where the compiler reports 8. Those were not a different opinion: they were struct bodies it had never parsed, and it reported the unread fields as unread bindings. It no longer can — a function the frontend truncated is now excluded from the unused-local / unused-param / param-changed audit outright, because "never read" is a claim about a whole function and a half-read function cannot support one. That fix alone took moonbitlang/core from 270 actionable findings to 147, and this repository from 11 to 0.Reading further, the same principle turned out to apply to the module frame too, and to a class of names the resolver had never been taught: a file the frontend did not finish has a provably incomplete module frame, so "nothing visible binds this name" is not evidence of anything there; and a Type::member path — @debug.Repr::opaque_(v) — is a member reference, not a binding, exactly as a field name and a method name already were. With extern "c" fn modelled as a declaration (it was skipped, which lost the name, so every call site of an extern function read as unbound) and the numeric-literal / enum-constructor gaps closed, moonbitlang/core goes from 270 to 49 actionable with undefined name down from 64 to 17 (published as 0 at the time; that release's own scan contradicts it, since corrected to the measured value), and this repository stays at 0. 0.4.7 then wires the .mbti audit's unknown-type check to the module-wide interface table, clearing the remaining 13 false reports: 36.
moonbitlang/core files parsed actionable undefined names 0.4.5 1029 305 270 64 0.4.6 1029 320 49 17 0.4.7 1029 320 36 17
| files | parsed | actionable | undefined names | |
|---|---|---|---|---|
| 0.4.6 | 1029 | 320 | 49 | 17* |
| 0.4.7 | 1029 | 320 | 36 | 17 |
moonx riantr/moonbit_static_analysis@latest riantr/pyroduct@latest| fix | what it was | findings |
|---|---|---|
| Repr added to the ambient list | a constructor of the Repr enum in core/debug, in implicit scope — and in no core/builtin interface, which is where every other ambient name came from | 16 |
| import alias inside \{...} | @ is not an identifier character, so \{@sm.all_states()} recorded the alias WITHOUT the @ and the walk's !has_prefix("@") guard could not see it. A regression 0.4.8 introduced | 6 |
| extend is a declaration | read as a statement, extend became a name | 8 |
| C-style three-clause for | MoonBit's other for form; reading its head as a binder demanded in | coverage |
$|{"filename":\{sb => self.filename.escape_to(sb)},...} pre-0496 (be21ecd) 0496
files 1029 1029 UNCHANGED
parsed 935 957 +22
actionable 64 56 -8 test suite 1163 passed, 0 failed (1136 before 0496; +27)
per-function census 66 -> 58 over SEVEN corpora (core 64 -> 56; proton 2 -> 2)
the 58 12 families, and not one is a defect in the analysed code
the files that carry them 24, and moon check READS 24 of 24
the census TRACKED, PINNED, and it fails on an UNCLASSIFIED finding
module-view corpus pins moonbit_core 33 (37 before 0496); the other six BYTE-IDENTICAL
mutation gates 79 in the release gate (78 before 0496)
0496's own gate 14 mutations: 13 KILLED, 1 declared survivor with its proof
gates re-anchored 6 (0459 M4, 0480 M1-M3, 0488 M5-M6)| family | n | what the lens claimed | the fix |
|---|---|---|---|
| A | 61 | a destructured name is bound to the whole tuple | it is bound to an element — the type lens already answered "Unknown" |
| B | 25 | a continue/break that carries a value is unreachable code | the carried values are the terminator's own payload, not sibling statements |
| C | 8 | a statement after a loop whose body always terminates is dead | the loop's "may have broken out" is a separate signal from the joined flow |
| D | 10 | an extern/FFI call, an omittable parameter read as a positional slot, a name declared twice, and a function with no body | four defects wearing one name, each isolated by its own probe |
| E | 5 | a trait's method is not a value | the .mbti reader harvests the trait's methods as Trait::method |
| F | 4 | an integer / is typed as float | div_type, and the constant fold beside it |
| G | 2 | a name bound by an is-pattern as-binding is not bound | the as binding is collected at any depth |
| H | 1 | a comprehension does not bind its pattern | the names are bound in a copy of the environment |
actionable 48 -> 37
notices 9988 -> 9988 UNCHANGED
unreliable 2956 -> 2956 UNCHANGED
withheld 552 -> 552 UNCHANGED
parsed 935 -> 935 UNCHANGED
files 1029 -> 1029 UNCHANGED per-function actionable findings 116 -> 2 (the 2 are the documented #doc(hidden) limit)
^^ CORRECTED IN 0.4.32 -- that census covered SIX corpora.
module-view corpus pins UNCHANGED on six corpora; moonbit_core 48 -> 37
moon test --deny-warn 1136/1136 on wasm; 1056/1056 on js and wasm-gc
moon check --deny-warn exit 0
self-scan (own source) 167/167 parsed, actionable=0
mutation gates 78 gates green; one mutation retired with its reason| the form | the increment |
|---|---|
| a C-style for init clause is a declaration | 0470 |
| an anonymous fn's tail between ) and { | 0471 |
| a for header is ONE grammar, not two | 0473 |
| a loop in expression position went to the wrong reader | 0474 |
| a binder head carrying ONE declaration | 0475 |
| the step after a binder head's initialisation clause | 0476 |
| a constructor pattern on the left of a let | 0477 |
| the <? payload, expr <? { "k": v, .. } | 0478 |
| the UTF-8 BOM is skipped, not refused | 0479 |
| the unbraced escape, '\uXXXX' | 0480 |
| a nested optional type | 0481 |
| a for header separated by line breaks | 0482 |
| a package-qualified holder, @pkg.Type::method | 0483 |
| a trait method whose receiver is a reference | 0484 |
| a braced struct pattern in is position | 0485 |
| a +-joined effect clause | 0486–0487 |
| a <+ with a next-line operand, a $| interpolant | 0488 |
| corpus | 0489 | 0490 | 0491 |
|---|---|---|---|
| moui | 265 | 58 | 7 |
| proton | 136 | 92 | 5 |
| moonbit_image | 94 | 68 | 44 |
| moonbit_labeler | 95 | 48 | 40 |
69:38 - error: undefined name 'as' (UndefinedName) [structural+type]| program | moon check |
|---|---|
| b as &Sink | ACCEPTED |
| Box::{ n: 2 } as &Sink | ACCEPTED |
| Box::{ ..o, n: 9 } as &Sink | ACCEPTED |
| b as (no target) | REJECTED |
| b as Box (not a trait object) | REJECTED |
| corpus | actionable before | after | |
|---|---|---|---|
| moui | 1 | 0 | the as false positive |
| moonbit_labeler | 0 | 0 | byte-identical |
| moonbit_image | 0 | 0 | byte-identical |
| async | 19 | 19 | byte-identical |
| proton | 68 | 68 | byte-identical |
| proton_contract | 2 | 2 | byte-identical |
| # | notice | count |
|---|---|---|
| 1 | test declaration | 429 |
| 2 | match expression | 155 |
| 3 | if expression | 45 |
| 4 | struct declaration | 36 |
| 5 | derive declaration | 31 |
| 6 | enum declaration | 14 |
| 7 | anonymous function literal | 10 |
| 8 | const declaration | 6 |
| 9 | short lambda | 2 |
| 10 | try expression | 1 |
| case | local never read | result |
|---|---|---|
| function contains no unanalysed region | yes | reported unused |
| function contains one match | yes | not reported |
| corpus | .mbt | actionable on | off | delta |
|---|---|---|---|---|
| moui | 551 | 0 | 0 | 0 (byte-identical) |
| moonbit_labeler | 35 | 0 | 0 | 0 |
| moonbit_image | 17 | 0 | 0 | 0 |
| async | 362 | 19 | 18 | 1 |
| proton | 114 | 68 | 66 | 2 |
| proton_contract | 2 | 2 | 2 | 0 |
| all six | 1081 | 89 | 86 | 3 |
derive(Debug, Eq)
struct Box { n : Int }| notice | before | after | delta |
|---|---|---|---|
| derive (old wording) | 575 | 0 | −575 |
| derive (correct wording) | 0 | 547 | +547 |
| struct | 572 | 829 | +257 |
| enum | 155 | 246 | +91 |
| extend | 238 | 308 | +70 |
| total | 5,026 | 5,399 | +373 |
-> (Int) -> String the `->` is AFTER the matching `)`
-> (() -> Int) the `->` is INSIDE the parens\bfn\s+[A-Za-z_]\w*\s*\([^()]*\)\s*->\s*\(pub fn[T] ServiceTask::start(
self : ServiceTask[T],
complete : (ServiceTaskResult[T]) -> Unit,
) -> (() -> Unit)? { ← `-> (`, and an arrow insideInterp::exec_stmt SExpr(e) => { let _ = self.eval(e, env) }
Interp::analyze_fn let ret = join_vals(self.ret_vals)
join_vals if vals.length() == 0 { return AType("None") }fn f() -> Int {
1
}
fn g() -> Int {
f() + 1 // reported: no method matching +(None, int)
}fn f(g : Int?) -> Int {
guard g is Some(v) else {
return 0
}
return v
}| Round | Result | Why |
|---|---|---|
| 1st | 1 killed / 3 survived / 3 no-op | three pattern strings never matched the source at all |
| 2nd | 6 killed / 1 survived (M6) | patterns fixed; M6 turned out to be a fixture-coverage problem |
| 3rd | 8 killed / 0 survived | two fixtures added, plus M8 |
guard config.package_config() is Some(package_config) else {
raise MissingPackageMetadata
}| Corpus | 0.4.23 | 0.4.24 | At finding level |
|---|---|---|---|
| wzzc-dev/moui 0.1.12 | parsed 483 / actionable 1 | parsed 483 / actionable 1 | identical |
| moonbitlang/async 0.22.4 | parsed 216 / actionable 22 | parsed 216 / actionable 19 | −8, 0 new |
| moonbit-community/proton 0.3.4 | parsed 95 / actionable 103 | parsed 95 / actionable 100 | −6, 0 new |
| riantr/moonbit_image 0.3.7 | parsed 2 / actionable 3 | parsed 2 / actionable 0 | −3, 0 new |
| proton_contract 0.3.4 | parsed 4 / actionable 3 | parsed 4 / actionable 3 | identical |
| riantr/moonbit_labeler 0.2.13 | parsed 25 / actionable 0 | parsed 25 / actionable 0 | identical |
fn f(g : Int?) -> Int {
guard g is Some(v) else { return 0 }
return v // reported unreachable — a false positive
}| Corpus | Files | Signature -> T raise | Type position () -> T raise |
|---|---|---|---|
| moonbitlang/async 0.22.4 | 409 | 72 | 10 |
| moonbit-community/proton 0.3.4 | 136 | 70 | 33 |
| riantr/moonbit_image 0.3.7 | 17 | 14 | 0 |
| proton_contract 0.3.4 | 3 | 2 | 0 |
| riantr/moonbit_labeler 0.2.13 | 35 | 2 | 1 |
| wzzc-dev/moui 0.1.12 | 607 | 2 | 0 |
// moonbitlang/async 0.22.4 src/internal/fd_util/pipe.mbt:31
guard! fds is [r, w]
// reported: operand of '!' must be bool, got '[int]'| Corpus | 0.4.22 parsed / actionable | 0.4.23 parsed / actionable | At finding level |
|---|---|---|---|
| wzzc-dev/moui 0.1.12 | parsed 481 / actionable 1 | parsed 483 / actionable 1 | 0 new |
| moonbitlang/async 0.22.4 | parsed 206 / actionable 17 | parsed 216 / actionable 22 | +5 UndefinedName |
| moonbit-community/proton 0.3.4 | parsed 72 / actionable 43 | parsed 95 / actionable 103 | +60 UndefinedName |
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | parsed 2 / actionable 3 | +3 UndefinedName |
| proton_contract 0.3.4 | parsed 3 / actionable 1 | parsed 4 / actionable 3 | +2 UndefinedName |
| riantr/moonbit_labeler 0.2.13 | parsed 24 / actionable 0 | parsed 25 / actionable 0 | 0 new |
// proton facade_error.mbt:6
OperationFailed(action~, status=..., detail=...)
// reported: no visible binding for 'OperationFailed'// declared order, core/pkg.generated.mbt:92
ActionCommand::new(intent~, label?, shortcut?, enabled?, group?, description?)
// call order, core/gesture.mbt:754
ActionCommand::new(intent=..., label=..., group=..., description=..., shortcut=...)| Shape | Result | |
|---|---|---|
| A | Reordered named arguments (moui core/gesture.mbt:754) | actionable 8 → 1 |
| B | ann_to_ty missing an "Int32" arm | removed 87 silent points the formatter had created in its own source |
| Corpus | 0.4.21 | 0.4.22 | At finding level |
|---|---|---|---|
| wzzc-dev/moui 0.1.12 | parsed 481 / actionable 8 | parsed 481 / actionable 1 | 7 false positives gone, 0 new |
| riantr/moonbit_labeler 0.2.13 | parsed 24 / actionable 0 | byte-identical | — |
| moonbitlang/async 0.22.4 | parsed 206 / actionable 17 | byte-identical | — |
| moonbit-community/proton 0.3.4 | parsed 72 / actionable 43 | byte-identical | — |
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | byte-identical | — |
| proton_contract 0.3.4 | parsed 3 / actionable 1 | byte-identical | — |
| Shape | Where | |
|---|---|---|
| A | guard COND else { ... } read as an identifier | 550 sites / 126 files |
| B | a named argument whose value is a lambda ended the argument list | parsed 436 → 481 |
| C | is did not continue across a newline — and the file STILL PARSED | embedded_runtime_backend.mbt:227 |
| D | an is pattern bound only its FIRST name, and only at depth 1 | 7 findings, 13 sites corpus-wide |
guard (self.frame, self.image, self.host_bridge)
is (Some(frame_state), Some(image_state), Some(host_bridge)) &&
!self.disposed else {fn f(x : Int) -> Int {
guard try_os(x) is Some(Os(error)) else { return 0 }
error + 1
}runtime.prepare_bridge_request(request, window) is (Gone | PageUnavailable)| Corpus | 0.4.20 | 0.4.21 | Finding-level |
|---|---|---|---|
| wzzc-dev/moui 0.1.12 | parsed 436 / actionable 10 | parsed 481 / actionable 8 | 45 more files readable |
| riantr/moonbit_labeler 0.2.13 | parsed 23 / actionable 3 | parsed 24 / actionable 0 | 3 × undefined function '@core.X::new' gone |
| moonbitlang/async 0.22.4 | parsed 206 / actionable 29 | parsed 206 / actionable 17 | 12 × undefined name 'guard' gone, 0 new |
| moonbit-community/proton 0.3.4 | parsed 71 / actionable 44 | parsed 72 / actionable 43 | 2 guard gone, 1 new (see below) |
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | byte-identical | — |
| proton_contract 0.3.4 | parsed 3 / actionable 1 | byte-identical | — |
| Corpus | 0.4.19 | 0.4.20 |
|---|---|---|
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | byte-identical on all six metrics |
| riantr/moonbit_labeler 0.2.13 | parsed 23 / actionable 3 | parsed 23 / actionable 3 |
| moonbitlang/async 0.22.4 | parsed 196 / actionable 30 | parsed 206 / actionable 29 |
| wzzc-dev/moui 0.1.12 | parsed 432 / actionable 14 | parsed 436 / actionable 10 |
| Class | Count | Corpus size | Root cause |
|---|---|---|---|
| Named arguments compared positionally | 5 | ActionCommand::new(...) ×5 | the call OMITS defaulted parameters and passes the rest by label; the lens drops labels and compares by position, so description (String) was checked against the 4th parameter enabled (Bool) |
| self in impl ... with fn name(self) | 4 | 1047 sites / 204 files | skip_decl_braced's sentinel gives up on the impl body, so the parameters are never bound |
| expr as Type cast | 1 | 29 sites / 21 files | as is unhandled and read as a bare name |
| Corpus | 0.4.18 | 0.4.19 |
|---|---|---|
| riantr/moonbit_labeler 0.2.13 | parsed 22 / actionable 3 | parsed 23 / actionable 3 |
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | byte-identical on all six metrics |
| wzzc-dev/moui 0.1.12 | parsed 389 / actionable 4 | parsed 432 / actionable 14 |
Error [4100] x : &Node
╰── The type Node is not a trait
Error [4091] x.n
╰── @moonbitlang/core/ref.Ref[Node] has no field n| Fix | Shape | Corpus volume |
|---|---|---|
| &T | Ref[T] annotation | 31 sites / 13 files |
| ~ | label~ : T / x~ | 594 files |
| fn[T] | type parameters | 1563 sites / 266 files |
| b"..." | byte-string literal | 878 sites / 134 files |
| { ..base } | struct update | 370 sites / 88 files |
| .m(...) | leading-dot continuation | 1124 lines / 131 files |
| Corpus | 0.4.17 | 0.4.18 |
|---|---|---|
| riantr/moonbit_labeler 0.2.13 | parsed 18 / actionable 3 | parsed 22 / actionable 3 |
| riantr/moonbit_image 0.3.7 | parsed 1 / actionable 0 | parsed 1 / actionable 0 (byte-identical) |
| moonbit-community/proton 0.3.4 | — | parsed 70 / actionable 45 |
| moonbit-community/proton_contract 0.3.4 | — | parsed 3 / actionable 1 |
| moonbitlang/async 0.22.4 | — | parsed 196 / actionable 30 |
| wzzc-dev/moui 0.1.12 | parsed 193 / actionable 588 | parsed 389 / actionable 6 |
| form | moon | |
|---|---|---|
| ('0' : Int) | MOON-OK | the only legal form, the one implemented |
| let y = 1 : Int | MOON-BAD | no parentheses: rejected |
| (1 : Int, 2) | MOON-BAD | ascription on a tuple element: rejected |
| (s : Int) where s : String | MOON-BAD | so: an assertion, not a coercion |
| corpus | new finding | recorded limitation |
|---|---|---|
| moui | macos_native_ime.mbt:437 parameter 'prefix' may not be used | prefix IS used, at line 442, inside a catch { } body; the catch arm consumes its block with skip_braces, so a read that happens only there is invisible (KNOWN LIMIT, pinned at parser_0432_test.mbt:155) |
| proton | app_control.mbt:5 undefined name 'noraise' | parse_raise_effect handles ! and raise E but not noraise |
| proton | updater/hex.mbt:22,45 no visible binding for 'MalformedSignature' ×2 | a suberror variant used in a different file — the per-file enum_ctors limitation |
| Probe | Result |
|---|---|
| top-level call present, the real line | reports Byte::default |
| top-level call removed | completely silent |
| Array::make called alone | reported |
| Byte::default called alone | reported |
| outer also missing + inner Byte::default | only the inner is reported |
| Spelling | Result |
|---|---|
| Array::make(4, 0) | silent (the name is in the table) |
| Array::mkae(4, 0) | reported (the name is not) |
fn every_mode() -> Array[Mode] {
[Select, Rect, Polygon, Keypoint, EditBinding]
}| Corpus | 0.4.16 | 0.4.17 |
|---|---|---|
| riantr/snn_mbt 0.155.0 | 6 | 6 (all true positives, untouched) |
| 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 |
| Total | 11 | 9 |
| Site | Finding | Evidence |
|---|---|---|
| neuron_identity.mbt | parameter dt is never read | read the source; dt appears nowhere in the body |
| dsac.mbt ×2 + 3 × examples/*/main.mbt | for a in 0..<n binder a/s never read | same; probed that moon check warns on an unread for-binder too |
| Cause | Count | Site |
|---|---|---|
| a cross-package const typed as a FUNCTION | 4 | IfaceSymTab.values's Bool was written true and read by nobody |
| a braced suberror body parsed as expressions | 4 | always routed to the "skip to end of line" path |
| Byte::default missing from the builtin list | 1 | — |
// snn_mbt/istdp.mbt
r: 3.0F * hz, // hz is a `const hz : Float` from @snn_kwQualified names were once OVER-reported (Layer::ReLU, fn hz); fixing that left them UNDER-reported.
| Corpus | 0.4.13 | 0.4.14 | 0.4.15 | 0.4.16 |
|---|---|---|---|---|
| snn_mbt | 33 | 19 | 9 | 6 (all true) |
| moonbit_doubleML | 28 | 3 | 1 | 0 |
| moonbit_labeler | 6 | 5 | 5 | 5 (2 with --extern-iface-dir) |
| moonbit_image | 4 | 4 | 4 | 0 |
| pyroduct | 0 | 0 | 0 | 0 |
| Total | 71 | 31 | 19 | 11 |
| Cause | Count | Evidence |
|---|---|---|
| an extern "C" parameter reported unused | 6 | probe: extern is silent, a normal fn beside it warns |
| Bool == Bool reported as unsupported operands | 2 | probe: 0 errors, 0 warnings |
| a UNIT (payload-less) enum variant unread | 4 | probe: 0 errors; the payload variant beside it already worked |
enum Layer {
Conv2d(Conv2dParam) // resolves
ReLU // reported undefined
}| Source | moon check |
|---|---|
| extern "C" fn expf(x : Float) -> Float = "expf" | silent |
| fn p7(z : Float) -> Float { } | warns |
| fn p6(y : Float) -> Float { 1.0 } | warns |
| Corpus | 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 |
| Total | 71 | 31 | 19 |
| Cause | Count | Evidence |
|---|---|---|
| F-suffixed float literals 0.0F unmodelled → undefined name 'F' | 28 | valid per moon run; 7F is the syntax error |
| / typed as float division → cannot assign 'float' into '[int]' | 14 | 7 / 2 is 3, not 3.5 |
| exponent notation 1.0e-12 unlexed → undefined name 'e' | 14 | valid per moon run |
| suberror bodies re-scanned → undefined variable 'String' | 4 | the same line already reports ParseError |
1.5 -> 1 | 0.25 -> 0 | 3.14159 -> 3| Corpus | Files | Parsed | 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 (no regression) | 119 | 103 → 104 | 0 |
| Total actionable | 71 → 31 |
| Cause | Count | Where |
|---|---|---|
| color[u]: a Map treated as a list, reported as an index type error | 2 | src/paths.mbt 135/152 |
| self read only inside an interpolation, reported as an unused parameter | 1 | src/slot.mbt 127 |
| _-prefixed unused binding/parameter — the analyzer's rule was the opposite of the compiler's | 2 | src/algebra.mbt 118, audit/mbti.mbt 69 |
| real errors caught | false positives | |
|---|---|---|
| 0.4.12 | 3 / 3 | 1 (the 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) { // line 226 — an if-EXPRESSION
...
if is_coordinate(target) { // line 411 — an if-STATEMENTmoonx 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| round | parsed | actionable | what it fixed |
|---|---|---|---|
| start (0.4.7) | 46/46 | 104 | — |
| after round 2 | 46/46 | 8 | substitution skips, walk_target write-through, stale .mbti |
| after round 3 | 46/46 | 0 | string-interpolation reads were invisible |
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