| 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)). |