| 1 | % (c) 2009-2024 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(uml_generator,[write_uml_sequence_chart_all_states/1, | |
| 6 | write_uml_sequence_chart/1]). | |
| 7 | ||
| 8 | :- use_module(probsrc(module_information)). | |
| 9 | :- module_info(group,dot). | |
| 10 | :- module_info(description,'This module generates UML diagrams for PlantUML.'). | |
| 11 | ||
| 12 | :- use_module(library(lists)). | |
| 13 | ||
| 14 | :- use_module(probsrc(specfile)). | |
| 15 | :- use_module(probsrc(tcltk_interface)). | |
| 16 | :- use_module(probsrc(state_space), [transition/4, op_trace_ids/1, visited_expression/2]). | |
| 17 | :- use_module(probsrc(bmachine), [b_definition_prefixed/5, b_get_definition/5, type_with_errors/4]). | |
| 18 | :- use_module(probsrc(debug), [debug_println/2]). | |
| 19 | :- use_module(probsrc(translate), [translate_bvalue/2, translate_event_with_limit/3]). | |
| 20 | :- use_module(probsrc(eval_let_store),[extend_typing_scope_for_stored_lets/2]). | |
| 21 | :- use_module(probsrc(error_manager),[add_internal_error/2, add_error/3, add_message/4, add_warning/4]). | |
| 22 | :- use_module(probsrc(self_check)). | |
| 23 | ||
| 24 | ||
| 25 | % generate UML sequence chart from history for use with PlantUML | |
| 26 | write_uml_sequence_chart_all_states(Files) :- | |
| 27 | op_trace_ids(TIDs), | |
| 28 | length(Files, FLen), | |
| 29 | length(TIDs, TLen), | |
| 30 | (FLen == TLen -> true | |
| 31 | ; add_internal_error('Number of files does not match trace length.',write_uml_sequence_chart_all_states(Files))), | |
| 32 | write_uml_sequence_chart_all_states(Files,TIDs). | |
| 33 | write_uml_sequence_chart_all_states([],[]). | |
| 34 | write_uml_sequence_chart_all_states([File|T],[ID|IDT]) :- | |
| 35 | write_uml_sequence_chart(File,[ID|IDT]), | |
| 36 | write_uml_sequence_chart_all_states(T,IDT). | |
| 37 | ||
| 38 | write_uml_sequence_chart(File) :- | |
| 39 | op_trace_ids(TIDs), | |
| 40 | write_uml_sequence_chart(File,TIDs). | |
| 41 | write_uml_sequence_chart(File,TIDs) :- | |
| 42 | open(File,write,Stream), | |
| 43 | call_cleanup(print_uml_sequence_chart_aux(TIDs,Stream), | |
| 44 | close(Stream)). | |
| 45 | ||
| 46 | print_uml_sequence_chart_aux(TIDs,Stream) :- | |
| 47 | get_history_transition_terms(TIDs,Terms), | |
| 48 | format(Stream,'@startuml~n',[]), | |
| 49 | % format(Stream,'autonumber~n',[]), | |
| 50 | (last(transition_from(StateId,_),Terms) -> true ; StateId=root), | |
| 51 | print_uml_actors(Stream,StateId), | |
| 52 | maplist(print_seq_chart_transition(Stream),Terms), | |
| 53 | format(Stream,'@enduml~n',[]). | |
| 54 | ||
| 55 | :- use_module(probsrc(bmachine),[b_get_typed_expression_definition/3]). | |
| 56 | :- use_module(probsrc(custom_explicit_sets),[expand_custom_set_to_list/4]). | |
| 57 | :- use_module(probsrc(bsyntaxtree), [get_texpr_pos/2]). | |
| 58 | :- use_module(probsrc(state_space),[visited_expression/2]). | |
| 59 | :- use_module(probsrc(specfile),[expand_const_and_vars_to_full_store/2]). | |
| 60 | ||
| 61 | print_uml_actors(Stream,StateId) :- | |
| 62 | b_get_typed_expression_definition('SEQUENCE_CHART_ACTORS',[variables_and_additional_defs],AExpr), | |
| 63 | % should be constants instead of variables; TODO: evaluate in StateId | |
| 64 | get_texpr_pos(AExpr,Pos), | |
| 65 | visited_expression(StateId,State),expand_const_and_vars_to_full_store(State,BState), | |
| 66 | b_interpreter:b_compute_expression_nowf(AExpr,[],BState,Val,'SEQUENCE_CHART_ACTORS',0), | |
| 67 | expand_custom_set_to_list(Val,ExpandedVal,_Done,'SEQUENCE_CHART_ACTORS'), | |
| 68 | if(get_actor_fields(Fields,ExpandedVal), | |
| 69 | print_uml_actor(Stream,Fields,Pos), | |
| 70 | add_warning(uml_generator,'SEQUENCE_CHART_ACTORS should return set of records {rec(name:"N",kind:"actor",color:"green")}, not: ',Val,Pos)), | |
| 71 | fail. | |
| 72 | print_uml_actors(_,_). | |
| 73 | ||
| 74 | get_actor_fields(Fields,ExpandedVal) :- | |
| 75 | member(rec(Fields),ExpandedVal). | |
| 76 | get_actor_fields(Fields,ExpandedVal) :- | |
| 77 | member((int(_),rec(Fields)),ExpandedVal). % we can influence the order of actors by using a sequence | |
| 78 | ||
| 79 | :- use_module(probsrc(kernel_strings),[to_b_string/2]). | |
| 80 | print_uml_actor(Stream,Fields,Pos) :- | |
| 81 | (member(field(id,NN),Fields) -> true ; member(field(name,NN),Fields) -> true | |
| 82 | ; add_warning(uml_generator,'Missing name field: ',Kind,Pos), fail), | |
| 83 | to_b_string(NN,string(Name)), | |
| 84 | (member(field(color,CC),Fields) -> to_b_string(CC,string(Color)) ; Color=''), | |
| 85 | (member(field(kind,KK),Fields) | |
| 86 | -> to_b_string(KK,string(Kind)), | |
| 87 | (actor_kind(Kind) -> true ; add_warning(uml_generator,'Unknown actor kind: ',Kind,Pos)) | |
| 88 | ; Kind='participant'), | |
| 89 | (Color='' -> format(Stream,'~w ~w~n',[Kind,Name]) | |
| 90 | ; format(Stream,'~w ~w #~w~n',[Kind,Name,Color])). | |
| 91 | ||
| 92 | actor_kind(actor). | |
| 93 | actor_kind(boundary). | |
| 94 | actor_kind(control). | |
| 95 | actor_kind(entity). | |
| 96 | actor_kind(database). | |
| 97 | actor_kind(collections). | |
| 98 | actor_kind(queue). | |
| 99 | ||
| 100 | ||
| 101 | % try to find user-provided SEQUENCE_CHART definition for event, indicating the labels and arrow kind | |
| 102 | print_seq_chart_transition(Stream,transition_from(CurStateID,Term)) :- | |
| 103 | specfile:get_operation_name(Term,OpName), | |
| 104 | b_definition_prefixed(expression,'SEQUENCE_CHART_',OpName,DefName,_Pos), | |
| 105 | debug_println(19,found_def(DefName)), | |
| 106 | get_operation_arguments(Term,OpArgs), | |
| 107 | b_get_definition(DefName,expression,DefArgs,RawSubst,_Deps), | |
| 108 | arg(1,RawSubst,Pos), | |
| 109 | length(DefArgs,DefNrArgs), | |
| 110 | maplist(gen_raw_arg(Pos),OpArgs,RawArgs), | |
| 111 | (prefix_length(RawArgs,DefRawArgs,DefNrArgs) | |
| 112 | -> true % we allow the DEFINITION to take fewer arguments; possibly useful when ANY variables lifted to args | |
| 113 | ; add_warning(write_uml_sequence_chart,'DEFINITION has too many arguments: ',DefName/DefNrArgs,Pos), | |
| 114 | fail | |
| 115 | ), | |
| 116 | extend_typing_scope_for_stored_lets([prob_ids(visible),variables_and_additional_defs],Scope), | |
| 117 | %add_message(seq_chart,'Typing DEFINITION for UML Sequence Chart: ',DefName,Pos), | |
| 118 | type_with_errors(definition(Pos,DefName,DefRawArgs),Scope,_Type,TypedExpr), | |
| 119 | visited_expression(CurStateID,State), | |
| 120 | (state_corresponds_to_set_up_constants(State,BState) -> true | |
| 121 | ; BState=[] | |
| 122 | ), | |
| 123 | tcltk_interface:b_compute_expression_with_prob_ids(TypedExpr,BState,Value), | |
| 124 | process_uml_bvalue(Value,Term,DefName,Pos,Stream), | |
| 125 | !. | |
| 126 | print_seq_chart_transition(Stream,transition_from(_,Term)) :- | |
| 127 | specfile:get_operation_name(Term,OpName), | |
| 128 | specfile:get_operation_arguments(Term,OpArgs), length(OpArgs,Len), | |
| 129 | translate_event_with_limit(Term,100,TS), | |
| 130 | format('No SEQUENCE_CHART_~w/~w DEFINITION found for transition ~w~n',[OpName,Len,TS]), | |
| 131 | format(Stream,'main --> main: ~w~n',[TS]). | |
| 132 | ||
| 133 | % we support ("ActorFrom","ActorTo") | |
| 134 | % and ("ActorFrom","ArrowStyle","ActorTo") which corresponds to (("ActorFrom","ArrowStyle"),"ActorTo") | |
| 135 | % and ("ActorFrom","ArrowStyle","ArrowSuffix","ActorTo") | |
| 136 | % and rec(from:AF, to:AT, arrow:"-->", suffix: "") | |
| 137 | process_uml_bvalue(rec(Fields),OperationTerm,DefName,Pos,Stream) :- | |
| 138 | (select(field(from,ActorFrom),Fields,Fields1) | |
| 139 | -> translate_bvalue(ActorFrom,AF) | |
| 140 | ; add_message(uml_generator,'Record in definition has no \'from\' field: ',DefName,Pos), | |
| 141 | AF = '??', Fields1=Fields), | |
| 142 | (select(field(to,ActorTo),Fields1,Fields2) -> translate_bvalue(ActorTo,AT) | |
| 143 | ; add_message(uml_generator,'Record in definition has no \'to\' field: ',DefName,Pos), | |
| 144 | AT=AF, Fields2=Fields1), | |
| 145 | (member(field(arrow,string(ArrowStyle)),Fields2) | |
| 146 | -> (valid_plant_uml_arrow_style(ArrowStyle,Pos) -> true | |
| 147 | ; add_message(uml_generator,'Unrecognised arrow field: ',ArrowStyle,Pos) | |
| 148 | ) | |
| 149 | ; ArrowStyle='-->'), | |
| 150 | (member(field(suffix,SuffixVal),Fields2) | |
| 151 | -> to_b_string(SuffixVal,string(StrSuffix)), | |
| 152 | check_valid_plant_uml_arrow_suffix(StrSuffix,Suffix,Pos) | |
| 153 | ; Suffix=''), | |
| 154 | translate_event_with_limit(OperationTerm,100,TS), !, | |
| 155 | format(Stream,'~w ~w ~w ~w : ~w~n',[AF,ArrowStyle,AT, Suffix,TS]). | |
| 156 | process_uml_bvalue((From,ActorTo),OperationTerm,_,Pos,Stream) :- | |
| 157 | get_arrow_style(From,ActorFrom,ArrowStyle,Suffix,Pos), | |
| 158 | translate_bvalue(ActorFrom,AF), translate_bvalue(ActorTo,AT), | |
| 159 | translate_event_with_limit(OperationTerm,100,TS), !, | |
| 160 | format(Stream,'~w ~w ~w ~w : ~w~n',[AF,ArrowStyle,AT, Suffix,TS]). | |
| 161 | process_uml_bvalue(string(''),_,_,_,_Stream) :- !. % ignore this event | |
| 162 | process_uml_bvalue(string(Str),_,_,_,Stream) :- | |
| 163 | !, | |
| 164 | format(Stream, '~w~n', [Str]). % raw print, user has specified the whole line | |
| 165 | process_uml_bvalue(Value,_,_DefName,Pos,_) :- | |
| 166 | add_warning(write_uml_sequence_chart,'Unexpected value for UML sequence chart: ',Value,Pos), | |
| 167 | fail. | |
| 168 | ||
| 169 | ||
| 170 | get_arrow_style(((From,string(Arrow)),string(StrSuffix)),From,Arrow,Suffix,Pos) :- | |
| 171 | valid_plant_uml_arrow_style(Arrow,Pos), | |
| 172 | check_valid_plant_uml_arrow_suffix(StrSuffix,Suffix,Pos),!. | |
| 173 | get_arrow_style((From,string(Arrow)),From,Arrow,'',Pos) :- valid_plant_uml_arrow_style(Arrow,Pos),!. | |
| 174 | get_arrow_style(From,From,'-->','',_). | |
| 175 | ||
| 176 | ||
| 177 | % see https://plantuml.com/sequence-diagram | |
| 178 | valid_plant_uml_arrow_suffix(nothing,''). | |
| 179 | valid_plant_uml_arrow_suffix('',''). % nothing | |
| 180 | valid_plant_uml_arrow_suffix(activate,'++'). | |
| 181 | valid_plant_uml_arrow_suffix('++','++'). % activate the target; TO DO: can be followed by color | |
| 182 | valid_plant_uml_arrow_suffix(deactivate,'--'). | |
| 183 | valid_plant_uml_arrow_suffix('--','--'). % deactivate source | |
| 184 | valid_plant_uml_arrow_suffix(create,'**'). | |
| 185 | valid_plant_uml_arrow_suffix('**','**'). % Create an instance of the target | |
| 186 | valid_plant_uml_arrow_suffix(destroy,'!!'). | |
| 187 | valid_plant_uml_arrow_suffix('!!','!!'). % Destroy an instance of the target | |
| 188 | ||
| 189 | ||
| 190 | check_valid_plant_uml_arrow_suffix(Arrow,Res,_) :- valid_plant_uml_arrow_suffix(Arrow,R),!, Res=R. | |
| 191 | check_valid_plant_uml_arrow_suffix(Arrow,Res,Pos) :- | |
| 192 | add_message(write_uml_sequence_chart,'Unrecognized plantUML arrow suffix (use nothing,++,--,**,!!): ',Arrow,Pos), | |
| 193 | Res=Arrow. | |
| 194 | ||
| 195 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#green]>',unknown) ). | |
| 196 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#green]->',unknown) ). | |
| 197 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#blue]>',unknown) ). | |
| 198 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#red]>',unknown) ). | |
| 199 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#0000FF]>',unknown) ). | |
| 200 | ||
| 201 | % add a final x to denote a lost message | |
| 202 | % use \ or / instead of < or > to have only the bottom or top part of the arrow | |
| 203 | % repeat the arrow head (for example, >> or //) head to have a thin drawing | |
| 204 | % use -- instead of - to have a dotted arrow | |
| 205 | % add a final "o" at arrow head | |
| 206 | % use bidirectional arrow <-> | |
| 207 | valid_plant_uml_arrow_style('-->',_). | |
| 208 | valid_plant_uml_arrow_style('->',_). | |
| 209 | valid_plant_uml_arrow_style('->x',_). | |
| 210 | valid_plant_uml_arrow_style('->>',_). | |
| 211 | valid_plant_uml_arrow_style('-\\',_). | |
| 212 | valid_plant_uml_arrow_style('\\\\-',_). | |
| 213 | valid_plant_uml_arrow_style('//--',_). | |
| 214 | valid_plant_uml_arrow_style('->o',_). | |
| 215 | valid_plant_uml_arrow_style('o\\\\--',_). | |
| 216 | valid_plant_uml_arrow_style('<->',_). | |
| 217 | valid_plant_uml_arrow_style('<->o',_). | |
| 218 | valid_plant_uml_arrow_style('<-',_). | |
| 219 | valid_plant_uml_arrow_style('<--',_). % TO DO: other reverse arrows | |
| 220 | % TODO: things like o<<--[#green]>o are also allowed | |
| 221 | valid_plant_uml_arrow_style(Arrow,Pos) :- atom_codes(Arrow,Codes), | |
| 222 | % detect patterns like '-[#green]->' | |
| 223 | Codes = [0'-, 0'[ | RestCodes], | |
| 224 | append(ColorCodes,[0']|Rest],RestCodes), | |
| 225 | valid_end_arrow_codes(Rest),!, | |
| 226 | (valid_plant_uml_color_codes(ColorCodes) -> true | |
| 227 | ; add_message(write_uml_sequence_chart,'Unrecognized plantUML color in arrow: ',Arrow,Pos)). | |
| 228 | valid_plant_uml_arrow_style(Arrow,Pos) :- | |
| 229 | add_message(write_uml_sequence_chart,'Unrecognized plantUML arrow: ',Arrow,Pos). | |
| 230 | ||
| 231 | valid_end_arrow_codes(">"). | |
| 232 | valid_end_arrow_codes("->"). | |
| 233 | ||
| 234 | % from https://plantuml.com/color | |
| 235 | % You can use specify fill and line colors either: | |
| 236 | % with its standard name or CSS name | |
| 237 | % using HEX value (6 digits): #RRGGBB | |
| 238 | % using HEX value (8 digits) with alpha compositing or RGBA color model: #RRGGBBaa | |
| 239 | % using short HEX value (3 digits): #RGB (so #ABC means #AABBCC) | |
| 240 | % using very short HEX value (1 digits): #x which is a shortcut for #xxxxxx (so you get some gray) | |
| 241 | % A special color values: transparent can be used, synonym of transparent black (#00000000). | |
| 242 | % see also https://www.w3schools.com/colors/colors_names.asp | |
| 243 | % TODO: support gradients | |
| 244 | ||
| 245 | :- use_module(probsrc(tools_matching),[is_svg_color_name/1]). | |
| 246 | valid_plant_uml_color_codes([0'#|TC]) :- % accept things like #green | |
| 247 | atom_codes(Color,TC), | |
| 248 | (maplist(hex_code,TC) -> true %we do not check length is 1,3,6,8 | |
| 249 | ; is_svg_color_name(Color) | |
| 250 | ; Color = transparent). | |
| 251 | ||
| 252 | hex_code(C) :- C >= 48, C =< 57, !. | |
| 253 | hex_code(C) :- C >= 97, C =< 102, !. | |
| 254 | hex_code(C) :- C >= 65, C =< 70, !. | |
| 255 | ||
| 256 | ||
| 257 | % gen a raw value argument to pass to a DEFINITION call | |
| 258 | gen_raw_arg(Pos,Val,value(Pos,Val)). | |
| 259 | ||
| 260 | ||
| 261 | % get current history as transition terms with starting state id: | |
| 262 | get_history_transition_terms(IDT,Terms) :- | |
| 263 | reverse(IDT,RIDT), | |
| 264 | get_transition_terms(RIDT,root,Terms). | |
| 265 | ||
| 266 | get_transition_terms([],_CurrentState,[]). | |
| 267 | get_transition_terms([TransId|Rest],CurID,[transition_from(CurID,Term)|TRes]) :- | |
| 268 | transition(CurID,Term,TransId,DestID),!, | |
| 269 | get_transition_terms(Rest,DestID,TRes). | |
| 270 | get_transition_terms([TransId|_],CurrentState,_) :- | |
| 271 | add_error(get_transition_terms,'Could not execute transition id: ', TransId:from(CurrentState)),fail. |