Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
swarm-report/
*.iml
.kotlin
.gradle
Expand Down
78 changes: 78 additions & 0 deletions docs/guides/r8-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# R8 Dead-Code Elimination Verification

## Why it matters

Featured's core guarantee for local flags is that when a flag value is fixed at build time,
the code reachable only through the disabled branch is completely removed from the final APK.
This relies on R8 honouring the `-assumevalues` ProGuard rules generated by
`ProguardRulesGenerator`.

A rule that is syntactically correct but semantically wrong would silently fail to eliminate
dead code. The `featured-shrinker-tests` module gives automated, deterministic verification
that the exact rule format produced by the plugin is sufficient for R8 to perform DCE.

## How it works

The tests use a three-step synthetic pipeline:

1. **Bytecode generation (ASM)** — `SyntheticBytecodeFactory` builds `.class` files in
memory that mirror the structure the plugin generates at build time: a `ConfigValues`
holder, an extensions class that reads from it, branch-target classes (`IfBranchCode`,
`ElseBranchCode`, `PositiveCountCode`), and a caller entry point.

2. **Rules generation** — `ProguardRulesWriter` writes `.pro` files in the exact format
`ProguardRulesGenerator` produces, optionally including the `-assumevalues` block.
Comment on lines +23 to +24
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guide states that ProguardRulesWriter writes .pro files in the exact format ProguardRulesGenerator produces, but the test rules intentionally add extra directives (e.g., -keep, -dontwarn) and don’t include the generator’s header comments. Consider clarifying that the -assumevalues block matches the plugin output while the rest is test scaffolding.

Suggested change
2. **Rules generation**`ProguardRulesWriter` writes `.pro` files in the exact format
`ProguardRulesGenerator` produces, optionally including the `-assumevalues` block.
2. **Rules generation**`ProguardRulesWriter` writes test `.pro` files whose
`-assumevalues` block matches the format produced by `ProguardRulesGenerator`,
optionally including that block. The surrounding directives are test scaffolding
(for example `-keep` and `-dontwarn`) rather than a byte-for-byte copy of the
plugin's full output.

Copilot uses AI. Check for mistakes.

3. **R8 invocation** — `R8TestHarness.runR8()` calls R8 programmatically via
`R8Command.builder()`, producing an output JAR with DCE applied.

After each run, `JarAssertions` inspects the output JAR and asserts which classes are
present or absent, proving that the rule caused (or did not cause) elimination.

## Test scenarios

### Boolean flags (`R8BooleanFlagEliminationTest`)

| Test | Rule | Expected |
|------|------|----------|
| `if-branch class is eliminated when boolean flag returns false` | `-assumevalues … return false` | `IfBranchCode` absent; `ElseBranchCode` present |
| `else-branch class is eliminated when boolean flag returns true` | `-assumevalues … return true` | `ElseBranchCode` absent; `IfBranchCode` present |
| `both branch classes survive when no boolean assumevalues rule is present` | No `-assumevalues` | Both classes present |

The third test is a control: it proves that elimination is caused by the rule, not by R8's
own constant-folding.

### Int flags (`R8IntFlagEliminationTest`)

| Test | Rule | Expected |
|------|------|----------|
| `guarded class is eliminated when int flag is assumed to return zero` | `-assumevalues … return 0` | `PositiveCountCode` absent; `IntCaller` present |
| `guarded class survives when int flag has no assumevalues rule` | No `-assumevalues` | Both classes present |

With `-assumevalues return 0`, R8 constant-folds `0 > 0` to `false` and eliminates the
guarded block entirely.

## Running the tests

```bash
./gradlew :featured-shrinker-tests:test
```

To run only one test class:

```bash
./gradlew :featured-shrinker-tests:test --tests "dev.androidbroadcast.featured.shrinker.r8.R8BooleanFlagEliminationTest"
```

## Adding new scenarios

1. **New flag type** — add bytecode generators in `SyntheticBytecodeFactory.kt`, JAR
assembler functions in `JarAssembler.kt`, rule writers in `ProguardRulesWriter.kt`, and
a new test class in `r8/` that extends `R8TestHarness`.

2. **New rule variant** — add a `write*Rules()` function in `ProguardRulesWriter.kt` and a
corresponding `@Test` method in the relevant test class.

3. **Verifying a rule format change** — update the `write*Rules()` function to match the
new format produced by `ProguardRulesGenerator`, then run the tests to confirm DCE still
works.
2 changes: 0 additions & 2 deletions featured-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,4 @@ mavenPublishing {
dependencies {
testImplementation(gradleTestKit())
testImplementation(libs.kotlin.testJunit)
testImplementation(libs.r8)
testImplementation(libs.asm)
}
Loading
Loading