Syntax-guided program reduction (Perses algorithm)
Project description
nappe
A syntax-guided test case reducer implementing the Perses algorithm (Sun et al., ICSE 2018).
Reference implementation: nnunley/bonsai (Rust).
What It Does
Given a failing test case and a program file that triggers a bug, nappe reduces the file to the smallest possible program that still reproduces the failure — while guaranteeing syntactic validity at every step.
It also provides a check command for static analysis of source files,
detecting reducible patterns like dead code, unused assignments, and style
issues.
How It Works
- Parse the input file into a concrete syntax tree using tree-sitter
- Systematically try to remove or simplify subtrees via a priority queue (largest first)
- Each candidate reduction is validated by reparsing — only syntactically valid reductions proceed
- Test each valid candidate against an "interestingness test" (any shell command that exits 0 when the bug is still present)
- Repeat until no further reductions are possible
Transforms
- Delete — remove a node entirely
- Unwrap — replace a node with one of its type-compatible children
- Unify identifiers — rename bindings to canonical short forms (planned: requires scope data)
- Dead definition removal — delete unreferenced definitions (planned: requires scope data)
Key Properties
- All intermediate results are syntactically valid
- Largest subtrees tried first (maximum reduction per test)
- Test results cached (duplicate calls avoided)
- Handles inputs with pre-existing parse errors
Supported Languages
Python, JavaScript, TypeScript, Rust, Go, C, C++ (via tree-sitter grammars).
Implementation
The project has two implementations:
- Rust (primary) — native implementation in
src/, built withcargo. This is the default and recommended implementation. - Python (reference) — pure Python implementation in
src/nappe/, kept for comparison and as a reference for the algorithm. Accessible via the--legacyflag.
Why Rust?
The Rust implementation provides significantly better performance for large files and complex reduction scenarios, while maintaining full feature parity with the Python reference.
Installation
Rust (recommended)
cargo build --release
cp target/release/nappe ~/.local/bin/
Python (reference)
uv sync
Usage
nappe reduce — Syntax-guided reduction (default)
# Reduce using a pytest interestingness test (recommended)
nappe reduce --test test_interesting.py::test_still_fails input.py
# Reduce using a shell command
nappe reduce --test-cmd "grep -q 'error'" input.py
# Auto-reduce to smallest valid program (no test needed)
nappe reduce --auto input.py
# Limit reduction time
nappe reduce --test test_interesting.py --max-time 30m --max-tests 1000 input.py
# Use Python implementation (legacy)
nappe --legacy reduce --test test_interesting.py::test_still_fails input.py
nappe check — Static analysis and fixes
# Check files for reducible patterns
nappe check src/**/*.py
# Apply safe fixes automatically
nappe check --fix src/**/*.py
# Apply all fixes (including unsafe ones like dead code removal)
nappe check --unsafe-fixes src/**/*.py
# Output as JSON
nappe check --output-format json src/**/*.py
# Filter by rule
nappe check --select RED200,RED201 src/**/*.py
nappe shrink — Shrinkray-compatible interface
nappe shrink "./check.sh" input.py
Check Rules
| Code | Description | Safe? |
|---|---|---|
| RED100 | Dead function (no callers) | unsafe |
| RED101 | Dead class (no instantiations) | unsafe |
| RED102 | Unused variable assignment | unsafe |
| RED200 | Constant expression simplification | safe |
| RED201 | Redundant parentheses | safe |
| RED202 | Unnecessary semicolon | safe |
| RED203 | Trailing whitespace | safe |
| RED204 | Redundant newline | safe |
Example: Reducing a Bug Trigger
Given a Python file that triggers a bug when fibonacci is called with
print():
Before (input.py):
import sys
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
def unused_helper(x):
return x * 2
def also_unused():
result = unused_helper(5)
return result
class MyClass:
def __init__(self):
self.value = 42
def method(self):
return self.value
if __name__ == "__main__":
for i in range(10):
print(fibonacci(i))
Write a pytest interestingness test (test_interesting.py):
import sys
def test_still_fails():
"""Exit 0 = candidate is still interesting (bug still present)."""
candidate = sys.argv[1]
content = open(candidate).read()
assert "def fibonacci" in content and "print(" in content
Run the reducer:
nappe reduce --test test_interesting.py::test_still_fails input.py
After (input.py):
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
Diff:
-import sys
-
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
-def unused_helper(x):
- return x * 2
-
-def also_unused():
- result = unused_helper(5)
- return result
-
-class MyClass:
- def __init__(self):
- self.value = 42
-
- def method(self):
- return self.value
-
-if __name__ == "__main__":
- for i in range(10):
- print(fibonacci(i))
+print(fibonacci(10))
The reducer removed the unused import sys, the unused_helper and
also_unused functions, the MyClass class, and simplified the if __name__
block — while preserving the core fibonacci function and the print() call
that triggers the bug.
Example: Checking and Fixing Code
Input (demo.py):
import os
def greet(name):
print(f"Hello, {name}")
def unused_helper(x):
return x * 2
count = 0;;
class EmptyClass:
pass
message = "hello"
greet("world")
Run the checker:
nappe check demo.py
Output:
demo.py:14:17: RED203 Trailing whitespace
14 | message = "hello"
^^^^^^^^^^^^^^^^^ RED203
demo.py:9:10: RED202 Unnecessary semicolon
9 | count = 0;;
^^^^^^^^^^^ RED202
demo.py:14:1: RED102 Unused variable assignment
14 | message = "hello"
^^^^^^^^^^^^^^^^^ RED102
demo.py:9:1: RED102 Unused variable assignment
9 | count = 0;;
^^^^^^^^^^^ RED102
demo.py:6:1: RED100 Dead function (no callers)
6 | def unused_helper(x):
^^^^^^^^^^^^^^^^^^^^^ RED100
demo.py:11:1: RED101 Dead class (no instantiations)
11 | class EmptyClass:
^^^^^^^^^^^^^^^^^ RED101
Found 6 issues (2 safe, 4 unsafe).
Apply safe fixes:
nappe check --fix demo.py
After (demo.py):
import os
def greet(name):
print(f"Hello, {name}")
def unused_helper(x):
return x * 2
count = 0;
class EmptyClass:
pass
message = "hello"
greet("world")
The --fix flag applies only safe fixes (RED200–RED204). Use --unsafe-fixes
to also remove dead functions, dead classes, and unused assignments.
Development
Rust (primary)
cargo build --release # build
cargo test # test
cargo clippy # lint
cargo fmt # format
Python (reference)
uv run ruff check src/nappe/ # lint
uv run ruff format src/nappe/ # format
uv run ty check src/nappe/ # type check
uv run pytest tests/ # test
License
MIT
References
- Syntax-Guided Program Reduction: A Survey and the nappe Implementation — Review paper covering the Perses algorithm, related work, and the nappe system.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nappe-0.1.0.tar.gz.
File metadata
- Download URL: nappe-0.1.0.tar.gz
- Upload date:
- Size: 19.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d6dbf71ebf5f369b3ee00b86a7319f2f8f1b0c2943a2231301dc7fe69663157
|
|
| MD5 |
f1ae9b794a5e02538485c85a6d002ba7
|
|
| BLAKE2b-256 |
810894bb23700bbc076c13c242cf4dc71b3b5b23e498ef7d195a87b231bc5ebd
|
Provenance
The following attestation bundles were made for nappe-0.1.0.tar.gz:
Publisher:
publish.yml on NoRaincheck/nappe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nappe-0.1.0.tar.gz -
Subject digest:
5d6dbf71ebf5f369b3ee00b86a7319f2f8f1b0c2943a2231301dc7fe69663157 - Sigstore transparency entry: 1805171168
- Sigstore integration time:
-
Permalink:
NoRaincheck/nappe@ffa287065dd490118d83b5cb93d8a28e8d8e16b0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/NoRaincheck
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ffa287065dd490118d83b5cb93d8a28e8d8e16b0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file nappe-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nappe-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a868bad565448381ddd9ba95d4fc5a7ef9a1f06f33575fdd9e2e0632bc5ead9b
|
|
| MD5 |
d889b12ab4c4a801d655d08b1cfa4505
|
|
| BLAKE2b-256 |
afd4d05eb3c8ec90fbe9efc5ff7fd3f2d3b81cb50d388a6e214d89358bf462d7
|
Provenance
The following attestation bundles were made for nappe-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on NoRaincheck/nappe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nappe-0.1.0-py3-none-any.whl -
Subject digest:
a868bad565448381ddd9ba95d4fc5a7ef9a1f06f33575fdd9e2e0632bc5ead9b - Sigstore transparency entry: 1805171202
- Sigstore integration time:
-
Permalink:
NoRaincheck/nappe@ffa287065dd490118d83b5cb93d8a28e8d8e16b0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/NoRaincheck
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ffa287065dd490118d83b5cb93d8a28e8d8e16b0 -
Trigger Event:
push
-
Statement type: