1 % (c) 2026-2026 Lehrstuhl fuer Softwaretechnik und Programmiersprachen,
2 % Heinrich Heine Universitaet Duesseldorf
3 % This software is licenced under EPL 1.0 (http://www.eclipse.org/org/documents/epl-v10.html)
4
5 :- module(dimacs_parser, [read_dimacs_cnf_file/2, convert_dimacs_cnf2b/2]).
6
7 :- use_module('../../src/module_information.pl').
8 :- module_info(group,satsolver).
9 :- module_info(description,'This is a parser for reading Dimacs .cnf files and converting them to B').
10
11 :- use_module(probsrc(error_manager),[add_error/3, add_internal_error/2, add_warning/3]).
12 :- use_module(probsrc(tools), [ajoin/2]).
13 :- use_module(library(lists), [nth1/3]).
14
15 blank_char(0' ).
16 blank_char(0'\t).
17 blank_char(0'\n).
18 blank_char(0'\r).
19
20 blank --> [C], { blank_char(C) }.
21
22 blanks --> blank, !, blanks.
23 blanks --> [].
24
25 blanks1 --> blank, blanks.
26
27 digit_code(C) --> [C], { C >= 0'0, C =< 0'9 }.
28
29 digits([C|Cs]) --> digit_code(C), !, digits(Cs).
30 digits([]) --> [].
31
32 digits1([C|Cs]) --> digit_code(C), digits(Cs).
33
34 signed_int(N) --> "-", !, digits1(Ds), { number_codes(N0, Ds), N is -N0 }.
35 signed_int(N) --> digits1(Ds), { number_codes(N, Ds) }.
36
37 % comment lines: "c" followed by anything up to end of line
38
39 ?comment_line --> "c", line_rest, opt_newline.
40
41 line_rest --> [C], { C \== 0'\n }, !, line_rest.
42 line_rest --> [].
43
44 opt_newline --> "\n".
45 opt_newline --> [].
46
47 % problem line: p cnf <vars> <clauses>
48
49 problem_line(NumVars, NumClauses) -->
50 "p", blanks1, "cnf", blanks1,
51 signed_int(NumVars), blanks1, signed_int(NumClauses).
52
53 % clauses: sequence of nonzero ints terminated by 0
54
55 cnf_clause(Lits) --> blanks, clause_literals(Lits).
56
57 clause_literals(Lits) -->
58 signed_int(N),
59 ( { N =:= 0 }
60 -> { Lits = [] }
61 ; { Lits = [N|Ls] }, blanks, clause_literals(Ls)
62 ).
63
64 % top-level sequence of clauses interleaved with comments
65
66 clause_section(Clauses) --> blanks, clause_section_(Clauses).
67
68 clause_section_(Clauses) --> comment_line, !, clause_section(Clauses).
69 clause_section_([Lits|Rest]) --> cnf_clause(Lits), !, clause_section(Rest).
70 clause_section_([]) --> [].
71
72 ?skip_comments --> blanks, comment_line, !, skip_comments.
73 skip_comments --> blanks.
74
75 % whole file
76
77 dimacs_cnf(cnf_encoding(NumVars, NumClauses, Clauses)) -->
78 skip_comments,
79 problem_line(NumVars, NumClauses),
80 clause_section(Clauses).
81
82 % I/O helpers
83
84 read_all_codes(Stream, Codes) :-
85 get_code(Stream, C),
86 ( C = -1
87 -> Codes = []
88 ; Codes = [C|Cs], read_all_codes(Stream, Cs)
89 ).
90
91 parse_dimacs_cnf(Codes, Term) :-
92 phrase(dimacs_cnf(Term), Codes),
93 validate_dimacs_cnf(Term).
94
95 :- use_module(probsrc(tools_io),[safe_intelligent_open_file/4]).
96 read_dimacs_cnf_file(FileName, Term) :-
97 safe_intelligent_open_file(FileName, read, Stream,[encoding(utf8)]),
98 call_cleanup(
99 ( read_all_codes(Stream, Codes),
100 parse_dimacs_cnf(Codes, Term)
101 ),
102 close(Stream)
103 ).
104
105 %
106 % check if number of propositional variables and number of clauses are correct
107 validate_dimacs_cnf(cnf_encoding(NumVars, NumClauses, Clauses)) :-
108 format('Read in CNF with ~w propositional variables and ~w clauses~n',[NumVars, NumClauses]),
109 length(Clauses, ActualClauses),
110 ( ActualClauses = NumClauses -> true
111 ; ajoin(['CNF header declares ',NumClauses,' but found: '],Msg),
112 add_warning(dimacs_parser,Msg,ActualClauses)
113 ),
114 ? ( nth1(ClauseNr,Clauses,Clause),
115 ? nth1(LitNr,Clause,Lit),
116 abs(Lit) > NumVars,
117 ajoin(['CNF literal ',LitNr,' in clause nr ',ClauseNr,' exceeds ',NumVars,':'],Msg),
118 add_warning(dimacs_parser,Msg,Lit),
119 fail
120 ; true
121 ).
122
123
124 :- use_module(probsrc(tools),[get_tail_filename/2,split_filename/3]).
125
126 % convert a .cnf file to a .mch file with the same root filename
127 convert_dimacs_cnf2b(FileName,BMachineFileName) :-
128 read_dimacs_cnf_file(FileName,Term),
129 get_tail_filename(FileName,Tail),
130 split_filename(Tail,MachName,_Ext),
131 split_filename(FileName,Base,_Extension),
132 atom_concat(Base,'.mch',BMachineFileName),
133 % TODO: check that file does not already exist or is @generated
134 safe_intelligent_open_file(BMachineFileName, write, Stream, [encoding(utf8)]),
135 call_cleanup(portray_cnf_as_b(Stream,MachName,Term),
136 close(Stream)).
137
138
139 :- use_module(library(system),[ datime/1]).
140 :- use_module(probsrc(version),[version_str/1]).
141 :- use_module(probsrc(tools_strings),[number_codes_min_length/3]).
142 :- use_module(probsrc(tools_strings),[is_simple_classical_b_identifier/1]).
143 portray_cnf_as_b(Stream,Name,cnf_encoding(NumVars,NumClauses,Clauses)) :-
144 format(Stream,' /*@generated */~n',[]),
145 (is_simple_classical_b_identifier(Name) % TODO: check id_requires_escaping
146 -> format(Stream,'MACHINE ~w~n',[Name])
147 ; format(Stream,'MACHINE `~w`~n',[Name])
148 ),
149 version_str(Str),datime(datime(Yr,Mon,Day,Hr,Min,_Sec)),
150 number_codes_min_length(Min,2,MC),
151 format(Stream,' /* Generated on ~w/~w/~w at ~w:~s using ProB version ~w */~n',[Day,Mon,Yr,Hr,MC,Str]),
152 format(Stream,'CONSTANTS~n',[]),
153 output_all_constants(Stream,1,NumVars), nl(Stream),
154 format(Stream,'DEFINITIONS~n',[]),
155 format(Stream,' SET_PREF_SOLVER_FOR_PROPERTIES == "sat";~n',[]),
156 (convert_as_array ->
157 % CUSTOM_GRAPH Encoding of Variable Incidence Graph
158 (NumClauses+NumVars > 600 -> ENG = sfdp ; ENG= fdp), % sfdp is faster, but fdp more useful
159 format(Stream,' SET_PREF_DOT_ENGINE == "~w";~n',[ENG]),
160 format(Stream,' CUSTOM_GRAPH_NODES1 == {(i).i:1..~w|rec(`id`:i, label:```x${i}```,color:IF x(i)=TRUE THEN "olivedrab2" ELSE "tomato" END,shape:"box",style:"rounded",penwidth:2)};~n',[NumVars]),
161 format(Stream,' CUSTOM_GRAPH_NODES2 == {(c).c:1..~w|rec(`id`:c+~w, label:```Clause ${c}```,color:"blue",shape:"box")};~n',[NumClauses,NumVars]),
162 format(Stream,' CUSTOM_GRAPH_EDGES1 == {(ci,c,i).ci|->c:CLAUSES & i:ran(c) & i>0 |rec(color:"black", style:IF x(i)=TRUE THEN "solid" ELSE "dotted" END, label:"+", dir:"none", penwidth:2,from:i,to:ci+~w)};~n',[NumVars]),
163 format(Stream,' CUSTOM_GRAPH_EDGES2 == {(ci,c,i).ci|->c:CLAUSES & i:ran(c) & i<0 |rec(color:"purple", style:IF x(-i)=FALSE THEN "solid" ELSE "dotted" END, label:"-", dir:"none", penwidth:2,from:-i,to:ci+~w)};~n',[NumVars]),
164
165 % VisB Encoding of clauses and literals
166 % does not work well with large number of small clauses
167 format(Stream,' WIDTH==6; HEIGHT==4;WIDTH1==WIDTH+1;HEIGHT1==HEIGHT+1;~n',[]),
168 format(Stream,' MAXCLAUSESIZE == max({(i).i:dom(CLAUSES)|size(CLAUSES(i))});~n',[]),
169 format(Stream,' VISB_SVG_BOX == rec(width:WIDTH1*MAXCLAUSESIZE,height:1+HEIGHT1*size(CLAUSES));~n',[]),
170 format(Stream,' VISB_SVG_OBJECTS == {(i,j) . i:dom(CLAUSES) & j:dom(CLAUSES(i)) | VISB_LITERAL(i,j,CLAUSES(i)(j))};~n',[]),
171 format(Stream,' VISB_LITERAL(i,j,lit) == (rec(svg_class:"rect",rx:1,width:WIDTH,height:HEIGHT,~n',[]),
172 format(Stream,' fill:IF lit<0 & x(-lit)=FALSE THEN "lightgreen" ELSIF lit>0 & x(lit)=TRUE THEN "palegreen" ELSE "burlywood" END,~n',[]),
173 format(Stream,' stroke:"gray", hovers:rec(stroke:"black"), `stroke-width`:0.2,~n',[]),
174 format(Stream,' x:(j-1)*WIDTH1, y:1+(i-1)*HEIGHT1,title:```Literal ${j} in clause ${i}```),~n',[]),
175 format(Stream,' rec(svg_class:"text",x:(j-1)*WIDTH1+WIDTH/2,y:1+(i-1)*HEIGHT1+HEIGHT/2,~n',[]),
176 format(Stream,' `font-size`:2.0,`alignment-baseline`:"middle",`text-anchor`:"middle",~n',[]),
177 format(Stream,' text:IF lit<0 THEN ```\xAC\x${-lit}``` ELSE ```x${lit}```END));~n',[])
178 % see unicode_translation(negation,'\xAC\').
179 ; true),
180 % Clause representation as a DEFINITION:
181 format(Stream,' CLAUSES ==~n',[]),
182 output_clauses_as_b_list(Clauses,Stream), % output clauses for visualisations
183 format(Stream,'PROPERTIES~n',[]),
184 output_typing(Stream,1,NumVars),
185 output_clauses_as_b_pred(Clauses,Stream),
186 format(Stream,'END~n',[]).
187
188 output_clauses_as_b_list([],Stream) :- format(Stream,' []~n',[]).
189 output_clauses_as_b_list(Clauses,Stream) :-
190 format(Stream,' [~n',[]),
191 output_clauses_as_b_list2(Clauses,Stream),
192 format(Stream,' ]~n',[]).
193
194 output_clauses_as_b_list2([Clause|TC],Stream) :-
195 format(Stream,' ',[]),
196 format(Stream,'~w',[Clause]), % Prolog list is also valid B list
197 (TC=[] -> format(Stream,'~n',[])
198 ; format(Stream,',~n',[]),
199 output_clauses_as_b_list2(TC,Stream)
200 ).
201
202 output_clauses_as_b_pred([],_Stream).
203 output_clauses_as_b_pred([Clause|TC],Stream) :-
204 format(Stream,' (',[]),
205 output_clause(Clause,Stream),
206 (TC=[] -> format(Stream,')~n',[])
207 ; format(Stream,') &~n',[]),
208 output_clauses_as_b_pred(TC,Stream)
209 ).
210
211 output_clause([],Stream) :- nl(Stream).
212 output_clause([Lit|TC],Stream) :-
213 output_lit(Lit,Stream),
214 (TC=[] -> true
215 ; format(Stream,' or ',[]),
216 output_clause(TC,Stream)
217 ).
218
219 output_lit(Lit,Stream) :- Lit<0, !, L is -Lit,
220 format_id(Stream,L),format(Stream,' = FALSE',[]).
221 output_lit(Lit,Stream) :-
222 format_id(Stream,Lit),format(Stream,' = TRUE ',[]).
223
224 :- dynamic convert_as_array/0.
225 convert_as_array. % takes longer to convert to CNF later by ProB, but easier to manipulate, e.g., x~[{TRUE}]
226
227 format_id(Stream,Nr) :- convert_as_array,!,format(Stream,'x(~w)',[Nr]).
228 format_id(Stream,Nr) :- format(Stream,'x~w',[Nr]).
229
230 output_typing(Stream,From,To) :- convert_as_array,!,
231 format(Stream,' x:~w..~w --> BOOL &~n',[From,To]).
232 output_typing(Stream,From,_To) :-
233 format(Stream,' x~w : BOOL &~n',[From]). % TODO: add other if necessary
234
235 output_all_constants(Stream,_From,_To) :- convert_as_array,!,
236 format(Stream,' x',[]).
237 output_all_constants(Stream,From,To) :-
238 output_ids(Stream,From,To).
239 output_ids(Stream,From,To) :-
240 format(Stream,' ',[]), format_id(Stream,From),
241 F1 is From+1,
242 (F1>To -> true
243 ; format(Stream,',~n',[]),output_ids(Stream,F1,To)).