Skip to content

Add IC3/PDR engine for unbounded safety verification#265

Open
lsk567 wants to merge 9 commits intomasterfrom
ic3/pdr
Open

Add IC3/PDR engine for unbounded safety verification#265
lsk567 wants to merge 9 commits intomasterfrom
ic3/pdr

Conversation

@lsk567
Copy link
Contributor

@lsk567 lsk567 commented Feb 13, 2026

Summary

  • Adds an IC3/PDR (Property Directed Reachability) engine that proves unbounded safety properties without requiring a bound, complementing the existing BMC and k-induction engines
  • Supports ic3 as a new control command (usage: v = ic3; check; print_results; v.print_cex;)
  • Handles infinite-state (integer arithmetic) systems via symbolic cubes and integer inequality generalization
  • Generates concrete counterexample traces compatible with print_cex and print_cex_json

Changes

  • IC3Engine.scala (new): Core IC3/PDR algorithm — frame management, proof obligations, consecution checks, cube generalization (unsat cores + sign-based inequality abstraction), clause propagation, fixpoint detection, and BMC-style CEX trace reconstruction
  • Context.scala: Add checkAssumptions() / getUnsatCore() to solver interface
  • Z3Interface.scala: Implement assumption-based checking and unsat core extraction via Z3 Java API
  • ControlCommandChecker.scala: Add ic3 command validation
  • SymbolicSimulator.scala: Add ic3 command dispatch with label support for print_cex
  • build.sbt: Configure java.library.path for Z3 native libraries
  • Test files: test-ic3-0.ucl (property proved), test-ic3-1.ucl (CEX found + printed), test-ic3-2.ucl (requires strengthening)

Test plan

  • test-ic3-0.ucl: x >= 0 proved (1-inductive, fixpoint at frame 2)
  • test-ic3-1.ucl: x < 5 fails with CEX trace (x: 0→1→2→3→4→5)
  • test-ic3-2.ucl: y <= x proved with strengthening (not 1-inductive, fixpoint at frame 4)
  • examples/cpu_induction.ucl: All 9 properties proved (bitvectors, enums, uninterpreted types, arrays)
  • Full test suite: no regressions (34 pre-existing failures unchanged)

🤖 Generated with Claude Code

IC3/PDR (Property Directed Reachability) proves safety properties without
requiring a bound, complementing the existing BMC and k-induction engines.
It incrementally constructs frame-based over-approximations of reachable
states, blocks counterexample cubes via generalization (unsat cores +
integer inequality abstraction), and detects fixpoints through clause
propagation. Supports counterexample trace generation for print_cex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KE7
Copy link
Contributor

KE7 commented Feb 13, 2026

LGTM

lsk567 and others added 3 commits February 13, 2026 13:38
macos-13 is no longer supported by GitHub Actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The macOS CI was failing because olafurpg/setup-scala (deprecated) relied
on the sbt launcher to bootstrap from Maven Central without caching,
causing transient download failures. Align macOS job with Ubuntu by using
actions/setup-java + coursier/setup-action + SBT caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
actions/setup-java@v1 defaults to x64 architecture, installing an x86_64
JVM on the macos-14 ARM64 runner. This causes UnsatisfiedLinkError when
loading the ARM64 Z3 native libraries. Use setup-java@v4 with Zulu
distribution which provides native ARM64 JDK 11 for macOS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@polgreen polgreen left a comment

Choose a reason for hiding this comment

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

Minor point:

  • the tests aren't included in the regression tests run by the CI
  • it would be helpful if it could output the final frame so you could sanity check that frame is actually inductive and sufficient to prove the property

Major point:

  • this doesn't work. I haven't looked into why, but this property passes using IC3, and fails using BMC:
/* Test IC3: property should fail due to overflow */
module main {
  var x : bv3;

  init {
    x = 1bv3;
  }

  next {
    x' = x + 1bv3;
  }

  invariant x_non_neg : x > 0bv3;

  control {
    v1 = bmc(3); // fails
    check;
    v1.print_cex(x);
    v2 = ic3; // passes, and should not
    check;
    v2.print_cex(x);
  }
}

libraryDependencies += "com.github.scopt" %% "scopt" % "3.7.1"
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.3"

fork in run := true
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this now?

javaOptions in run += s"-Djava.library.path=${baseDirectory.value}/z3/bin"

fork in Test := true
javaOptions in Test += s"-Djava.library.path=${baseDirectory.value}/z3/bin"
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this now?

Copy link
Contributor

Choose a reason for hiding this comment

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

These tests need to be added to the regression tests that are run by CI

/** Store mapping from Z3 BoolExpr to smt.Expr for unsat core retrieval. */
var assumptionMap: scala.collection.immutable.Map[z3.BoolExpr, Expr] = scala.collection.immutable.Map.empty

override def checkAssumptions(assumptions: List[Expr]): SolverResult = {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if this should be inside the Z3 interface or within IC3, it seems specific to IC3

@polgreen
Copy link
Contributor

polgreen commented Feb 23, 2026

Also, really hard to debug this because I can't make sense of the SMT files it prints, because it prints one to check the initial state, and then it prints two that just contain the property and nothing else

lsk567 and others added 5 commits February 23, 2026 10:57
Register test-ic3-0, test-ic3-1, and test-ic3-2 in VerifierSpec.scala
under a new IC3VerifSpec class, following the existing test pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- build.sbt: Add comments explaining fork/javaOptions are needed for Z3
  JNI native library loading via sbt (complements LD_LIBRARY_PATH from
  setup scripts)
- Context.scala, Z3Interface.scala: Document checkAssumptions and
  getUnsatCore as standard SMT-LIB operations (check-sat-assuming,
  get-unsat-core)
- IC3Engine.scala: Disable SMT file generation during IC3 execution to
  prevent empty/broken output files from the shared solver's filePrefix
  mechanism

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After IC3 reaches a fixpoint, output the final frame clauses as the
inductive invariant. Clauses are deduplicated and printed with
user-level variable names instead of internal symbol encodings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Z3Model.evaluate() only handled IntNum and Bool values from Z3 models,
throwing RuntimeError for bitvector (BitVecNum) values. In IC3's
extractConcreteCube(), this error was silently caught, causing all
bitvector state variables to be dropped and producing a trivial `true`
cube. This led to a `false` blocking clause that poisoned frames,
creating a spurious fixpoint and incorrectly reporting properties as
PASSED.

Add BitVecNum handling to evaluate() that returns proper BitVectorLit
with the correct width. Add regression test with 3-bit bitvector
overflow (x increments from 1bv3 and wraps to 0bv3 after 7 steps).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@polgreen
Copy link
Contributor

This doesn't work:

module main {
  var x : real;
  var y: real;

  init {
    x = 0.0;
    y = 5.0;
  }

  next {
    x' = x + y;
    y' = y - 2.0;
  }

  invariant x_pos : x >= 0.0;

  control {
    v = ic3; // passes with IC3
    //v = bmc(10); // fails with bmc
    check;
    print_results;
    v.print_cex;
  }
}

Copy link
Contributor

@polgreen polgreen left a comment

Choose a reason for hiding this comment

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

See the example I've given above; this needs to handle and test all the datatypes we support, not just integers and bitvecs

val bvNum = value.asInstanceOf[z3.BitVecNum]
val bigInt = bvNum.getBigInteger()
val width = e.typ.asInstanceOf[BitVectorType].width
smt.BitVectorLit(bigInt, width)
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to consider all the datatypes we support, not just bitvectors and integers

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.

3 participants