# moon-slice

[![CI](https://github.com/NaIWaniubi/moon-slice/actions/workflows/ci.yml/badge.svg)](https://github.com/NaIWaniubi/moon-slice/actions/workflows/ci.yml)
[![Pages](https://github.com/NaIWaniubi/moon-slice/actions/workflows/pages.yml/badge.svg)](https://github.com/NaIWaniubi/moon-slice/actions/workflows/pages.yml)
[![mooncakes](https://img.shields.io/badge/mooncakes-NaIWaniubi%2Fmoon--slice%200.7.3-2f6f4e)](https://mooncakes.io/docs/NaIWaniubi/moon-slice)

[中文文档](README.zh-CN.md) · [Real-world validation](docs/CASE_STUDY.md) · [Acceptance checklist](ACCEPTANCE.md) · [Contributing](CONTRIBUTING.md) · [Roadmap](ROADMAP.md)

A program-slicing workbench for MoonBit. Point it at a `.mbt` source file and a
target line; get back the **backward slice** (which statements may have produced
the value at that line) and the **forward slice** (which statements that value
may affect).

Slicing is the standard way to answer "what here matters to this line", and it is
the usual first step for debugging, impact analysis before a refactor, and
narrowing down a diff during review.

![moon-slice report](docs/report.png)

[Open the generated report](docs/report.html) (self-contained HTML; no toolchain needed).

[Open the browser playground](https://NaIWaniubi.github.io/moon-slice/) — edit the source, choose a line and direction, and render the report live. The same page is in `web/index.html`.

The HTML report renders the PDG as an inline SVG, highlights the slice in the
source, and stays self-contained (no network, no CDN).

## Status

All layers are implemented and covered by `moon test` (338 tests, including a
10-example end-to-end corpus, a path-based precision oracle, a seeded
property-based differential suite, and package-level liveness). The data model is
frozen; the CLI is usable.

| Layer | Package | State |
| --- | --- | --- |
| Data model | `src/core` | frozen — types only |
| Parser adapter | `src/parser_adapter` | implemented — MoonBit AST → `SNode` |
| CFG | `src/cfg` | implemented — sequence, branch, loop, jump edges |
| Def-use | `src/defuse` | implemented — reaching definitions |
| CDG | `src/cdg` | implemented — structured control dependence |
| PDG | `src/pdg` | implemented — data + control edges |
| Slicing | `src/slice` | implemented — backward/forward BFS |
| Query helpers | `src/query` | implemented — line → node, dead/unreachable, data-flow paths |
| Liveness | `src/liveness` | implemented — private functions nothing calls |
| Suppressions | `src/suppress` | implemented — `// moon-slice: ignore` directives |
| Package scope | `src/interproc` | implemented — cross-file expansion and callers |
| Report | `src/report` | implemented — JSON, DOT, SARIF, inline SVG, HTML |
| CLI | `cmd/main` | implemented — `slice` (file or package), `graph`, `callgraph`, `dead`, `unused`, `callers`, `review`, `path`, `html`, debug dumps |
| Browser playground | `src/web`, `web/` | implemented — JS build + click-to-slice page |
| Example corpus | `examples/`, `testdata/` | 10 examples + expected JSON |

## Install

Requires `moonc` **0.10.16 or newer** (verified with `v0.10.17`). The analysis
packages are pure MoonBit; Node is only needed for the optional web tools.

```bash
git clone https://github.com/NaIWaniubi/moon-slice
cd moon-slice
./dev.sh check      # type-check the whole module
./dev.sh test       # run the test suite
```

The library is published on mooncakes.io as `NaIWaniubi/moon-slice@0.7.0`:

```bash
moon add NaIWaniubi/moon-slice@0.7.3
```

<https://mooncakes.io/docs/NaIWaniubi/moon-slice> · release details in
[PUBLISHING.md](PUBLISHING.md).

## Quickstart

Everything goes through the wrapper, which puts the MoonBit toolchain on `PATH`:

```bash
./dev.sh check      # type-check the whole module
./dev.sh test       # run the test suite
```

Run the CLI with `./dev.sh run cmd/main -- ...`:

```bash
./dev.sh run cmd/main -- version
# moon-slice 0.6.0
./dev.sh run cmd/main -- slice --file examples/04_while.mbt --line 7 --direction backward
# 4,5,6,7,8
./dev.sh run cmd/main -- slice --file examples/04_while.mbt --line 7 --direction forward
# 7,10
./dev.sh run cmd/main -- slice --file examples/04_while.mbt --line 7 --direction backward --json
# {"nodes":[0,1,2,3,4,5,6],"lines":[4,5,6,7,8],"errors":[]}
./dev.sh run cmd/main -- graph --file examples/04_while.mbt --format dot
./dev.sh run cmd/main -- callgraph --file bench/callgraph.mbt
# digraph callgraph { "twice" -> "inc"; "main_fn" -> "twice"; }
./dev.sh run cmd/main -- dead  --file examples/08_dead_assignment.mbt
# 4,5
./dev.sh run cmd/main -- path  --file examples/04_while.mbt --from i --to total
# n1 L4 -> n5 L7
./dev.sh run cmd/main -- html  --file examples/04_while.mbt --line 7 --out report.html
```

`bench/bench.mjs` measures the native release binary on generated inputs; see
`bench/README.md` for the command and the current numbers.

`path --from A --to B` is the taint-lite query: it finds the shortest directed
**data-flow** path from a definition of `A` to a use of `B` (control edges are
not value flow, so they are excluded by default; pass `--with-control` to search
the whole PDG). `dead` reports both definitions with no reaching use and
statements unreachable from a function entry.

The CLI exits `0` on success, `1` when `dead --fail-on-findings` finds something,
and `2` on any usage error, unreadable input, write failure, parse diagnostic or
unknown node id; it never prints a result for a run that failed. When one line holds several statements, it prints the
candidate node ids and `--node ID` selects one. `--precise` switches control
dependence from the structural rule to post-dominators.

Every failure under `--json` prints a machine-readable object and still exits 2:

```json
{"error":{"code":"read_failed","message":"failed to read ..."}}
```

The JSON shapes are fixed, so a consumer can rely on them:

| Command | Shape |
| --- | --- |
| `slice` / `path --json` | `{"nodes":[..],"lines":[..],"errors":[..]}` |
| `dead --json` | `{"nodes":[..],"lines":[..],"errors":[..],"suppressed":N}` |
| `dead --dir D --json` | `{"findings":[{"file","line","kind"}],"files","suppressed","errors"}` |
| `unused --dir D --json` | `{"findings":[{"file","line","name"}],"packages","files","suppressed","errors"}` |
| `slice --dir D --file F --json` | `{"files":[{"file","nodes","lines"}],"errors"}` |
| `callers --dir D --name F --json` | `{"name","files","callers":[{"file","line","caller"}],"errors"}` |
| `dead --format sarif` | SARIF 2.1.0 (`runs[0].results[]`, `properties.files`) |
| `path --json` with no path | `{"found":false}` |
| `callgraph --json` | `[["caller","callee"],..]` |
| `graph --format json` | `{"nodes":[{"id","kind","line","synthetic"}],"edges":[{"from","to","kind"}]}` |
| any failure with `--json` | `{"error":{"code","message"}}` |

The HTML report embeds the same result for consumers that cannot parse HTML:
`<script type="application/json" id="moon-slice-result">` holds
`{"target":{"line","node"},"nodes":[..],"lines":[..],"errors":[..]}`.

The same pipeline from MoonBit code:

```moonbit
let program = @parser_adapter.parse_source(source, "examples/04_while.mbt")
let cfg     = @cfg.build(program)
let defuse  = @defuse.analyze(program, cfg)
let cdg     = @cdg.build(program, cfg)
let pdg     = @pdg.build(defuse, cdg)
let node    = @query.target_node(program, 7).unwrap()
let result  = @slice.backward(pdg, node, program=program)
println(result.lines) // [4, 5, 6, 7, 8]
```

## Architecture

Five passes over one shared, immutable data model. Each stage consumes the
previous stage's output and nothing else, so each can be tested on a
hand-built input:

```text
.mbt source
    │
    ▼
parser adapter   moonbitlang/parser AST ──▶ @core.Program      (SNode tree, Loc, VarId)
    │
    ▼
CFG              @core.Program  ──▶ @core.Cfg                   (Int32×Int32 edges, entry, exit)
    │
    ▼
def-use          Program + Cfg  ──▶ @core.DefUse               (reaching definitions)
    │
    ▼
CDG              Program + Cfg  ──▶ @core.Cdg                   (controller → controlled)
    │
    ▼
PDG              DefUse + Cdg   ──▶ @core.Pdg                   (data + control edges)
    │
    ▼
slice            Pdg + target   ──▶ @core.SliceResult           (node ids, sorted lines)
    │
    ▼
report           Program + Result ──▶ JSON / DOT / HTML          (wave 2)
```

Design points worth knowing:

- **The IR is the contract.** `src/core/types.mbt` is frozen; every package
  agrees on `SNode`, `Cfg`, `DefUse`, `Cdg`, `Pdg`, `SliceResult`. This is what
  lets the passes be built and tested independently and in parallel.
- **Node ids are the unit; lines are the view.** A node carries a `Loc` and a
  `start_line`. `SliceResult` returns both `nodes` and `lines`; `lines` is the
  unique sorted projection used for display and for the example corpus.
- **Variables are `VarId { name, scope }`, not strings.** Shadowing therefore
  gives two distinct ids, and def-use does not conflate them.
- **Slices include the target.** The target statement's own definition is part
  of its slice, and the reported line lists are inclusive.

## Supported subset

Modeled with precise data and control edges:

- free functions and `impl T with fn ...` methods / `impl T with view ...`
  views (each method body is analysed as its own function)
- `let` and `let mut` bindings; assignment to a mutable binding
- `if` / `else` (including `else if` chains) and merges after the join
- `while` and `for` / `for .. in` loops, with loop-carried definitions
- `match` with multiple arms
- `guard ... else` (the continuation and the else body are both branches)
- C-style `for init; condition; update`: the loop binder is visible to the
  condition and the update is a real definition in the body region
- `return`, `break`, `continue`
- expression statements, operator expressions, calls
- identifiers and field access (`p.x`)

Anything else the adapter does not recognize is kept as a generic `Expr` node
whose **uses are all identifiers appearing in it** — deliberately conservative,
so the slice can only get bigger, never silently wrong.

Field and array writes are modeled as an access to the base variable `p`, not to
a distinct `p.left`. That is a heap abstraction choice, not an oversight; see
below.

## Limitations

These are real and intentional for the MVP. They are the places where the tool
may report a larger slice than a precise analysis would:

- **Intra-procedural by default.** Each function is analyzed on its own. A call
  is a data dependency on its arguments; the callee body is not inlined. The
  optional `--interprocedural` mode adds callee return-value cones (whole body
  for bare call statements), transitively; it does not map arguments precisely
  and does not propagate mutations/side effects back.
- **Conservative aliasing.** Distinct names that may refer to the same object
  are not tracked. `p.left` and `p.right` are both treated as a use/def of `p`,
  so a write to one field is assumed to affect reads of any field. This shows up
  as extra lines in slices like `examples/10_field_write.mbt`; it never omits a
  line.
- **Control dependence has two modes.** The default (`build`) is derived from
  the syntax tree: an `if`/`while`/`for`/`match` controls the statements lexically
  inside it, and `return`/`break`/`continue` are handled conservatively. It never
  under-approximates, so a slice may include a few extra lines. `--precise`
  (`build_precise`) solves post-dominators instead and is exact for the CFG, at
  the cost of needing a well-formed one. Neither models interprocedural control
  dependence or exception edges (`raise`/`try` are not CFG edges here).
- **`dead` is definition- and reachability-based.** It reports definitions with
  no reaching use and statements unreachable from a function entry. It does not
  infer that a store is dead only because a callee never reads it, and it does
  not flag an unused parameter as dead (the synthetic entry node defines it).
- **One line can hold several statements.** The CLI never guesses silently: it
  lists the candidate node ids and `--node ID` picks one. Without `--node` it
  uses the first statement on the line and says which node that was.
- **No points-to, no reflection, no macro expansion.** Dynamic dispatch and
  metaprogramming are opaque; the conservative "all identifiers are uses" rule
  applies. `callers` resolves `x.f()` only when the receiver's type is written in
  the source, and interprocedural slicing does not follow those edges at all.
- **Line granularity.** Results are sets of line numbers. Two statements on the
  same line collapse into one entry, and multi-line expressions are attributed
  to their first line. The `nodes` field of `--json` keeps the distinction; use
  `--node ID` to target one of them.

## Examples

`examples/` holds ten small programs, one per modeling concern — `let`, `mut`,
`if/else`, `while`, `for`, `match`, `return`, dead assignment, shadowing, field
write. Each begins with its expected result:

```moonbit
// EXPECT-BACKWARD 7 -> [4, 5, 6, 7, 8]
// EXPECT-FORWARD 7 -> [7, 10]
fn loop_sum(n : Int) -> Int {
  let mut i = 0
  let mut total = 0
  while i < n {
    total = total + i // target
    i = i + 1
  }
  return total
}
```

`testdata/README.md` explains every expected set line by line, including the
deliberate edge cases: dead stores absent from the slice, loop-carried
definitions, control dependence contributing the loop header, and scope-sensitive
shadowing. `testdata/expected.json` is the same table in machine-readable form
for the wave-2 end-to-end test.

Note that line 8 is in the backward slice of line 7 — `i = i + 1` reaches the
next iteration of the loop body — while line 8's own value does not affect
`total` on this iteration. Loop-carried edges are the most common source of
surprise in a slicing result.

`examples/` has no `moon.pkg`, so the build never tries to compile it as a
package; it is data. The files are still valid MoonBit and were verified with
the real compiler.

## HTML report

`html` writes a single self-contained file: the dependency graph is an inline
SVG layered in source order (solid = data, dashed = control; orange = target,
green = slice), followed by highlighted source, metrics, parse errors, and the
DOT fallback. The page has no network dependency. See `docs/report.png` for a
rendered example.

## Browser playground

The same MoonBit pipeline compiles to JavaScript (`moon build --target js src/web`).
`web/index.html` is a no-server playground that behaves like a small tool rather
than a text box:

- **the line gutter is clickable** — click a number to slice that statement;
- the gutter **marks the slice in green and the target in amber**, painted from
  the report's own embedded JSON, so the standalone report stays complete;
- **five examples** ship with it (while, match, dead stores, interprocedural,
  shadowing), each carrying its own target line and direction;
- editing **re-runs automatically** after a pause, `Ctrl`+`Enter` runs at once;
- **Copy link** puts the exact source, line and direction in the URL.

The prebuilt engine is `web/moon-slice.js`; `web/build.sh` rebuilds it,
`web/smoke.mjs` asserts it renders a full report, and `web/presets.mjs` checks
every example *and* every direction against the same numbers the CLI documents.

![moon-slice playground: click a line, the slice appears](docs/demo.gif)

`.github/workflows/pages.yml` publishes `web/` to GitHub Pages on every push to
`master`: https://NaIWaniubi.github.io/moon-slice/

## Data-flow paths

`path --from A --to B` finds the shortest directed **data** path from a
definition of `A` to a use of `B`. Control edges are not value flow, so they are
excluded by default; `--with-control` searches the full PDG. It is the
taint-lite use case: mark a source variable and a sink variable, and read the
flow in one line.

```bash
./dev.sh run cmd/main -- path --file examples/04_while.mbt --from i --to total
# n1 L4 -> n5 L7
./dev.sh run cmd/main -- path --file examples/07_return.mbt --from base --to total
# n1 L4 -> n3 L6 -> n4 L7
```

## Precision

Correctness is checked two ways, and the second one does not reuse the first
one's code:

- **Hand-written expectations.** Every example carries `EXPECT-BACKWARD` /
  `EXPECT-FORWARD` headers, `testdata/expected.json` mirrors them, and
  `moon test` asserts the exact line sets.
- **An independent oracle.** `src/integration_test/precision_oracle_test.mbt`
  re-derives reaching definitions by enumerating CFG walks (each node visited at
  most twice, so loop-carried definitions are represented) and compares the
  result with the work-list analysis. It runs on straight-line code, if/else,
  nested branches, while, a C-style `for` with `continue`, match, shadowing,
  `guard`, recursion, field/array writes, a nested loop, and the whole example
  corpus. A disagreement fails the test with both pair sets printed.

- **Two control-dependence builders.** `cdg::build` is syntax-directed and
  cannot under-approximate; `cdg::build_precise` solves post-dominators and
  derives control dependence exactly (Ferrante–Ottenstein–Warren). On
  `if c { x = 1 } else { x = 2 }` the structural builder also marks each branch
  region as controlling the other, which is impossible; the precise builder does
  not. On `while { if c { return }; x = x + 1 }` the structural builder emits 18
  control edges and the precise one emits 7. Use `--precise` to switch.
- **Seeded property testing.** A grammar-driven generator builds 40 programs per
  run and checks structural invariants, oracle agreement, slice closure,
  criterion membership, line projection and interprocedural superset-ness. The
  seed is printed on failure, so every failure reproduces exactly.

Ambiguous targets are never silently guessed. `statement_candidates` returns
every statement on a line; the CLI prints the candidate node ids and accepts
`--node ID` to pick one, and the HTML report highlights the selected row and
names the node in its metrics.

## What a diff affects

`review` reads a unified diff and forward-slices every changed line, so a
reviewer can see what a change touches beyond what it appears to touch:

```bash
bash tools/review.sh              # changes since HEAD~1
bash tools/review.sh origin/main
bash tools/review.sh HEAD~3 --json
```

```text
src/query/query.mbt: changed 55 line(s) -> affected 18 line(s) in visible_lines, unreachable_nodes, path_between
  affected lines: 187,188,189,...
review: 5 file(s), 163 changed line(s), 212 affected line(s)
```

`tools/review.sh` produces the diff with `git diff -U0`; the CLI side is
`review --diff FILE`, so the analysis is testable without a git process and the
wrapper is three lines of shell.

## Dead code and unused functions

Two questions, two commands. `dead` looks inside functions:

```bash
./dev.sh run cmd/main -- dead --file examples/08_dead_assignment.mbt   # 4,5
./dev.sh run cmd/main -- dead --dir examples                          # the whole tree
```

`unused` looks at the package as a whole: a private function is visible to its
whole package, so the question "does anything still call this?" can only be
answered per package, not per file.

```bash
./dev.sh run cmd/main -- unused --dir src
./dev.sh run cmd/main -- unused --dir examples        # 10 findings: the snippets are standalone
```

It is deliberately conservative, and each rule exists because real code broke the
simpler version:

- **entry points stay silent**: `main` and `init` are called by the runtime;
- **`pub` functions are the API**, so they are never reported;
- **methods are out of scope** — `impl T with fn m` and `fn T::m(self)` are
  reached through dot syntax or trait dispatch, which this pass does not model;
- **a name mentioned anywhere in the package suppresses the finding**, including
  comments and interpolation text, so the report only contains names that occur
  exactly once: at their own definition.

On `moonbitlang/async` (368 files) and `moonbitlang/x` (166 files) it reports
nothing; on `moonbitlang/core` (1452 files) it finds four functions, every one of
them named `unused_function` / `suppress_unused_v128_import_on_*` by the library
itself. Details and the two false-positive classes this found in moon-slice are
in [docs/CASE_STUDY.md](docs/CASE_STUDY.md).

### Suppressing a finding

A finding you have already judged can be silenced in place; unlike a baseline it
moves with the code when lines shift.

```moonbit
let unused = compute() // moon-slice: ignore dead-store

// moon-slice: ignore[unused-function]
fn helper() -> Unit { ... }
```

- On a line with code, the directive covers that line. On its own line, it
  covers the next line of code — blank lines and comments in between are
  skipped.
- Bare `ignore` covers every rule; naming rules narrows it to those three:
  `dead-store`, `unreachable-code`, `unused-function`. Any other spelling,
  including a typo, leaves the directive inert, because a wrong name must never
  silence a line by accident.
- The directive has to be the last thing on the line, so `"// moon-slice:
  ignore"` inside a string literal is not a directive.
- Suppressed findings still count in `suppressed` (together with the ones a
  baseline hides) and are still recorded by `--write-baseline`, so a baseline
  stays a full picture of what the analysis sees.

## Slicing across a package

`slice --file F` sees one file. With `--dir D` the package becomes the scope, so
a call is followed into whichever file under `D` defines the callee:

```bash
# Callee in a sibling file: `twice` lives in b.mbt, the call site in a.mbt.
./dev.sh run cmd/main -- slice --dir pkg --file pkg/a.mbt --line 2
# pkg/a.mbt: 2
./dev.sh run cmd/main -- slice --dir pkg --file pkg/a.mbt --line 2 --interprocedural
# pkg/a.mbt: 2
# pkg/b.mbt: 2,6,7,8
```

The result is per file, because a node id only means something inside the program
it came from. Semantics match the single-file expansion: a bare call contributes
the callee's whole body, a value-returning call contributes only its
return-value cone (so a dead store in the callee stays out), and expansion is
transitive. A definition in the same file always wins over a same-named one
elsewhere.

The mirror question has its own command:

```bash
./dev.sh run cmd/main -- callers --dir . --name baseline_key
# cmd/main/main.mbt:352: filter_baseline
# cmd/main/main.mbt:374: write_baseline_file
# cmd/main/main.mbt:449: write_scan_baseline
# cmd/main/main.mbt:573: run_dead_dir
# cmd/main/main.mbt:613: write_unused_baseline
# cmd/main/main.mbt:765: run_unused_dir
```

`callers` works through qualified calls (`T::f(x)`) and through receiver calls
(`x.f()`) whose type is written down in the source:

```moonbit
fn use_point(p : Point) -> Int { p.norm() }   // parameter annotation
let q : Point = ...      ; q.norm()           // annotated let / let mut
impl Point { fn m(self) { self.norm() } }     // self in an impl
fn Point::m(self) { self.norm() }             // self in a qualified method
```

A dot call becomes an edge only when the receiver's type declares that method in
the same package, so `p.norm()` is never credited to an unrelated free function
named `norm`; the rule that keeps the call graph honest is pinned by tests. A
receiver with no annotation (`let p = make(); p.norm()`), a chained receiver
(`a.b.f()`), and a name annotated with two different types are all left
unresolved on purpose — a missing edge beats an invented one. Interprocedural
slicing still expands only direct calls, so `x.f()` shifts `callers` without
widening a slice.

Slicing itself: [docs/CASE_STUDY.md](docs/CASE_STUDY.md) records the package-scope
run on moon-slice's own `main.mbt` → `review.mbt` call.

## CI integration

`dead --dir D` and `unused --dir D` scan a tree in one command, skip `.git`,
`_build`, `target`, `.mooncakes` and `node_modules`, and return a stable order.
Both compose with SARIF, a baseline and a gate:

```bash
# Upload findings to GitHub code scanning.
./dev.sh run cmd/main -- dead   --dir . --format sarif --out dead.sarif
./dev.sh run cmd/main -- unused --dir . --format sarif --out unused.sarif

# Record today's findings, then fail only on new ones.
./dev.sh run cmd/main -- dead   --dir . --write-baseline .moon-slice-baseline
./dev.sh run cmd/main -- unused --dir . --write-baseline .moon-slice-baseline
./dev.sh run cmd/main -- dead   --dir . --baseline .moon-slice-baseline --fail-on-findings
./dev.sh run cmd/main -- unused --dir . --baseline .moon-slice-baseline --fail-on-findings
```

The baseline is a text file of `path:line` entries; `--write-baseline` generates
it, and `--fail-on-findings` exits `1` only for findings that survive filtering.
A file that cannot be parsed is reported in `--json`/SARIF (and as a `# error:`
line in text mode) but does not hide the other files. A single finding is
cheaper to silence where it lives: `// moon-slice: ignore[dead-store]` (see
[Suppressing a finding](#suppressing-a-finding)).

`action.yml` wraps the build and the commands for GitHub Actions, and
`templates/review-workflow.yml` is a ready-to-copy workflow:

```yaml
- uses: NaIWaniubi/moon-slice@v0.7.2
  with:
    command: review
    since: ${{ github.event.pull_request.base.sha }}

- uses: NaIWaniubi/moon-slice@v0.7.2
  with:
    command: dead
    args: --dir . --baseline .moon-slice-baseline --format sarif --out moon-slice.sarif --fail-on-findings
```

The action builds the native binary from the tag it is pinned to, so the version
in the workflow is the version that runs.

## Comparing the two control-dependence builders

`compare` prints how many control edges the structural builder reports, how many
the post-dominator builder reports, and which edges each drops or adds. With
`--line N` it also prints both slices for that line, so the precision difference
is visible on one screen:

```bash
./dev.sh run cmd/main -- compare --file bench/precise_cdg.mbt --line 7
# structural control edges: 18
# precise control edges:    7
# removed by precise:       12
# added by precise:         1
# slice line 7 (structural): 2,3,4,7
# slice line 7 (precise):    2,3,4,7
```

The removed edges are the structural rule's over-approximations (a branch region
"controlling" a sibling, the loop head "controlled" by its own body). The added
edge is the loop-carried dependence `if -> loop head`, which the syntax tree
cannot express.

## Call graph

`callgraph --file F` prints a function-level DOT graph from the recorded call
sites. Every free-function application in a statement is recorded, including
nested applications such as `g(h(x))` and calls in `if`/`while`/`for`
conditions; method calls and function values are omitted. Only callees defined
in the same file are emitted. A call from a function to itself is a real edge.

## Inter-procedural mode

`slice ... --interprocedural` expands the recorded call sites transitively. A
value-returning call adds only the callee's return-value cone (so dead stores in
the callee are excluded); a bare call statement adds the whole callee body
because its side effects cannot be summarized yet. Calls inside an added body
expand too. Side effects of value-returning calls are not modeled. The HTML
report honors the same flag. The default remains intra-procedural.

```bash
./dev.sh run cmd/main -- slice --file bench/callgraph.mbt --line 6 --direction backward --interprocedural
# 2,6
./dev.sh run cmd/main -- slice --file bench/callgraph.mbt --line 12 --direction backward --interprocedural
# 2,6,7,8,12
```

## Verifying the corpus

The headers and `testdata/expected.json` must agree, and every mutation
described above must stay consistent. The table in `testdata/README.md` is the
human-readable version; the script does not parse that Markdown table, so keep
the table and JSON in sync by review. One script checks the headers and JSON:

```bash
bash testdata/verify_corpus.sh
# corpus ok: 10 examples, headers and expected.json agree
```

It enforces: exactly 10 examples, each file ≤ 30 lines, both `EXPECT-*` headers
present and well-formed, every listed line within the file, lists strictly
ascending, the target included in its own slice, and the JSON table matching the
headers. It is a plain shell + `node` script, independent of the MoonBit
packages, so it runs before the analysis passes exist.

## Development

```bash
./dev.sh check                    # type-check
./dev.sh test                     # run tests
./dev.sh test --update            # refresh snapshot tests
./dev.sh coverage analyze > uncovered.log
bash test/cli_smoke.sh         # CLI end-to-end assertions
```

PowerShell / cmd users: replace `./dev.sh` with `dev.cmd`.

`moon fmt` and `moon info` are coordinate-after-merge steps in this repo; running
them inside a feature branch rewrites shared files and causes merge conflicts.

Deeper notes: [algorithm map](docs/ALGORITHMS.md) ·
[demo walkthrough](docs/DEMO.md) · [coverage report](COVERAGE.md).

## License

Apache License 2.0 — see [LICENSE](LICENSE). The `moonbitlang/parser` dependency
is also Apache-2.0.
