Skip to content

Add YAML-driven property-based testing for FAIR conversion#4

Open
ericbodden wants to merge 29 commits intodevelopfrom
property-based-testing
Open

Add YAML-driven property-based testing for FAIR conversion#4
ericbodden wants to merge 29 commits intodevelopfrom
property-based-testing

Conversation

@ericbodden
Copy link
Copy Markdown
Member

@ericbodden ericbodden commented Mar 21, 2026

Summary

  • Declarative property specs: src/test/resources/fair-properties.yaml defines 15 semantic invariants for the Jimple→FAIR conversion — 9 statement-level def/use contracts (apply, copy, replace, set-constant, ret, assume) and 6 CFG/flow-level properties (assume-at-head, branch-conditions, symmetric-edges, entry-no-predecessors, narrowing-paired-with-assume, assume-matches-edge).
  • Parameterized test runner: A JUnit 5 @TestFactory cross-products every property × every method across all 16 test Java classes, generating 340 dynamic tests.
  • Claude Code skill: /generate-property-checks reads the YAML spec and generates Kotlin implementations in SemanticPropertyChecks.kt, so adding a new property is: write YAML → run skill → review generated code.
  • Infrastructure: Added SnakeYAML dependency, increased test heap to 2g (SootUp view loading), and updated .gitignore to track .claude/skills/ per recommended practice.
  • Bug fix: Predicate narrowing (SET_VALUE) was placed on the wrong branch. ifeq always jumps when the condition is zero (false), so trueTarget is the false-path of the predicate. Swapped narrowingTrue/narrowingFalse placement so narrowing lands in the same block as the guarded statements.

New files

File Purpose
src/test/resources/fair-properties.yaml Declarative property specs (user-maintained)
src/test/kotlin/properties/FAIRProperty.kt Data class + YAML loader
src/test/kotlin/properties/SemanticPropertyChecks.kt Kotlin check implementations keyed by property name
src/test/kotlin/properties/FAIRPropertyTest.kt JUnit 5 dynamic test runner
.claude/skills/generate-property-checks/SKILL.md Claude Code skill for auto-generating checks

Bug fix: predicate narrowing on wrong branch

In TypestateExampleForBranchElimination.unreachableCodeTypeStateWithDivisionByZero(), the predicate narrowing apply SET_VALUE(IS_OPEN) was ending up on a different branch from the if-body statements. Root cause: JVM ifeq jumps when the condition == 0 (false), so getTargetStmts() returns the block where the predicate returned false, but the converter was placing narrowingTrue there. Fixed by swapping narrowingTrue/narrowingFalse in the branch prepend logic.

New semantic properties

Property Scope Invariant
narrowing-paired-with-assume block Blocks with SET_VALUE apply must also contain a FAIRAssumeStmt
assume-matches-edge cfg Sibling TRUE/FALSE branch assumes must use complementary comparison operators

Test plan

  • ./gradlew test — 340 tests pass (up from 286)
  • Verify SET_VALUE(IS_OPEN) and guarded statements now share the same block
  • Verify a broken property impl produces a clear failure message

🤖 Generated with Claude Code

ericbodden and others added 29 commits March 20, 2026 10:01
Implement a hybrid approach for eliminating infeasible branches:

1. Conversion-time predicate narrowing: New predicate() DSL construct
   in InvokeExprHandler lets analyses declare that a method return
   value constrains abstract state (e.g., isOpen()=true => state OPEN).
   The converter emits SetValueOp narrowing statements in branch targets.

2. Interpretation-time assume evaluation: New AssumeHandler<T> interface
   lets analyses evaluate FAIRAssumeStmt conditions against known abstract
   values. StmtResult.Unreachable propagates through FixpointEngine and
   BodyIterationStrategy to skip infeasible blocks.

Gatekeeper in DslStmtSemantics.translateIf() ensures assumes are only
generated when the analysis can evaluate them (has an AssumeHandler).

Updates to example analyses:
- Typestate: predicate rule for isOpen() with whenTrue(OPEN)
- ConstProp: withComparisons() for all 6 relational operators +
  FlatIntAssumeHandler for dead branch detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop FAIRConstantStmt case (removed upstream) and keep AssumeHandler-based
executeAssume() from the branch elimination feature. Removed merge artifact
files (BACKUP/BASE/LOCAL/REMOTE).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update all 16 test Java files to use standard void main(String[] args)
signature and remove return statements. Update Controller.kt to look up
main with VoidType instead of IntType.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual stmt-by-stmt iteration with SootUp JimplePrinter
and a TrackingStmtPrinter that records display-line positions via
IdentityHashMap. The Jimple panel now shows full formatted output
with labels, gotos, locals, and braces while preserving the
four-panel interlinked highlighting via stmtIdx to displayLine mapping.

Also fixes VoidType bug in ConvertEndpoint (main methods are void).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite FAIRBody.toString() and viz FAIR rendering to show block
structure: labels on branch targets, goto pseudo-instructions at
block ends, and two blank lines between FAIR instruction groups
from different Jimple statements. Labels use 1-based numbering and
empty pass-through blocks are resolved to avoid duplicate labels.

In the viz, goto lines render in italics and labels/blanks are
non-clickable. A new 'kind' field on FairLineInfo distinguishes
stmt/label/goto/blank lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a declarative property spec (fair-properties.yaml) with 13
semantic invariants — 9 statement-level def/use contracts and 4 CFG
flow-level properties — verified across all 16 test Java classes (286
dynamic tests). Properties are loaded by a JUnit 5 @testfactory runner
that cross-products every property × every method. A shared SootUp view
avoids OOM from repeated JRT class loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously the entire .claude/ directory was gitignored. Per recommended
practice, commit shared configuration (skills, rules, agents) and only
exclude settings.local.json which contains personal overrides.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two visualization improvements:

1. Jimple panel now has a Simplified toggle (on by default) that
   rewrites verbose invoke syntax to Java-like calls:
   - virtualinvoke r0.<Foo: int bar(int)>(x) becomes r0.bar(x)
   - staticinvoke <Foo: int bar(int)>(x) becomes Foo.bar(x)
   - specialinvoke r0.<Foo: void <init>()>() becomes new Foo()
   Toggling off shows the raw Jimple. Simplification is done client-side
   so no re-fetch is needed.

2. Method dropdown now lists methods in Java source file order, sorted
   by the earliest Java line number found in each method's Jimple body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ifeq jumps when condition == 0 (false), so trueTarget is the false-path
of the predicate and falseTarget is the true-path. Swap narrowingTrue/
narrowingFalse placement so SET_VALUE narrowing lands in the same block
as the guarded statements (e.g. division in unreachableCodeTypeState).

Add two new YAML-driven semantic properties:
- narrowing-paired-with-assume: SET_VALUE blocks must contain an assume
- assume-matches-edge: sibling TRUE/FALSE assumes must use complementary
  comparison operators

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ericbodden ericbodden force-pushed the property-based-testing branch from 2c5294d to 1efa5a3 Compare March 22, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant