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.30// 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 # 683/683 on wasm; 611/611 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.
| 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 reports 7 actionable on moui 0.1.12, where the module view reports 0. Those 7 are not 7 defects — they are an inventory of what is still wrong with the behavioral lens. Six classes of lens defect have been fixed since the view was added (a list value against a list parameter, a call that omitted an omittable parameter, a function whose body is an if, a string concatenation, a string comparison, and a name used from a sibling file of the same package, which the single-file lens could not see). What remains is four smaller classes: a name bound by an is-pattern, a None receiver, a cross-file constructor's arity, and unreachable code. Use it to see the lens, not to gate a build.(This note said 58 and "what remains is one class" until 0.4.30 fixed the sibling-file class: 51 of the 58 were that one class, and the seven that remain are the four above plus the unreachable-code finding.)
| 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 |
| 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