1 module satd.solvers.cdcl;
2 import satd.cnf : CNF, Clause, Literal, Set;
3 import satd.dimacs : Preamble, parseResult;
4 import std.container : RedBlackTree, redBlackTree;
5 import std.typecons : Tuple;
6 import std.algorithm : each, any, sort, filter, map, countUntil, min, count, all;
7 import std.range : front, popFront, iota, enumerate, empty, retro;
8 import std.math : abs;
9 import std.string : format;
10 import std.stdio : File, stdin;
11 import std.array : join, split, array;
12 import std.conv : to;
13 import std.variant : Algebraic;
14 
15 import std.stdio;
16 
17 /// LAMBDA は conflict node の意。
18 const Literal LAMBDA = 0;
19 
20 struct ImplicationGraph
21 {
22     /// dlevel とは、decision level のことをさす。
23     alias Node = Tuple!(Literal, "literal", size_t, "dlevel");
24 
25     Set!Node nodes;
26     Set!Node[Node] successors;
27     Set!Node[Node] predecessors;
28     Clause.ID[Node][Node] edges;
29 
30     Literal[] decisionLiterals;
31 
32     void initalize()
33     {
34         nodes = redBlackTree!Node;
35     }
36 
37     /// 与えられた implication graph を deep copy するためのコンストラクタ
38     this(ImplicationGraph graph)
39     {
40         this.nodes = graph.nodes.dup;
41         foreach (key, value; graph.successors)
42             this.successors[key] = value.dup;
43         foreach (key, value; graph.predecessors)
44             this.predecessors[key] = value.dup;
45         foreach (key, value; graph.edges)
46             this.edges[key] = value.dup;
47 
48         this.decisionLiterals = graph.decisionLiterals.dup;
49     }
50 
51     /// 与えられた decision level 以上の頂点やそれに繋がっている辺を削除します。
52     void removeLevel(size_t dlevel)
53     {
54         foreach (node; nodes)
55         {
56             if (node.dlevel >= dlevel)
57                 nodes.removeKey(node);
58         }
59         foreach (key, value; successors)
60         {
61             if (key.dlevel >= dlevel)
62                 successors.remove(key);
63             foreach (node; value)
64             {
65                 if (node.dlevel >= dlevel)
66                     value.removeKey(node);
67             }
68         }
69         foreach (key, value; predecessors)
70         {
71             if (key.dlevel >= dlevel)
72                 successors.remove(key);
73             foreach (node; value)
74             {
75                 if (node.dlevel >= dlevel)
76                     value.removeKey(node);
77             }
78         }
79         foreach (key, value; edges)
80         {
81             if (key.dlevel >= dlevel)
82             {
83                 edges.remove(key);
84                 continue;
85             }
86             foreach (key2, _; value)
87             {
88                 if (key2.dlevel >= dlevel)
89                 {
90                     value.remove(key2);
91                 }
92             }
93         }
94     }
95 
96     /// 与えられたリテラルを持つ頂点を取得します。
97     Node getNode(Literal lit)
98     {
99         auto tmp = nodes.array.filter!(n => n.literal == lit);
100         assert(tmp.count == 1);
101         return tmp.front;
102     }
103 
104     /// 1UIP cut を取得します。
105     Set!Node get1UIPCut()
106     {
107         Set!Node res = redBlackTree!Node;
108 
109         Set!Node visited = redBlackTree!Node;
110 
111         const ulong dlevel = decisionLiterals.length;
112 
113         Node[] queue = [Node(0, dlevel)];
114         bool found1UIP = false;
115         immutable Node decisionNode = Node(decisionLiterals[$ - 1], dlevel);
116         bool is1UIPDecisionNode = false;
117 
118         while (!found1UIP)
119         {
120             Node[] nextQueue;
121             foreach (Node n; queue)
122             {
123                 if (n in predecessors)
124                 {
125                     foreach (pred; predecessors[n])
126                     {
127                         if (pred == decisionNode)
128                             is1UIPDecisionNode = true;
129                         if (visited.insert(pred))
130                         {
131                             if (pred.dlevel != dlevel)
132                                 res.insert(pred);
133                             else
134                                 nextQueue ~= pred;
135                         }
136                     }
137                 }
138             }
139             if (!is1UIPDecisionNode && nextQueue.length == 1)
140             {
141                 found1UIP = true;
142                 res.insert(nextQueue[0]);
143             }
144             else if (nextQueue.length == 0)
145             {
146                 found1UIP = true;
147                 res.insert(decisionNode);
148             }
149             queue = nextQueue;
150             // debug stderr.writefln("nextQueue: %s", nextQueue);
151         }
152 
153         return res;
154     }
155 
156     /++
157      + implication graph を conflict graph の制約を満たすように変形します。
158      + この関数呼出後のグラフは以下の制約のすべてを満たしていることが保証されています。
159      +
160      + 1. Λ とただ一つの conflict variable を含んでいる。
161      + 2. 任意の頂点について、Λ への道が存在する。
162      + 3. Λ を除くすべての頂点 l について、それが decision literal であるか、または
163      +    (l_1 or l_2 or ... or l_k or l) が既知の節として存在して、かつ
164      +    not l_1, not l_2, ..., not l_k が predecessor としてグラフに存在する。
165      +/
166     void transformToConflictGraph(size_t level)
167     {
168         Set!Node conflictGraphNodes = redBlackTree!Node;
169         Node[] queue;
170         queue ~= Node(0, level);
171 
172         while (!queue.empty)
173         {
174             Node n = queue.front;
175             queue.popFront();
176             if (conflictGraphNodes.insert(n))
177             {
178                 if (n in predecessors)
179                     foreach (pred; predecessors[n])
180                         queue ~= pred;
181             }
182         }
183 
184         Set!Node[Node] successors_;
185         Set!Node[Node] predecessors_;
186         Clause.ID[Node][Node] edges_;
187 
188         foreach (n; conflictGraphNodes)
189         {
190             if (n in successors)
191                 foreach (node; successors[n])
192                     if (node in conflictGraphNodes)
193                     {
194                         if (n !in successors_)
195                             successors_[n] = redBlackTree!Node;
196                         successors_[n].insert(node);
197                     }
198             if (n in predecessors)
199                 foreach (node; predecessors[n])
200                     if (node in conflictGraphNodes)
201                     {
202                         if (n !in predecessors_)
203                             predecessors_[n] = redBlackTree!Node;
204                         predecessors_[n].insert(node);
205                     }
206         }
207 
208         foreach (from, tos; successors_)
209             foreach (to; tos)
210                 edges_[from][to] = edges[from][to];
211 
212         nodes = conflictGraphNodes;
213         successors = successors_;
214         predecessors = predecessors_;
215         edges = edges_;
216     }
217 }
218 
219 alias CDCLSolverResult = Algebraic!(Literal[], typeof(null));
220 
221 /// CDCL を実装したソルバー
222 class CDCLSolver
223 {
224     Clause[Clause.ID] clauses;
225     Set!(long) unassignedVariables;
226     /// まだ充足されていない節の ID の集合
227     RedBlackTree!(Clause.ID, "a > b") availClauses;
228     ImplicationGraph implicationGraph;
229     /// implication graph 上の頂点の decision level の最大値
230     size_t currentLevel;
231 
232     RedBlackTree!(Clause.ID, "a > b") unitClauses;
233     Set!(Clause.ID)[Literal] clausesContainingLiteral;
234     /// CNF の単純化が適用されていない形の節
235     Clause[Clause.ID] originalClauses;
236     Literal[] decisionVariables;
237     bool generateGraph = false;
238     bool generateAnotherGraph = false;
239     Preamble preamble;
240 
241     /// ソルバーの状態が各 decision level に対応して保持されている配列
242     CDCLSolver[] history;
243 
244     bool restart = false;
245     long restartThreshold = 100;
246     double restartMult = 1.5;
247     ulong conflictCount;
248 
249     private Clause.ID usedIDNum;
250 
251     Clause.ID nextClauseID()
252     {
253         return ++usedIDNum;
254     }
255 
256     this()
257     {
258         implicationGraph.initalize();
259     }
260 
261     /// 読み込んだ CNF の情報を元にソルバーを初期化します。
262     void initialize(parseResult res)
263     {
264         availClauses = redBlackTree!("a > b", Clause.ID);
265         unitClauses = redBlackTree!("a > b", Clause.ID);
266         foreach (clause; res.clauses)
267         {
268             this.clauses[clause.id] = clause;
269             if (clause.isUnitClause)
270                 unitClauses.insert(clause.id);
271             availClauses.insert(clause.id);
272 
273             foreach (literal; clause.literals.array)
274             {
275                 if (literal !in clausesContainingLiteral)
276                     clausesContainingLiteral[literal] = redBlackTree!(Clause.ID);
277                 clausesContainingLiteral[literal].insert(clause.id);
278             }
279 
280             // debug stderr.writefln("%d: %s", clause.id, clause);
281         }
282         clausesContainingLiteral[0] = redBlackTree!(Clause.ID);
283 
284         foreach (key, value; this.clauses)
285         {
286             this.originalClauses[key] = Clause(value);
287         }
288         usedIDNum = clauses.length;
289         unassignedVariables = redBlackTree!long(iota(1,
290                 res.preamble.variables + 1).array.to!(long[]));
291         this.preamble = res.preamble;
292     }
293 
294     /// for deep copy
295     this(CDCLSolver solver)
296     {
297         foreach (key, value; solver.clauses)
298             this.clauses[key] = Clause(value);
299         this.unassignedVariables = solver.unassignedVariables.dup;
300         this.availClauses = solver.availClauses.dup;
301         this.implicationGraph = ImplicationGraph(solver.implicationGraph);
302         this.availClauses = solver.availClauses.dup;
303         this.decisionVariables = solver.decisionVariables.dup;
304         this.currentLevel = solver.currentLevel;
305     }
306 
307     enum SolverStatus
308     {
309         SAT,
310         CONFLICT,
311         OK
312     }
313 
314     /// 問題を与えて初期化したソルバーで実行すると、その問題を解いて結果を返します。
315     CDCLSolverResult solve()
316     {
317         // debug stderr.writefln("given clauses: %s", this.clauses.values);
318 
319         if (this.clauses.values.any!(c => c.literals.length == 0))
320             return CDCLSolverResult(null);
321         if (this.preamble.variables == 0 && this.preamble.clauses == 0)
322             return CDCLSolverResult(cast(Literal[])[]);
323         while (true)
324         {
325             while (true)
326             {
327                 immutable SolverStatus status = deduce();
328                 // debug stderr.writefln("Deduce done. nodes: %(%s, %)",
329                 // implicationGraph.nodes.array.map!(p => format("(%d, %d)", p[0], p[1])));
330                 if (status == SolverStatus.SAT)
331                     return CDCLSolverResult(implicationGraph.nodes.array.map!(node => node.literal)
332                             .array);
333                 if (status == SolverStatus.CONFLICT)
334                 {
335                     toDOT(true);
336                     auto res = analyzeConflict();
337                     if (res.blevel == -1)
338                         return CDCLSolverResult(null);
339 
340                     backtrack(res.blevel);
341                     addConflictClause(res.conflict);
342                     conflictCount++;
343 
344                     // リスタートが有効であり、conflict の回数が閾値に達していたならば、リスタートを実施します。
345                     if (restart && conflictCount == restartThreshold)
346                     {
347                         writefln("c restart. conflicted %d times", conflictCount);
348                         backtrack(0);
349                         restartThreshold = cast(long)(cast(double) restartThreshold * restartMult);
350                     }
351                 }
352                 else
353                     break;
354             }
355             decideNextBranch();
356         }
357     }
358 
359     /// 真偽値が未割り当ての変数を1つ選んで真を割り当てます。
360     void decideNextBranch()
361     {
362         // 現在のソルバーの状態を保存します。
363         history ~= new CDCLSolver(this);
364 
365         Literal lit = unassignedVariables.front;
366         // debug stderr.writefln("decision literal: %d", lit);
367         currentLevel++;
368         assignLiteral(lit);
369         decisionVariables ~= lit;
370         implicationGraph.decisionLiterals ~= lit;
371     }
372 
373     /++
374      + 単位伝播を繰り返します。
375      + 途中で矛盾が生じたら CONFLICT を返します。
376      + 矛盾なくすべての変数に真偽値を割り当てられたら SAT を返します。
377      + それ以外の場合には OK を返します。
378      +/
379     SolverStatus deduce()
380     {
381         while (!unitClauses.empty)
382         {
383             // debug stderr.writefln("unitClauses: %s", unitClauses);
384 
385             Clause.ID clsID = unitClauses.front;
386             unitClauses.removeKey(clsID);
387             Literal lit = clauses[clsID].unitLiteral;
388             if (iota(0, currentLevel + 1).map!(l => ImplicationGraph.Node(-lit, l))
389                     .any!(n => n in implicationGraph.nodes))
390             {
391                 assignLiteral(LAMBDA, lit);
392                 addEdge(-lit, LAMBDA, 0);
393                 addEdge(lit, LAMBDA, 0);
394 
395                 foreach (pred; this.originalClauses[clsID].literals.array.filter!(
396                         pred => pred != lit))
397                     addEdge(pred, lit, clsID);
398                 implicationGraph.transformToConflictGraph(currentLevel);
399                 return SolverStatus.CONFLICT;
400             }
401             assignLiteral(lit);
402             foreach (oclit; this.originalClauses[clsID].literals.array.filter!(
403                     oclit => oclit != lit))
404                 addEdge(oclit, lit, clsID);
405             toDOT(false);
406         }
407         if (unassignedVariables.empty)
408             return SolverStatus.SAT;
409         else
410             return SolverStatus.OK;
411     }
412 
413     alias analyzeConflictResult = Tuple!(long, "blevel", Clause, "conflict");
414     /++
415      + 矛盾を分析します。
416      + Returns: バックトラック先のレベルと学習節
417      +/
418     analyzeConflictResult analyzeConflict()
419     {
420         while (true)
421         {
422             if (currentLevel == 0)
423                 return analyzeConflictResult(-1, Clause(0, []));
424 
425             auto reasonNodes = implicationGraph.get1UIPCut();
426 
427             long blevel;
428             if (reasonNodes.array.length >= 2)
429                 blevel = reasonNodes.array
430                     .map!(node => node.dlevel)
431                     .array
432                     .sort!("a > b")[1];
433             else
434                 blevel = 0;
435 
436             // debug stderr.writefln("reasonNodes: %(%s %)",
437             // reasonNodes.array.map!(c => format("(%s, %s)", c[0], c[1])));
438 
439             auto learningLiterals = reasonNodes.array.map!(node => -node.literal).array;
440             Clause conflict = newClause(learningLiterals);
441             return analyzeConflictResult(blevel, conflict);
442         }
443     }
444 
445     /++
446      + 与えられたレベルまでバックトラックします。
447      + 具体的には、ソルバーの状態を与えられたレベルのときのソルバーの状態まで復元します。
448      +/
449     void backtrack(size_t dlevel)
450     {
451         dlevel = min(history.length - 1, dlevel);
452         // debug stderr.writefln("backtrack from %d to %d", currentLevel, dlevel);
453         // debug stderr.writefln("history length: %d, currentLevel: %d",
454         // this.history.length, currentLevel);
455         assert(this.history.length == currentLevel);
456         CDCLSolver oldSolver = this.history[dlevel];
457         this.history.length = dlevel;
458 
459         this.clauses = oldSolver.clauses;
460         this.unassignedVariables = oldSolver.unassignedVariables;
461         this.availClauses = oldSolver.availClauses;
462         this.decisionVariables = oldSolver.decisionVariables;
463         this.implicationGraph = oldSolver.implicationGraph;
464         this.currentLevel = oldSolver.currentLevel;
465 
466         this.unitClauses.clear();
467         availClauses.array
468             .filter!(clauseID => clauses[clauseID].isUnitClause)
469             .each!(clauseID => unitClauses.insert(clauseID));
470         // debug stderr.writefln("availClauses: %(%s, %)", availClauses.array.map!(id => clauses[id]));
471     }
472 
473     /++
474      + ソルバーの状態を、真偽値割り当てがなされていない初期状態にリセットします。
475      + 初期化時以外で追加された制約はそのまま追加された状態を維持します。
476      +/
477     void reset()
478     {
479         backtrack(0);
480     }
481 
482     /// 与えられたリテラルを含んだ新しい節を作成します。
483     Clause newClause(T...)(T literals)
484     {
485         return Clause(redBlackTree!Literal(literals));
486     }
487 
488     /// 与えられたリテラルの集合に対応する新しい節を作成します。
489     Clause newClause(T)(Set!T literals) if (isIntegral!T)
490     {
491         return Clause(literals);
492     }
493 
494     /// 与えられた節をソルバーの制約として追加します。
495     void addClause(Clause clause)
496     {
497         usedIDNum++;
498         clause.id = usedIDNum;
499 
500         assert(clause.id !in clauses);
501         originalClauses[clause.id] = Clause(clause);
502         availClauses.insert(clause.id);
503 
504         foreach (dlevel; 0 .. currentLevel)
505         {
506             history[dlevel].clauses[clause.id] = Clause(clause);
507             foreach (node; history[dlevel].implicationGraph.nodes)
508                 history[dlevel].clauses[clause.id].removeLiteral(-node.literal);
509             if (history[dlevel].clauses[clause.id].isUnitClause)
510                 history[dlevel].unitClauses.insert(clause.id);
511             history[dlevel].availClauses.insert(clause.id);
512         }
513         foreach (literal; clause.literals)
514             clausesContainingLiteral[literal].insert(clause.id);
515 
516         foreach (node; implicationGraph.nodes.array.filter!(node => node.literal != 0))
517             clause.removeLiteral(-node.literal);
518 
519         clauses[clause.id] = clause;
520         if (clause.isUnitClause)
521             unitClauses.insert(clause.id);
522     }
523 
524     /// 与えられた節を学習節として追加します。
525     void addConflictClause(Clause conflict)
526     {
527         // debug stderr.writefln("conflict clause: %s", conflict);
528         assert(!originalClauses.values.any!(c => c.literals == conflict.literals));
529         addClause(conflict);
530         // debug stderr.writefln("conflictClauseApplied: %s", conflict);
531     }
532 
533     /// 与えられたリテラルが真になるように変数への真偽値割り当てを行います。
534     void assignLiteral(T...)(T literals)
535     {
536         debug
537         {
538             // debug stderr.writefln("assignLiteral: %s", literals);
539             // debug stderr.writeln(unassignedVariables);
540             if (literals[0] != 0)
541                 assert(literals[0].abs in unassignedVariables);
542         }
543         foreach (lit; literals)
544         {
545             addNode(lit, this.currentLevel);
546             removeClausesContaining(lit);
547             removeLiteralFromClauses(-lit);
548             unassignedVariables.removeKey(abs(lit));
549         }
550     }
551 
552     void addNode(Literal lit, size_t dlevel)
553     {
554         // debug stderr.writefln("new node: %s, at level %s", lit, dlevel);
555         implicationGraph.nodes.insert(ImplicationGraph.Node(lit, dlevel));
556     }
557 
558     void addEdge(Literal from, Literal to, Clause.ID clauseID)
559     {
560         alias Node = ImplicationGraph.Node;
561 
562         // debug stderr.writefln("Add edge from %s to %s", -from, to);
563         Node fromNode = implicationGraph.getNode(-from), toNode = implicationGraph.getNode(to);
564         if (fromNode !in implicationGraph.successors)
565             implicationGraph.successors[fromNode] = redBlackTree!Node;
566         implicationGraph.successors[fromNode].insert(toNode);
567         if (toNode !in implicationGraph.predecessors)
568             implicationGraph.predecessors[toNode] = redBlackTree!Node;
569         implicationGraph.predecessors[toNode].insert(fromNode);
570         implicationGraph.edges[fromNode][toNode] = clauseID;
571 
572     }
573 
574     /// 与えられたリテラルを含む節を削除します。
575     void removeClausesContaining(Literal lit)
576     {
577         if (lit !in clausesContainingLiteral)
578             return;
579         foreach (clauseID; clausesContainingLiteral[lit].array.filter!(cid => cid in availClauses))
580         {
581             availClauses.removeKey(clauseID);
582             unitClauses.removeKey(clauseID);
583         }
584     }
585 
586     /// 与えられたリテラルをそれぞれの節の中から削除します。
587     void removeLiteralFromClauses(Literal lit)
588     {
589         if (lit !in clausesContainingLiteral)
590             return;
591         foreach (clauseID; clausesContainingLiteral[lit].array.filter!(cid => cid in availClauses))
592         {
593             if (!clauses[clauseID].isUnitClause)
594                 clauses[clauseID].removeLiteral(lit);
595             if (clauses[clauseID].isUnitClause)
596                 unitClauses.insert(clauseID);
597             if (clauses[clauseID].isEmptyClause)
598                 unitClauses.removeKey(clauseID);
599         }
600     }
601 
602     private int dotCounter;
603     /++
604      + implication graph の状態を DOT 言語で出力します。
605      + ファイル名は 1.dot, 2.dot, … のように続きます。
606      +/
607     void toDOT(bool conflict)
608     {
609         // if (conflict)
610         // debug stderr.writefln("conflict exported: %d", dotCounter);
611         if (!generateGraph && !generateAnotherGraph)
612             return;
613         if (implicationGraph.edges.length == 0)
614             return;
615 
616         string dotSource;
617         if (generateGraph)
618             dotSource = generateGraph1(conflict);
619         else
620             dotSource = generateGraph2(conflict);
621 
622         import std.file : write;
623 
624         write(format("%d.dot", dotCounter), dotSource);
625         dotCounter++;
626     }
627 
628     /// implication graph の定義に即した形のグラフを表す DOT 言語のソースを返します。
629     string generateGraph1(bool conflict)
630     {
631         string res = "digraph cdcl {\nnode[style=filled, fillcolor=white];\n";
632         res ~= "graph [layout = dot];\n";
633         if (conflict)
634             res ~= "\"0@%d\" [label = \"Λ\"];\n".format(currentLevel);
635         foreach (level, variable; decisionVariables)
636             res ~= format("\"%d@%d\" [shape = record, label = \"%d@%d\"];\n",
637                     variable, level + 1, variable, level + 1);
638 
639         foreach (from, tos; implicationGraph.edges)
640         {
641             foreach (to, clause; tos)
642             {
643                 res ~= format("\"%s@%d\" -> \"%s@%d\" [label = \"%s\"];\n", from.literal, from.dlevel,
644                         to.literal, to.dlevel, clause == 0 ? "" : (clause > preamble.clauses
645                             ? "L:" : "") ~ this.originalClauses[clause].toString);
646             }
647         }
648 
649         res ~= "}\n";
650         return res;
651     }
652 
653     /// implication graph を元に、単位伝播で用いられた節も頂点にしたグラフの DOT 言語のソースを返します。
654     string generateGraph2(bool conflict)
655     {
656         string label(string content, string color)
657         {
658             return "<font color=\"" ~ color ~ "\">" ~ content ~ "</font>";
659         }
660 
661         string generateClauseLabel(Clause.ID cl, Literal implicated)
662         {
663             if (cl == 0)
664                 return "conflict";
665 
666             auto literals = this.originalClauses[cl].literals.array;
667             Literal[] l, r;
668             auto tmp = literals.split(implicated);
669             l = tmp[0], r = tmp[1];
670             string ls = l.empty ? "" : format("%(%d ∨%) ∨ ", l);
671             string rs = r.empty ? "" : format(" ∨ %(%d ∨%)", r);
672             string res = "(%s%s%s)".format(ls, label(implicated.to!string, "black"), rs);
673             if (cl > preamble.clauses)
674                 return "L:" ~ res;
675             else
676                 return res;
677         }
678 
679         string res = "digraph cdcl {\nnode[style=filled, fillcolor=white];\n";
680         res ~= "graph [layout = dot, bgcolor=\"#eaeaea\"];\n";
681         if (conflict)
682             res ~= "\"0@%d\" [label = \"Λ\"];\n".format(currentLevel);
683         foreach (level, variable; decisionVariables)
684             res ~= format("\"%d@%d\" [shape = record, label = \"%d@%d\"];\n",
685                     variable, level + 1, variable, level + 1);
686 
687         alias T = Tuple!(Clause.ID, "clauseID", Literal, "implicated");
688         T[] clausesAndImplicateds;
689 
690         foreach (_, tos; implicationGraph.edges)
691             foreach (to, clause; tos)
692                 clausesAndImplicateds ~= T(clause, to.literal);
693 
694         foreach (p; clausesAndImplicateds)
695             if (p.clauseID != 0)
696                 res ~= format("\"cls%d\" [shape = invtrapezium, fontcolor=gray60, label=<%s>];\n",
697                         p.clauseID, generateClauseLabel(p.clauseID, p.implicated));
698 
699         auto clauseRendered = redBlackTree!(Clause.ID);
700 
701         foreach (from, tos; implicationGraph.edges)
702         {
703             foreach (to, clause; tos)
704             {
705                 if (clause == 0)
706                     res ~= format("\"%s@%d\" -> \"%s@%d\"\n", from.literal,
707                             from.dlevel, to.literal, to.dlevel);
708                 else
709                 {
710                     res ~= format("\"%d@%d\" -> \"cls%d\"\n", from.literal, from.dlevel, clause);
711                     if (clauseRendered.insert(clause))
712                         res ~= format("\"cls%d\" -> \"%d@%d\"\n", clause, to.literal, to.dlevel);
713                 }
714             }
715         }
716 
717         res ~= "}\n";
718         return res;
719     }
720 }