Trace-driven three-proof-strength control-flow, taint, value-range, slicing and contract analysis for MoonBit modules, as one Markdown report with Mermaid diagrams

| You call | You get back | Shape |
|---|---|---|
| LunaFlow analyze <coord> | report, trace, summary | .md, _trace.jsonl, _summary.json |
| LunaFlow analyze --funnel | the same three artefacts plus L2 alarms and L3 VCs | report §3 / §5.2 / §6.3 / §8 |
| LunaFlow analyze --level 2 | L1 baseline + abstract may-analysis | report §5.2, §6.3 |
| LunaFlow analyze --level 3 | L1 + L2 + contract VCs | report §8 |
| LunaFlow analyze --slice "<crit>" | dynamic backward/forward slice | Mermaid graph + statement table |
| LunaFlow analyze --coverage cov.xml | compiled-suite coverage mapped onto CFG nodes | report §4.4 |
| LunaFlow analyze --list-functions | function, file, statement count | FUNCTION<TAB>FILE<TAB>STATEMENTS |
| LunaFlow analyze --max-cfg-functions <n> | how many functions the CFG pass covers (default 600) | report §2 states what was not covered |
| LunaFlow analyze --families <codes> | only the finding families named, by code | the same report, filtered |
| LunaFlow analyze --exclude-families <codes> | everything except the families named | the same report, filtered |
| LunaFlow analyze --exclude <name> | skip a directory name while walking (repeatable) | the same report |
| LunaFlow analyze --metrics | the ADEQUACY block: read, understood, entered, found | four METRICS lines |
| LunaFlow analyze --per-function | every function entered as an entry point, the --max-functions cap lifted | the same report, more of the module |
| LunaFlow analyze --stability | re-sample the analysis and report which findings depend on the sample | report §5.6 |
| LunaFlow analyze --level 2 + the EVA | the memory-safety and undefined-behaviour classes | report §5.3 |
| LunaFlow --explain <code> | one family's contract — property, assumption, exclusions | three clauses, no scan |
| LunaFlow --explain-all | the roster of every family and the property it asserts | one block, no scan |
| @app.load_module(root, coord, version, limit) | the parsed module and its ingestion statistics | (ModuleAst, ScanStats) |
| @app.check_contracts(ast, ["f"]) | the verification conditions of a function | ProofResult |
| @app.run_abstract(ast, max_functions, max_steps) | the L2 findings of a module | AbsResult |
moon add riantr/LunaFlow@0.1.14import {
"riantr/LunaFlow@0.1.14",
}///|
import {
"riantr/LunaFlow/src/app",
}Cannot find import 'riantr/LunaFlow/src/app@0.1.0' in my/app/src/thing@0.1.0///|
import {
"riantr/LunaFlow/src/app" @lf,
}moon run src/cli -- analyze <author>/<module>[@<version>|@latest]moonx riantr/LunaFlow@latest riantr/LunaFlow@latestmoonx riantr/LunaFlow@latest riantr/LunaFlow@latest # the bare coordinate IS the target
moon run src/cli -- riantr/LunaFlow@latest # the same thing, locally
moonx riantr/LunaFlow@latest . # a path IS the target too
moonx riantr/LunaFlow@latest # no target at all: the current directory| Target | What it is | What happens |
|---|---|---|
| author/module[@version] | a coordinate | it is fetched through moon fetch and analysed |
| ./path, /abs/path, C:\path | a path | it is analysed where it stands |
| nothing | the default | the current directory is analysed |
moon run . --target wasm -- analyze riantr/moonbit_static_analysis@latest --out reportsError: [4021] Value run_async_main not found in package `moonbitlang/core/async`.| Import module | Symbols | Served by |
|---|---|---|
| moonbitlang/async | 95 | the toolchain's bundled core |
| __moonbit_fs_unstable | 12 | the toolchain's bundled core |
| wasi_snapshot_preview1 | 1 | the host runtime's WASI |
Error: [4014]
╭─[ src/app/scan.mbt:284:12 ]
284 │ stdout=@process.read_from_process(),
│ ──────────────┬─────────────
│ ╰─────────────── Expr Type Mismatch
has type : (ReadFromProcess, SharedProcessOutput)
wanted : ProcessOutput| Declaration | core 0.10.19 | core 0.10.20 |
|---|---|---|
| read_from_process() | -> &ProcessOutput | -> (ReadFromProcess, SharedProcessOutput) raise |
| run's stdout~ | &ProcessOutput = @stdio.stdout | ProcessOutput = Inherit |
LunaFlow analyze riantr/LunaFlow@latest@latest --out reports/ --funnelError: Invalid package coordinate `riantr/LunaFlow@latest@latest`
Caused by:
must contain a single version marker| 0.1.13 printed | 0.1.14 prints | |
|---|---|---|
| §10 Reproduction | riantr/LunaFlow@latest@latest | riantr/LunaFlow@latest |
| Report title | riantr/LunaFlow@latest@0.1.13 | riantr/LunaFlow@0.1.14 |
| The question | What the toolchain says |
|---|---|
| What is the C form? | extern "C" fn name(args) -> Ret = "c_symbol" — and the = "c_symbol" part is required (E4140/E3002 without it). |
| Which targets accept it? | native only. The wasm backend rejects extern "C" (E4156); native rejects a module import (fn f() = "pkg" "path") with the same code. The two forms are not interchangeable. |
| What is #external type T? | An opaque type. Natively it is a GC-managed external object — the toolchain's own moonbit.h builds one with moonbit_make_external_object(finalize, payload_size) — so the payload belongs to the C side. |
| How does a String cross? | As uint16_t * (moonbit_string_t): the C side receives the address of a MoonBit heap object. A Bytes crosses as uint8_t *. |
| How do the scalars cross? | By value. Int arrives as a 32-bit signed integer, Int64 as a 64-bit one, Double as a C double in a floating-point register — read through an int64_t prototype it arrives as garbage, which is exactly what a wrong C prototype does to a value. |
| Is there an 8-bit integer? | No. Int8/UInt8 do not exist; Int16/UInt16 do. |
| Who owns a pointer parameter? | The toolchain refuses one without #borrow(x)/#owned(x) (E0055) and says the current default — #owned — "will be changed in the future". |
| Does the compiler enforce it? | No. Using an #owned argument after the call compiles clean. |
| Does the compiler catch two claims about one C symbol? | No. Two declarations of the same symbol with different signatures compile clean — and the C function has ONE prototype. |
| Declared type | Space | Why |
|---|---|---|
| Int, Int32, Int64, UInt, Double, Bool, Char | value | copied by value; the C side receives the bits |
| String, Bytes, Array, Option, Result, Map, a struct | heap pointer | the C side receives the address of a MoonBit object — and this is the case that needs an ownership annotation |
| #external type T | foreign address | the payload belongs to the C side |
| a plain type T | heap pointer | opaque, but a MoonBit value |
| a fieldless enum | value | no heap representation |
| Code | What it asserts |
|---|---|
| FFI-SIGNATURE-CONFLICT | one C symbol is declared with two different MoonBit signatures — so one of the two claims is wrong. The toolchain does not notice this. |
| FFI-USE-AFTER-MOVE | an object given to C as #owned is used afterwards, although the C side owns it from the call on. The toolchain does not enforce this either. |
| FFI-UNANNOTATED-OWNERSHIP | a pointer parameter relies on the toolchain's current default, which the toolchain says will change. |
| FFI-TARGET-MISMATCH | the module declares a foreign function its published target cannot build — and a #cfg(target = "native") guard is respected, so a guarded declaration is not a false positive. |
| FFI-ADDRESS-UNKNOWN | a slot the module fills with a constant at one call site and computes at another: if it carries an address, the region cannot be named. |
| FFI-ADDRESS-CONFLICT | the same constant reaches two different C symbols: if it is an address, at most one of them can own it. |
| Constant | C symbols | Along with | Sites |
|---|---|---|---|
| 0x40000000 | map_page, poke | 0x1000, 0x7 | map_page@lib.mbt:47, poke@lib.mbt:60 |
| The question | What the toolchain says |
|---|---|
| Is there a pointer type? | No. No unsafe_ptr, no Pointer, no address arithmetic anywhere in the core. |
| Is there an address space? | No. No MMIO, no physical address, no mmap. |
| Is the word size the target's? | No. Int is 32 bits on native and on wasm: it wraps after 2^30 on both. The toolchain's own Why3 prelude pins the same range. |
| Is the byte order the machine's? | No. Only _be and _le variants exist — the program chooses the order. |
| Is there layout introspection? | No. No size_of, no align_of, no reflection. |
| Is there an ARM or RISC-V target? | No. The targets are wasm, wasm-gc, js, native, llvm. |
| What is there? | A GC, immutable values, and one route to a raw address: extern "C" with #external type, which the wasm backend rejects. |
let derived : Int32 = base + 2 // reads a global declared LATER
let base : Int32 = 10| The global | What MoonBit computes | What the analyzer says |
|---|---|---|
| let overflowing : Int32 = 2147483647 + 1 | -2147483648 | INTEGER-OVERFLOW, at the global's line |
| Class | Meaning |
|---|---|
| GLOBAL | bound to a top-level name: it lives in the module's global region. |
| FRAME | created in a function and never leaves it: a frame allocation would be sound for it. |
| HEAP | escapes — returned, stored into another object, or handed to a call the analyzer cannot see into. |
| FOREIGN | came from outside the analysed module: its region is not the analyzer's to name. |
| Finding | Engine | Rows | In every run | Depends on the sample? |
|---|---|---|---|---|
| TAINT-INDEX | L2-abstract | 1 | 1 | no |
| UNREACHABLE | L2-abstract | 18 | 18 | no |
| TAINT-INDEX-RANGE | dynamic+symbolic | 2 | 0 | yes |
| before | after | |
|---|---|---|
| classes listed in report §5.3 | 5 | 6 |
| EVA findings on the fixture | 9 | 11 |
| INDEX-OOB rows in §5.2 (classic) | 2 | 0 |
| The class | What MoonBit does — MEASURED | The family |
|---|---|---|
| buffer overflow | Buffer GROWS, so there is no fixed capacity to overflow; a fixed-size array's bounds are checked | INDEX-OOB |
| out-of-bounds read/write | a[5] on a three-element array is RuntimeError: unreachable at Array::at; so is a[-1] | INDEX-OOB |
| integer overflow | integer arithmetic WRAPS silently — Int::MAX_VALUE + 1 is Int::MIN_VALUE, with no panic and no diagnostic | INTEGER-OVERFLOW |
| division by zero | 100 / 0 is RuntimeError: divide by zero, and 100 % 0 is RuntimeError: remainder by zero | DIVISION-BY-ZERO |
| null pointer dereference | there are no null pointers; None.unwrap() is RuntimeError: unreachable at Option::unwrap | NONE-DEREF |
| uninitialized memory read | UninitializedArray::make(3) read before set returns an indeterminate value and does NOT panic | UNINITIALIZED-READ |
| use-after-free | no raw pointers, the collector owns the storage, values are immutable | RESERVED |
| illegal pointer arithmetic | no pointer type in safe MoonBit | RESERVED |
| memory leak | the collector reclaims unreachable storage; there is no manual free | RESERVED |
| format string | there are no format strings: the language interpolates with a typed syntax and the core has no printf-style formatting | RESERVED |
| invalid type cast | as is a trait-object downcast and nothing else — a numeric as is a compile error; Double::to_int saturates silently | INVALID-CAST |
USE-AFTER-FREE UseAfterFree
property a value may be used after the storage it referred to was released
assumption NOT APPLICABLE IN MOONBIT: safe MoonBit has no raw pointers, the
collector owns the storage, and values are immutable -- there is
nothing to free and nothing to dangle
excluded nothing -- and note the kind is RESERVED: safe MoonBit cannot
express the defect| The fixture's function | What the real runtime did | What the analyzer said |
|---|---|---|
| overflow_literal | returned -2147483648 | INTEGER-OVERFLOW, confidence 0.95 |
| divide_by_zero | RuntimeError: divide by zero | DIVISION-BY-ZERO, confidence 0.95 |
| index_past_end | RuntimeError: unreachable at Array::at | INDEX-OOB, confidence 0.95 |
| unwrap_none | RuntimeError: unreachable at Option::unwrap | NONE-DEREF |
| read_uninitialized | returned 13036 — the garbage in the slot, silently | UNINITIALIZED-READ |
| saturating_cast | returned 2147483647 | INVALID-CAST |
| safe_sum, safe_divisor, safe_index, safe_some, safe_some_on_both_branches, safe_initialized, safe_cast | all ran and returned the right answer | nothing reported |
let mut x : Int32? = None
if flag { x = Some(1) } else { x = Some(2) }
x.unwrap()| before | after | |
|---|---|---|
| statements understood | 3,283 | 3,341 |
| statements unsupported | 183 | 171 |
| frontend coverage | 94.7 % | 95.1 % |
| trace events | 15,656 | 11,865 |
moonx riantr/LunaFlow@latest --explain-allfamilies: code, title, and the property each one asserts
DEAD-CODE DeadCode -- a statement is unreachable after an unconditional exit
UNREACHABLE Unreachable -- no execution reaches this statement: the flow arriving at it has already left the block
INFINITE-LOOP InfiniteLoop -- the loop cannot terminate: its condition holds for every input and it has no break
TAINT-INDEX TaintedIndex -- a tainted value may reach an array index or a buffer offset
TAINT-INDEX-RANGE TaintedIndexInRange -- a tainted value reaches an index that is in range on every observed path
TAINT-ALLOC TaintedAllocation -- a tainted value may control an allocation size
RANGE-VIOLATION RangeViolation -- a value's range may be violated: an assertion may fail, or a size may be negative
PARSE-GAP ParseGap -- a construct the frontend could not model, folded into the statement it belongs to
BUDGET-EXCEEDED BudgetExceeded -- an analysis budget was exhausted, so the rest of the function was not analysed
UNSUPPORTED UnsupportedSink -- a tainted value may reach abort/panicmoonx riantr/LunaFlow@latest --explain TAINT-INDEXTAINT-INDEX TaintedIndex
property a tainted value may reach an array index or a buffer offset
assumption the taint source set was complete, and the sink was reached on an observed path
excluded an index the runtime clamps, and a value the source model did not mark taintedWARNING unknown family code in --exclude-families: TAINT-* (the roster is DEAD-CODE, UNREACHABLE, ...)ERROR the profile includes and excludes the same family code: DEAD-CODEMETRICS read files=1 parsed=1 partial=0 understood=100.0%
METRICS stmts statements=32 understood=32 unsupported=0 understood-share=100.0%
METRICS funcs functions=12 entered=12 skipped=0 entered-share=100.0%
METRICS findings total=12 error=6 warning=2 info=4| entered (of 428 extracted) | trace events | wall | |
|---|---|---|---|
| capped (default 150) | 150 (35.0 %) | 13,195 | 0.15 s |
| --per-function | 353 (82.4 %) | 74,645 | 0.23 s |
| Target | What it is | What happens |
|---|---|---|
| author/module[@version] | a coordinate | fetched through moon fetch and analysed |
| ./path, /abs/path, C:\path | a path | analysed where it stands |
| nothing | the default | the current directory is analysed |
moonx riantr/LunaFlow@latest . # the current directory
moonx riantr/LunaFlow@latest ./src/app # a tree, where it stands
moonx riantr/LunaFlow@latest riantr/LunaFlow@latest # a coordinate, fetched
moonx riantr/LunaFlow@latest # no target: the current directory| native | wasm | js | wasm-gc | |
|---|---|---|---|---|
| moon check (whole module) | 0 errors | 0 errors | 0 errors | 1 error |
| moon check src/app | 0 errors | 0 errors | 0 errors | 0 errors |
| moon test src/app | 73/73 | 73/73 | 73/73 | 73/73 |
Error: [4021] Value run_async_main not found in package `moonbitlang/core/async`.| Package | Kind | Native | Wasm | Wasm-GC | JS |
|---|---|---|---|---|---|
| riantr/LunaFlow (root) | executable | yes | yes | — | yes |
| riantr/LunaFlow/src/app | library | yes | yes | yes | yes |
| riantr/LunaFlow/src/command | library | yes | yes | yes | yes |
| riantr/LunaFlow/src/cli | executable | yes | yes | — | yes |
LunaFlow.png the mark this README shows
LunaFlow_Logo.png the same artwork at 1024x1024
LICENSE MIT
moon.mod module metadata (native, wasm and js)
moon.pkg the ROOT package: the executable moonx resolves
main.mbt the root entrypoint, a forwarder to src/command
src/app/ 18 library files: parser, CFG, L1 interpreter,
L2 abstract interpreter, L3 contracts, funnel,
taint/slice, family roster, evolved value
analysis, memory layout model, foreign address
space model, report renderer
src/app/*_test.mbt 134 tests in 14 files
src/command/ the CLI implementation, as a library
src/cli/main.mbt the published CLI entrypoint
fixtures/vuln_demo/ taint / dead-code / infinite-loop / contract fixture
fixtures/eva_demo/ the evolved value analysis fixture: the defect
classes and the ordinary shapes next to them
fixtures/memory_demo/ the memory layout fixture: the global region, the
allocation classes and the tail value
fixtures/ffi_demo/ the foreign address space fixture: the C boundary,
the ownership contract and the named addresses
features/ acceptance scenarios in Gherkin
docs/design.md design, comparison, acceptance criteria (A1-A18)
docs/qa/REPORT.md deep QA report (security, coverage, metrics)
docs/host_requirements.txt the published wasm's host contract, measured
reports/ generated reports for the validation modules
scripts/verify.mjs dependency-free acceptance checker
scripts/qa/ dependency-free QA harnesses| Command | Result |
|---|---|
| moon fmt --check | exit 0 |
| moon check --target native | exit 0, 150 warnings, 0 errors |
| moon check --target wasm | exit 0, 150 warnings, 0 errors |
| moon check --target js | exit 0, 175 warnings, 0 errors |
| moon test src/app --target native | 134 passed, 0 failed |
| moon test src/app --target wasm | 134 passed, 0 failed |
| moon test src/app --target js | 134 passed, 0 failed |
| moon build src/cli --target native | exit 0 |
| moon build src/cli --target js | exit 0 |
| moon build . --target wasm | exit 0 |
| moon build . --target js | exit 0 |
| node scripts/verify.mjs | 87 passed, 0 failed |
| node scripts/qa/gherkin.mjs | 7 passed, 0 failed |
| node scripts/qa/e2e.mjs | 56 passed, 0 failed |
| node scripts/qa/fuzz.mjs | 110 passed, 0 failed |
| node scripts/qa/path_traversal.mjs | 7 passed, 0 failed |
| node scripts/qa/deep_nesting_probe.mjs | 2 passed, 0 failed |
| node scripts/qa/vendor_isolation.mjs | 13 passed, 0 failed |
| node scripts/qa/cli_resolver.mjs | 3 passed, 0 failed |
| node scripts/qa/root_package.mjs | 66 passed, 0 failed |
| node scripts/qa/host_requirements.mjs | 108 imports, 107/107 covered |
| node scripts/qa/core_compat.mjs | 12 declarations identical in 3 cores |
| node scripts/qa/verify_published.mjs 0.1.11 | 4 passed, 0 failed |
| Module | Files | Statements | L1 events | L2 findings | L3 VCs | Dead | Taint sinks | Time |
|---|---|---|---|---|---|---|---|---|
| riantr/moonbit_doubleML@latest → 0.164.64 | 220 | 29,853 | 218,391 | 166 | 0 (no specs) | 0 | 73 | 4.1 s |
| riantr/moonbit_static_analysis@latest → 0.4.46 | 199 | 12,970 | 14,296 | 43 | 0 (no specs) | 0 | 6 | 1.0 s |
| fixtures/vuln_demo | 1 | 32 | 604 | 11 | 2 / 1 / 3 | 3 | 6 | 4 ms |
| LunaFlow itself (--dir .) | 39 | 4,601 | 16,674 | 32 | 2 / 1 / 3 | 3 | 4 | 0.24 s |
moon run src/cli --target native -- analyze --dir fixtures/vuln_demo \
--out reports --funnel --trials 4 --max-functions 120 --slice "i@copy_prefix"
moon run src/cli --target native -- analyze riantr/moonbit_static_analysis@latest \
--out reports --funnel --trials 4 --max-functions 120 --slice "buf@render_reports"
moon run src/cli --target native -- analyze riantr/moonbit_doubleML@latest \
--out reports --funnel --trials 4 --max-functions 120 --slice "theta_hat@aggregate_coef_se"| published 0.1.4 | 0.1.5 | |
|---|---|---|
| moonbit_static_analysis@0.4.45 — dead code | 37 | 0 |
| moonbit_static_analysis@0.4.45 — taint sinks | 98 | 5 |
| moonbit_doubleML@0.164.64 — dead code | 137 | 0 |
| moonbit_doubleML@0.164.64 — statements | 43,822 | 29,853 |
| moonbit_doubleML@0.164.64 — frontend coverage | 99.87 % | 98.94 % |
# analyze a published module; @latest is resolved once and pinned
moon run src/cli --target native -- analyze riantr/moonbit_doubleML@latest --out reports
# the full funnel, with a slice on a variable
moon run src/cli --target native -- analyze riantr/moonbit_doubleML@latest \
--out reports --funnel --slice "theta_hat@aggregate_coef_se"
# a local checkout, no registry resolution
moon run src/cli --target native -- analyze --dir fixtures/vuln_demo \
--out reports --funnel --slice "i@copy_prefix"
# focus on one function in a large module
moon run src/cli --target native -- analyze riantr/moonbit_static_analysis@latest \
--function render_reports --slice "buf@render_reports" --out reports
# compiled coverage, when the target builds
moon coverage analyze
moon coverage report -f cobertura -o cov.xml
moon run src/cli --target native -- analyze <coord> --coverage cov.xml --out reportsmoonx riantr/LunaFlow@latest riantr/LunaFlow@latestL1 concrete: 12 findings
L2 abstract: 11 findings, 4 new
L3 contracts: 2 proved / 1 failed / 3 unknownflowchart LR
L1["L1 concrete: 12 findings"] -->|"dedup, escalate"| L2["L2 abstract: 11 findings, 4 new"]
L2 -->|"critical functions"| L3["L3 contracts: 2 proved / 1 failed / 3 unknown"]| Function | VC kind | Status | Goal |
|---|---|---|---|
| get_checked | index-bounds | proved | ((n >= 0) && (n < values.length())) |
| get_checked | ensures | unknown | (values[n] >= 0) |
| get_unsafe | index-bounds | unknown | ((n >= 0) && (n < values.length())) |
| get_negative | index-bounds | failed | ((n >= 0) && (n < values.length())) |
| double | ensures | proved | ((n + n) >= 0) |
Install
Download zipTrace-driven three-proof-strength control-flow, taint, value-range, slicing and contract analysis for MoonBit modules, as one Markdown report with Mermaid diagrams