Skip to content

Commit d9f623d

Browse files
committed
Merge pull request scala#4078 from gbasler/topic/fix-analysis-budget
Avoid the `CNF budget exceeded` exception via smarter translation into CNF.
2 parents 53eab9a + 654912f commit d9f623d

12 files changed

+673
-277
lines changed

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

Lines changed: 148 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ trait Logic extends Debugging {
1616
import PatternMatchingStats._
1717

1818
private def max(xs: Seq[Int]) = if (xs isEmpty) 0 else xs max
19-
private def alignedColumns(cols: Seq[AnyRef]): Seq[String] = {
20-
def toString(x: AnyRef) = if (x eq null) "" else x.toString
19+
private def alignedColumns(cols: Seq[Any]): Seq[String] = {
20+
def toString(x: Any) = if (x == null) "" else x.toString
2121
if (cols.isEmpty || cols.tails.isEmpty) cols map toString
2222
else {
2323
val colLens = cols map (c => toString(c).length)
@@ -32,7 +32,7 @@ trait Logic extends Debugging {
3232
}
3333
}
3434

35-
def alignAcrossRows(xss: List[List[AnyRef]], sep: String, lineSep: String = "\n"): String = {
35+
def alignAcrossRows(xss: List[List[Any]], sep: String, lineSep: String = "\n"): String = {
3636
val maxLen = max(xss map (_.length))
3737
val padded = xss map (xs => xs ++ List.fill(maxLen - xs.length)(null))
3838
padded.transpose.map(alignedColumns).transpose map (_.mkString(sep)) mkString(lineSep)
@@ -46,7 +46,7 @@ trait Logic extends Debugging {
4646
type Tree
4747

4848
class Prop
49-
case class Eq(p: Var, q: Const) extends Prop
49+
final case class Eq(p: Var, q: Const) extends Prop
5050

5151
type Const
5252

@@ -105,43 +105,146 @@ trait Logic extends Debugging {
105105

106106
// would be nice to statically check whether a prop is equational or pure,
107107
// but that requires typing relations like And(x: Tx, y: Ty) : (if(Tx == PureProp && Ty == PureProp) PureProp else Prop)
108-
case class And(a: Prop, b: Prop) extends Prop
109-
case class Or(a: Prop, b: Prop) extends Prop
110-
case class Not(a: Prop) extends Prop
108+
final case class And(ops: Set[Prop]) extends Prop
109+
object And {
110+
def apply(ops: Prop*) = new And(ops.toSet)
111+
}
112+
113+
final case class Or(ops: Set[Prop]) extends Prop
114+
object Or {
115+
def apply(ops: Prop*) = new Or(ops.toSet)
116+
}
117+
118+
final case class Not(a: Prop) extends Prop
111119

112120
case object True extends Prop
113121
case object False extends Prop
114122

115123
// symbols are propositions
116-
abstract case class Sym(variable: Var, const: Const) extends Prop {
124+
final class Sym private[PropositionalLogic] (val variable: Var, val const: Const) extends Prop {
117125
private val id: Int = Sym.nextSymId
118126

119-
override def toString = variable +"="+ const +"#"+ id
127+
override def toString = variable + "=" + const + "#" + id
120128
}
121-
class UniqueSym(variable: Var, const: Const) extends Sym(variable, const)
129+
122130
object Sym {
123131
private val uniques: HashSet[Sym] = new HashSet("uniques", 512)
124132
def apply(variable: Var, const: Const): Sym = {
125-
val newSym = new UniqueSym(variable, const)
133+
val newSym = new Sym(variable, const)
126134
(uniques findEntryOrUpdate newSym)
127135
}
128-
private def nextSymId = {_symId += 1; _symId}; private var _symId = 0
136+
def nextSymId = {_symId += 1; _symId}; private var _symId = 0
129137
implicit val SymOrdering: Ordering[Sym] = Ordering.by(_.id)
130138
}
131139

132-
def /\(props: Iterable[Prop]) = if (props.isEmpty) True else props.reduceLeft(And(_, _))
133-
def \/(props: Iterable[Prop]) = if (props.isEmpty) False else props.reduceLeft(Or(_, _))
140+
def /\(props: Iterable[Prop]) = if (props.isEmpty) True else And(props.toSeq: _*)
141+
def \/(props: Iterable[Prop]) = if (props.isEmpty) False else Or(props.toSeq: _*)
142+
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+
}
134235

135236
trait PropTraverser {
136237
def apply(x: Prop): Unit = x match {
137-
case And(a, b) => apply(a); apply(b)
138-
case Or(a, b) => apply(a); apply(b)
238+
case And(ops) => ops foreach apply
239+
case Or(ops) => ops foreach apply
139240
case Not(a) => apply(a)
140241
case Eq(a, b) => applyVar(a); applyConst(b)
242+
case s: Sym => applySymbol(s)
141243
case _ =>
142244
}
143245
def applyVar(x: Var): Unit = {}
144246
def applyConst(x: Const): Unit = {}
247+
def applySymbol(x: Sym): Unit = {}
145248
}
146249

147250
def gatherVariables(p: Prop): Set[Var] = {
@@ -152,36 +255,27 @@ trait Logic extends Debugging {
152255
vars.toSet
153256
}
154257

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+
155266
trait PropMap {
156267
def apply(x: Prop): Prop = x match { // TODO: mapConserve
157-
case And(a, b) => And(apply(a), apply(b))
158-
case Or(a, b) => Or(apply(a), apply(b))
268+
case And(ops) => And(ops map apply)
269+
case Or(ops) => Or(ops map apply)
159270
case Not(a) => Not(apply(a))
160271
case p => p
161272
}
162273
}
163274

164-
// to govern how much time we spend analyzing matches for unreachability/exhaustivity
165-
object AnalysisBudget {
166-
private val budgetProp = scala.sys.Prop[String]("scalac.patmat.analysisBudget")
167-
private val budgetOff = "off"
168-
val max: Int = {
169-
val DefaultBudget = 256
170-
budgetProp.option match {
171-
case Some(`budgetOff`) =>
172-
Integer.MAX_VALUE
173-
case Some(x) =>
174-
x.toInt
175-
case None =>
176-
DefaultBudget
177-
}
178-
}
179-
180-
abstract class Exception(val advice: String) extends RuntimeException("CNF budget exceeded")
181-
182-
object exceeded extends Exception(
183-
s"(The analysis required more space than allowed. Please try with scalac -D${budgetProp.key}=${AnalysisBudget.max*2} or -D${budgetProp.key}=${budgetOff}.)")
184-
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.")
185279
}
186280

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

208302
val vars = new mutable.HashSet[Var]
@@ -226,10 +320,10 @@ trait Logic extends Debugging {
226320
props foreach gatherEqualities.apply
227321
if (modelNull) vars foreach (_.registerNull())
228322

229-
val pure = props map (p => eqFreePropToSolvable(rewriteEqualsToProp(p)))
323+
val pure = props map (p => rewriteEqualsToProp(p))
230324

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

234328
debug.patmat("removeVarEq vars: "+ vars)
235329
vars.foreach { v =>
@@ -255,49 +349,30 @@ trait Logic extends Debugging {
255349
}
256350
}
257351

258-
debug.patmat("eqAxioms:\n"+ cnfString(toFormula(eqAxioms)))
259-
debug.patmat("pure:"+ pure.map(p => cnfString(p)).mkString("\n"))
352+
debug.patmat(s"eqAxioms:\n${eqAxioms.mkString("\n")}")
353+
debug.patmat(s"pure:${pure.mkString("\n")}")
260354

261355
if (Statistics.canEnable) Statistics.stopTimer(patmatAnaVarEq, start)
262356

263-
(toFormula(eqAxioms), pure)
357+
(And(eqAxioms: _*), pure)
264358
}
265359

360+
type Solvable
266361

267-
// an interface that should be suitable for feeding a SAT solver when the time comes
268-
type Formula
269-
type FormulaBuilder
270-
271-
// creates an empty formula builder to which more formulae can be added
272-
def formulaBuilder: FormulaBuilder
273-
274-
// val f = formulaBuilder; addFormula(f, f1); ... addFormula(f, fN)
275-
// toFormula(f) == andFormula(f1, andFormula(..., fN))
276-
def addFormula(buff: FormulaBuilder, f: Formula): Unit
277-
def toFormula(buff: FormulaBuilder): Formula
278-
279-
// the conjunction of formulae `a` and `b`
280-
def andFormula(a: Formula, b: Formula): Formula
281-
282-
// equivalent formula to `a`, but simplified in a lightweight way (drop duplicate clauses)
283-
def simplifyFormula(a: Formula): Formula
284-
285-
// may throw an AnalysisBudget.Exception
286-
def propToSolvable(p: Prop): Formula = {
287-
val (eqAxioms, pure :: Nil) = removeVarEq(List(p), modelNull = false)
288-
andFormula(eqAxioms, pure)
362+
def propToSolvable(p: Prop): Solvable = {
363+
val (eqAxiom, pure :: Nil) = removeVarEq(List(p), modelNull = false)
364+
eqFreePropToSolvable(And(eqAxiom, pure))
289365
}
290366

291-
// may throw an AnalysisBudget.Exception
292-
def eqFreePropToSolvable(p: Prop): Formula
293-
def cnfString(f: Formula): String
367+
def eqFreePropToSolvable(f: Prop): Solvable
294368

295369
type Model = Map[Sym, Boolean]
296370
val EmptyModel: Model
297371
val NoModel: Model
298372

299-
def findModelFor(f: Formula): Model
300-
def findAllModelsFor(f: Formula): List[Model]
373+
def findModelFor(solvable: Solvable): Model
374+
375+
def findAllModelsFor(solvable: Solvable): List[Model]
301376
}
302377
}
303378

0 commit comments

Comments
 (0)