Skip to content

Commit 654912f

Browse files
committed
Avoid the CNF budget exceeded exception via smarter translation into CNF.
The exhaustivity checks in the pattern matcher build a propositional formula that must be converted into conjunctive normal form (CNF) in order to be amenable to the following DPLL decision procedure. However, the simple conversion into CNF via negation normal form and Shannon expansion that was used has exponential worst-case complexity and thus even simple problems could become untractable. A better approach is to use a transformation into an _equisatisfiable_ CNF-formula (by generating auxiliary variables) that runs with linear complexity. The commonly known Tseitin transformation uses bi- implication. I have choosen for an enhancement: the Plaisted transformation which uses implication only, thus the resulting CNF formula has (on average) only half of the clauses of a Tseitin transformation. The Plaisted transformation uses the polarities of sub-expressions to figure out which part of the bi-implication can be omitted. However, if all sub-expressions have positive polarity (e.g., after transformation into negation normal form) then the conversion is rather simple and the pseudo-normalization via NNF increases chances only one side of the bi-implication is needed. I implemented only optimizations that had a substantial effect on formula size: - formula simplification, extraction of multi argument operands - if a formula is already in CNF then the Tseitin/Plaisted transformation is omitted - Plaisted via NNF - omitted: (sharing of sub-formulas is also not implemented) - omitted: (clause subsumption)
1 parent 06ae047 commit 654912f

File tree

11 files changed

+646
-268
lines changed

11 files changed

+646
-268
lines changed

src/compiler/scala/tools/nsc/transform/patmat/Logic.scala

Lines changed: 125 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import scala.language.postfixOps
1111
import scala.collection.mutable
1212
import scala.reflect.internal.util.Statistics
1313
import scala.reflect.internal.util.HashSet
14-
import scala.annotation.tailrec
1514

1615
trait Logic extends Debugging {
1716
import PatternMatchingStats._
@@ -104,21 +103,16 @@ trait Logic extends Debugging {
104103
def implications: List[(Sym, List[Sym], List[Sym])]
105104
}
106105

107-
private def propToLinkedHashSet(ops: Seq[Prop]) = {
108-
val set = new mutable.LinkedHashSet[Prop]()
109-
ops.foreach(set += _)
110-
set
111-
}
112-
113106
// would be nice to statically check whether a prop is equational or pure,
114107
// but that requires typing relations like And(x: Tx, y: Ty) : (if(Tx == PureProp && Ty == PureProp) PureProp else Prop)
115-
final case class And(ops: mutable.LinkedHashSet[Prop]) extends Prop
108+
final case class And(ops: Set[Prop]) extends Prop
116109
object And {
117-
def apply(ops: Prop*) = new And(propToLinkedHashSet(ops))
110+
def apply(ops: Prop*) = new And(ops.toSet)
118111
}
119-
final case class Or(ops: mutable.LinkedHashSet[Prop]) extends Prop
112+
113+
final case class Or(ops: Set[Prop]) extends Prop
120114
object Or {
121-
def apply(ops: Prop*) = new Or(propToLinkedHashSet(ops))
115+
def apply(ops: Prop*) = new Or(ops.toSet)
122116
}
123117

124118
final case class Not(a: Prop) extends Prop
@@ -146,17 +140,111 @@ trait Logic extends Debugging {
146140
def /\(props: Iterable[Prop]) = if (props.isEmpty) True else And(props.toSeq: _*)
147141
def \/(props: Iterable[Prop]) = if (props.isEmpty) False else Or(props.toSeq: _*)
148142

143+
/**
144+
* Simplifies propositional formula according to the following rules:
145+
* - eliminate double negation (avoids unnecessary Tseitin variables)
146+
* - flatten trees of same connectives (avoids unnecessary Tseitin variables)
147+
* - removes constants and connectives that are in fact constant because of their operands
148+
* - eliminates duplicate operands
149+
* - convert formula into NNF: all sub-expressions have a positive polarity
150+
* which makes them amenable for the subsequent Plaisted transformation
151+
* and increases chances to figure out that the formula is already in CNF
152+
*
153+
* Complexity: DFS over formula tree
154+
*
155+
* See http://www.decision-procedures.org/slides/propositional_logic-2x3.pdf
156+
*/
157+
def simplify(f: Prop): Prop = {
158+
159+
// limit size to avoid blow up
160+
def hasImpureAtom(ops: Seq[Prop]): Boolean = ops.size < 10 &&
161+
ops.combinations(2).exists {
162+
case Seq(a, Not(b)) if a == b => true
163+
case Seq(Not(a), b) if a == b => true
164+
case _ => false
165+
}
166+
167+
// push negation inside formula
168+
def negationNormalFormNot(p: Prop): Prop = p match {
169+
case And(ops) => Or(ops.map(negationNormalFormNot)) // De'Morgan
170+
case Or(ops) => And(ops.map(negationNormalFormNot)) // De'Morgan
171+
case Not(p) => negationNormalForm(p)
172+
case True => False
173+
case False => True
174+
case s: Sym => Not(s)
175+
}
176+
177+
def negationNormalForm(p: Prop): Prop = p match {
178+
case And(ops) => And(ops.map(negationNormalForm))
179+
case Or(ops) => Or(ops.map(negationNormalForm))
180+
case Not(negated) => negationNormalFormNot(negated)
181+
case True
182+
| False
183+
| (_: Sym) => p
184+
}
185+
186+
def simplifyProp(p: Prop): Prop = p match {
187+
case And(fv) =>
188+
// recurse for nested And (pulls all Ands up)
189+
val ops = fv.map(simplifyProp) - True // ignore `True`
190+
191+
// build up Set in order to remove duplicates
192+
val opsFlattened = ops.flatMap {
193+
case And(fv) => fv
194+
case f => Set(f)
195+
}.toSeq
196+
197+
if (hasImpureAtom(opsFlattened) || opsFlattened.contains(False)) {
198+
False
199+
} else {
200+
opsFlattened match {
201+
case Seq() => True
202+
case Seq(f) => f
203+
case ops => And(ops: _*)
204+
}
205+
}
206+
case Or(fv) =>
207+
// recurse for nested Or (pulls all Ors up)
208+
val ops = fv.map(simplifyProp) - False // ignore `False`
209+
210+
val opsFlattened = ops.flatMap {
211+
case Or(fv) => fv
212+
case f => Set(f)
213+
}.toSeq
214+
215+
if (hasImpureAtom(opsFlattened) || opsFlattened.contains(True)) {
216+
True
217+
} else {
218+
opsFlattened match {
219+
case Seq() => False
220+
case Seq(f) => f
221+
case ops => Or(ops: _*)
222+
}
223+
}
224+
case Not(Not(a)) =>
225+
simplify(a)
226+
case Not(p) =>
227+
Not(simplify(p))
228+
case p =>
229+
p
230+
}
231+
232+
val nnf = negationNormalForm(f)
233+
simplifyProp(nnf)
234+
}
149235

150236
trait PropTraverser {
151237
def apply(x: Prop): Unit = x match {
152238
case And(ops) => ops foreach apply
153239
case Or(ops) => ops foreach apply
154240
case Not(a) => apply(a)
155241
case Eq(a, b) => applyVar(a); applyConst(b)
242+
case s: Sym => applySymbol(s)
156243
case _ =>
157244
}
158245
def applyVar(x: Var): Unit = {}
159246
def applyConst(x: Const): Unit = {}
247+
def applySymbol(x: Sym): Unit = {}
160248
}
161249

162250
def gatherVariables(p: Prop): Set[Var] = {
@@ -167,6 +255,14 @@ trait Logic extends Debugging {
167255
vars.toSet
168256
}
169257

258+
def gatherSymbols(p: Prop): Set[Sym] = {
259+
val syms = new mutable.HashSet[Sym]()
260+
(new PropTraverser {
261+
override def applySymbol(s: Sym) = syms += s
262+
})(p)
263+
syms.toSet
264+
}
265+
170266
trait PropMap {
171267
def apply(x: Prop): Prop = x match { // TODO: mapConserve
172268
case And(ops) => And(ops map apply)
@@ -176,27 +272,10 @@ trait Logic extends Debugging {
176272
}
177273
}
178274

179-
// to govern how much time we spend analyzing matches for unreachability/exhaustivity
180-
object AnalysisBudget {
181-
private val budgetProp = scala.sys.Prop[String]("scalac.patmat.analysisBudget")
182-
private val budgetOff = "off"
183-
val max: Int = {
184-
val DefaultBudget = 256
185-
budgetProp.option match {
186-
case Some(`budgetOff`) =>
187-
Integer.MAX_VALUE
188-
case Some(x) =>
189-
x.toInt
190-
case None =>
191-
DefaultBudget
192-
}
193-
}
194-
195-
abstract class Exception(val advice: String) extends RuntimeException("CNF budget exceeded")
196-
197-
object exceeded extends Exception(
198-
s"(The analysis required more space than allowed. Please try with scalac -D${budgetProp.key}=${AnalysisBudget.max*2} or -D${budgetProp.key}=${budgetOff}.)")
199-
275+
// TODO: remove since deprecated
276+
val budgetProp = scala.sys.Prop[String]("scalac.patmat.analysisBudget")
277+
if (budgetProp.isSet) {
278+
reportWarning(s"Please remove -D${budgetProp.key}, it is ignored.")
200279
}
201280

202281
// convert finite domain propositional logic with subtyping to pure boolean propositional logic
@@ -217,7 +296,7 @@ trait Logic extends Debugging {
217296
// TODO: for V1 representing x1 and V2 standing for x1.head, encode that
218297
// V1 = Nil implies -(V2 = Ci) for all Ci in V2's domain (i.e., it is unassignable)
219298
// may throw an AnalysisBudget.Exception
220-
def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Formula, List[Formula]) = {
299+
def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Prop, List[Prop]) = {
221300
val start = if (Statistics.canEnable) Statistics.startTimer(patmatAnaVarEq) else null
222301

223302
val vars = new mutable.HashSet[Var]
@@ -241,10 +320,10 @@ trait Logic extends Debugging {
241320
props foreach gatherEqualities.apply
242321
if (modelNull) vars foreach (_.registerNull())
243322

244-
val pure = props map (p => eqFreePropToSolvable(rewriteEqualsToProp(p)))
323+
val pure = props map (p => rewriteEqualsToProp(p))
245324

246-
val eqAxioms = formulaBuilder
247-
@inline def addAxiom(p: Prop) = addFormula(eqAxioms, eqFreePropToSolvable(p))
325+
val eqAxioms = mutable.ArrayBuffer[Prop]()
326+
@inline def addAxiom(p: Prop) = eqAxioms += p
248327

249328
debug.patmat("removeVarEq vars: "+ vars)
250329
vars.foreach { v =>
@@ -270,49 +349,30 @@ trait Logic extends Debugging {
270349
}
271350
}
272351

273-
debug.patmat(s"eqAxioms:\n$eqAxioms")
352+
debug.patmat(s"eqAxioms:\n${eqAxioms.mkString("\n")}")
274353
debug.patmat(s"pure:${pure.mkString("\n")}")
275354

276355
if (Statistics.canEnable) Statistics.stopTimer(patmatAnaVarEq, start)
277356

278-
(toFormula(eqAxioms), pure)
357+
(And(eqAxioms: _*), pure)
279358
}
280359

360+
type Solvable
281361

282-
// an interface that should be suitable for feeding a SAT solver when the time comes
283-
type Formula
284-
type FormulaBuilder
285-
286-
// creates an empty formula builder to which more formulae can be added
287-
def formulaBuilder: FormulaBuilder
288-
289-
// val f = formulaBuilder; addFormula(f, f1); ... addFormula(f, fN)
290-
// toFormula(f) == andFormula(f1, andFormula(..., fN))
291-
def addFormula(buff: FormulaBuilder, f: Formula): Unit
292-
def toFormula(buff: FormulaBuilder): Formula
293-
294-
// the conjunction of formulae `a` and `b`
295-
def andFormula(a: Formula, b: Formula): Formula
296-
297-
// equivalent formula to `a`, but simplified in a lightweight way (drop duplicate clauses)
298-
def simplifyFormula(a: Formula): Formula
299-
300-
// may throw an AnalysisBudget.Exception
301-
def propToSolvable(p: Prop): Formula = {
302-
val (eqAxioms, pure :: Nil) = removeVarEq(List(p), modelNull = false)
303-
andFormula(eqAxioms, pure)
362+
def propToSolvable(p: Prop): Solvable = {
363+
val (eqAxiom, pure :: Nil) = removeVarEq(List(p), modelNull = false)
364+
eqFreePropToSolvable(And(eqAxiom, pure))
304365
}
305366

306-
// may throw an AnalysisBudget.Exception
307-
def eqFreePropToSolvable(p: Prop): Formula
308-
def cnfString(f: Formula): String
367+
def eqFreePropToSolvable(f: Prop): Solvable
309368

310369
type Model = Map[Sym, Boolean]
311370
val EmptyModel: Model
312371
val NoModel: Model
313372

314-
def findModelFor(f: Formula): Model
315-
def findAllModelsFor(f: Formula): List[Model]
373+
def findModelFor(solvable: Solvable): Model
374+
375+
def findAllModelsFor(solvable: Solvable): List[Model]
316376
}
317377
}
318378

0 commit comments

Comments
 (0)