| 1 | /* |
| 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
| 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 5 | * |
| 6 | * Copyright 1997 - July 2008 CWI, August 2008 - 2019 MonetDB B.V. |
| 7 | */ |
| 8 | |
| 9 | /* |
| 10 | * The first attempt of the multiplex optimizer is to locate |
| 11 | * a properly typed multi-plexed implementation. |
| 12 | * The policy is to search for bat<mod>.<fcn> before going |
| 13 | * into the iterator code generation. |
| 14 | */ |
| 15 | #include "monetdb_config.h" |
| 16 | #include "opt_remap.h" |
| 17 | #include "opt_macro.h" |
| 18 | #include "opt_multiplex.h" |
| 19 | |
| 20 | static int |
| 21 | OPTremapDirect(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci, Module scope){ |
| 22 | str mod,fcn; |
| 23 | char buf[1024]; |
| 24 | int i, retc = pci->retc; |
| 25 | InstrPtr p; |
| 26 | str bufName, fcnName; |
| 27 | |
| 28 | (void) cntxt; |
| 29 | (void) stk; |
| 30 | mod = VALget(&getVar(mb, getArg(pci, retc+0))->value); |
| 31 | fcn = VALget(&getVar(mb, getArg(pci, retc+1))->value); |
| 32 | |
| 33 | if(strncmp(mod,"bat" ,3)==0) |
| 34 | mod+=3; |
| 35 | |
| 36 | if( OPTdebug & OPTremap){ |
| 37 | fprintf(stderr,"#Found a candidate %s.%s\n" ,mod,fcn); |
| 38 | } |
| 39 | |
| 40 | |
| 41 | snprintf(buf,1024,"bat%s" ,mod); |
| 42 | bufName = putName(buf); |
| 43 | fcnName = putName(fcn); |
| 44 | if(bufName == NULL || fcnName == NULL) |
| 45 | return 0; |
| 46 | |
| 47 | p= newInstruction(mb, bufName, fcnName); |
| 48 | |
| 49 | for(i=0; i<pci->retc; i++) |
| 50 | if (i<1) |
| 51 | getArg(p,i) = getArg(pci,i); |
| 52 | else |
| 53 | p = pushReturn(mb, p, getArg(pci,i)); |
| 54 | p->retc= p->argc= pci->retc; |
| 55 | for(i= pci->retc+2; i<pci->argc; i++) |
| 56 | p= pushArgument(mb,p,getArg(pci,i)); |
| 57 | |
| 58 | if( OPTdebug & OPTremap){ |
| 59 | fprintInstruction(stderr,mb,0,p,LIST_MAL_ALL); |
| 60 | } |
| 61 | |
| 62 | /* now see if we can resolve the instruction */ |
| 63 | typeChecker(scope,mb,p,TRUE); |
| 64 | if( p->typechk== TYPE_UNKNOWN) { |
| 65 | |
| 66 | if( OPTdebug & OPTremap){ |
| 67 | fprintf(stderr,"#type error\n" ); |
| 68 | fprintInstruction(stderr,mb,0,p,LIST_MAL_ALL); |
| 69 | } |
| 70 | |
| 71 | freeInstruction(p); |
| 72 | return 0; |
| 73 | } |
| 74 | pushInstruction(mb,p); |
| 75 | if( OPTdebug & OPTremap){ |
| 76 | fprintf(stderr,"success\n" ); |
| 77 | } |
| 78 | return 1; |
| 79 | } |
| 80 | |
| 81 | /* |
| 82 | * Multiplex inline functions should be done with care. |
| 83 | * The approach taken is to make a temporary copy of the function to be inlined. |
| 84 | * To change all the statements to reflect the new situation |
| 85 | * and, if no error occurs, replaces the target instruction |
| 86 | * with this new block. |
| 87 | * |
| 88 | * By the time we get here, we know that function is |
| 89 | * side-effect free. |
| 90 | * |
| 91 | * The multiplex upgrade is targeted at all function |
| 92 | * arguments whose actual is a BAT and its formal |
| 93 | * is a scalar. |
| 94 | * This seems sufficient for the SQL generated PSM code, |
| 95 | * but does in general not hold. |
| 96 | * For example, |
| 97 | * |
| 98 | * function foo(b:int,c:bat[:oid,:int]) |
| 99 | * ... d:= batcalc.+(b,c) |
| 100 | * and |
| 101 | * multiplex("user","foo",ba:bat[:oid,:int],ca:bat[:oid,:int]) |
| 102 | * upgrades the first argument. The naive upgrade of |
| 103 | * the statement that would fail. The code below catches |
| 104 | * most of them by simple prepending "bat" to the MAL function |
| 105 | * name and leave it to the type resolver to generate the |
| 106 | * error. |
| 107 | * |
| 108 | * The process terminates as soon as we |
| 109 | * find an instruction that does not have a multiplex |
| 110 | * counterpart. |
| 111 | */ |
| 112 | static int |
| 113 | OPTmultiplexInline(Client cntxt, MalBlkPtr mb, InstrPtr p, int pc ) |
| 114 | { |
| 115 | MalBlkPtr mq; |
| 116 | InstrPtr q = NULL, sig; |
| 117 | char buf[1024]; |
| 118 | int i,j,k, actions=0; |
| 119 | int refbat=0, retc = p->retc; |
| 120 | bit *upgrade; |
| 121 | Symbol s; |
| 122 | str msg; |
| 123 | |
| 124 | |
| 125 | s= findSymbol(cntxt->usermodule, |
| 126 | VALget(&getVar(mb, getArg(p, retc+0))->value), |
| 127 | VALget(&getVar(mb, getArg(p, retc+1))->value)); |
| 128 | |
| 129 | if( s== NULL || !isSideEffectFree(s->def) || |
| 130 | getInstrPtr(s->def,0)->retc != p->retc ) { |
| 131 | |
| 132 | if( OPTdebug & OPTremap){ |
| 133 | if( s== NULL) |
| 134 | fprintf(stderr,"#not found \n" ); |
| 135 | else |
| 136 | fprintf(stderr,"#side-effects\n" ); |
| 137 | } |
| 138 | |
| 139 | return 0; |
| 140 | } |
| 141 | /* |
| 142 | * Determine the variables to be upgraded and adjust their type |
| 143 | */ |
| 144 | if((mq = copyMalBlk(s->def)) == NULL) { |
| 145 | return 0; |
| 146 | } |
| 147 | sig= getInstrPtr(mq,0); |
| 148 | |
| 149 | if( OPTdebug & OPTremap){ |
| 150 | fprintf(stderr,"#Modify the code\n" ); |
| 151 | fprintFunction(stderr,mq, 0, LIST_MAL_ALL); |
| 152 | fprintInstruction(stderr,mb, 0, p,LIST_MAL_ALL); |
| 153 | } |
| 154 | |
| 155 | |
| 156 | upgrade = (bit*) GDKzalloc(sizeof(bit)*mq->vtop); |
| 157 | if( upgrade == NULL) { |
| 158 | freeMalBlk(mq); |
| 159 | return 0; |
| 160 | } |
| 161 | |
| 162 | setVarType(mq, 0,newBatType(getArgType(mb,p,0))); |
| 163 | clrVarFixed(mq,getArg(getInstrPtr(mq,0),0)); /* for typing */ |
| 164 | upgrade[getArg(getInstrPtr(mq,0),0)] = TRUE; |
| 165 | |
| 166 | for(i=3; i<p->argc; i++){ |
| 167 | if( !isaBatType( getArgType(mq,sig,i-2)) && |
| 168 | isaBatType( getArgType(mb,p,i)) ){ |
| 169 | |
| 170 | if( getBatType(getArgType(mb,p,i)) != getArgType(mq,sig,i-2)){ |
| 171 | |
| 172 | if( OPTdebug & OPTremap){ |
| 173 | fprintf(stderr,"#Type mismatch %d\n" ,i); |
| 174 | } |
| 175 | |
| 176 | goto terminateMX; |
| 177 | } |
| 178 | if( OPTdebug & OPTremap){ |
| 179 | fprintf(stderr,"#Upgrade type %d %d\n" ,i, getArg(sig,i-2)); |
| 180 | } |
| 181 | |
| 182 | setVarType(mq, i-2,newBatType(getArgType(mb,p,i))); |
| 183 | upgrade[getArg(sig,i-2)]= TRUE; |
| 184 | refbat= getArg(sig,i-2); |
| 185 | } |
| 186 | } |
| 187 | /* |
| 188 | * The next step is to check each instruction of the |
| 189 | * to-be-inlined function for arguments that require |
| 190 | * an upgrade and resolve it afterwards. |
| 191 | */ |
| 192 | for(i=1; i<mq->stop; i++) { |
| 193 | int fnd = 0; |
| 194 | |
| 195 | q = getInstrPtr(mq,i); |
| 196 | if (q->token == ENDsymbol) |
| 197 | break; |
| 198 | for(j=0; j<q->argc && !fnd; j++) |
| 199 | if (upgrade[getArg(q,j)]) { |
| 200 | for(k=0; k<q->retc; k++){ |
| 201 | setVarType(mq,getArg(q,j),newBatType(getArgType(mq, q, j))); |
| 202 | /* for typing */ |
| 203 | clrVarFixed(mq,getArg(q,k)); |
| 204 | if (!upgrade[getArg(q,k)]) { |
| 205 | upgrade[getArg(q,k)]= TRUE; |
| 206 | /* lets restart */ |
| 207 | i = 0; |
| 208 | } |
| 209 | } |
| 210 | fnd = 1; |
| 211 | } |
| 212 | /* nil:type -> nil:bat[:oid,:type] */ |
| 213 | if (!getModuleId(q) && q->token == ASSIGNsymbol && |
| 214 | q->argc == 2 && isVarConstant(mq, getArg(q,1)) && |
| 215 | upgrade[getArg(q,0)] && |
| 216 | getArgType(mq,q,0) == TYPE_void && |
| 217 | !isaBatType(getArgType(mq, q, 1)) ){ |
| 218 | /* handle nil assignment */ |
| 219 | if( ATOMcmp(getArgGDKType(mq, q, 1), |
| 220 | VALptr(&getVar(mq, getArg(q,1))->value), |
| 221 | ATOMnilptr(getArgType(mq, q, 1))) == 0) { |
| 222 | ValRecord cst; |
| 223 | int tpe = newBatType(getArgType(mq, q, 1)); |
| 224 | |
| 225 | setVarType(mq,getArg(q,0),tpe); |
| 226 | cst.vtype = TYPE_bat; |
| 227 | cst.val.bval = bat_nil; |
| 228 | cst.len = 0; |
| 229 | getArg(q,1) = defConstant(mq, tpe, &cst); |
| 230 | setVarType(mq, getArg(q,1), tpe); |
| 231 | } else{ |
| 232 | /* handle constant tail setting */ |
| 233 | int tpe = newBatType(getArgType(mq, q, 1)); |
| 234 | |
| 235 | setVarType(mq,getArg(q,0),tpe); |
| 236 | setModuleId(q,algebraRef); |
| 237 | setFunctionId(q,projectRef); |
| 238 | q= pushArgument(mb,q, getArg(q,1)); |
| 239 | getArg(q,1)= refbat; |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | /* now upgrade the statements */ |
| 245 | for(i=1; i<mq->stop; i++){ |
| 246 | q= getInstrPtr(mq,i); |
| 247 | if( q->token== ENDsymbol) |
| 248 | break; |
| 249 | for(j=0; j<q->argc; j++) |
| 250 | if ( upgrade[getArg(q,j)]){ |
| 251 | if ( blockStart(q) || |
| 252 | q->barrier== REDOsymbol || q->barrier==LEAVEsymbol ) |
| 253 | goto terminateMX; |
| 254 | if (getModuleId(q)){ |
| 255 | snprintf(buf,1024,"bat%s" ,getModuleId(q)); |
| 256 | setModuleId(q,putName(buf)); |
| 257 | q->typechk = TYPE_UNKNOWN; |
| 258 | |
| 259 | /* now see if we can resolve the instruction */ |
| 260 | typeChecker(cntxt->usermodule,mq,q,TRUE); |
| 261 | if( q->typechk== TYPE_UNKNOWN) |
| 262 | goto terminateMX; |
| 263 | actions++; |
| 264 | break; |
| 265 | } |
| 266 | /* handle simple upgraded assignments as well */ |
| 267 | if ( q->token== ASSIGNsymbol && |
| 268 | q->argc == 2 && |
| 269 | !(isaBatType( getArgType(mq,q,1))) ){ |
| 270 | setModuleId(q,algebraRef); |
| 271 | setFunctionId(q,projectRef); |
| 272 | q= pushArgument(mq,q, getArg(q,1)); |
| 273 | getArg(q,1)= refbat; |
| 274 | |
| 275 | q->typechk = TYPE_UNKNOWN; |
| 276 | typeChecker(cntxt->usermodule,mq,q,TRUE); |
| 277 | if( q->typechk== TYPE_UNKNOWN) |
| 278 | goto terminateMX; |
| 279 | actions++; |
| 280 | break; |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | |
| 286 | if(mq->errors){ |
| 287 | terminateMX: |
| 288 | |
| 289 | if( OPTdebug & OPTremap){ |
| 290 | fprintf(stderr,"Abort remap\n" ); |
| 291 | if (q) |
| 292 | fprintInstruction(stderr,mb,0,q,LIST_MAL_ALL); |
| 293 | } |
| 294 | |
| 295 | freeMalBlk(mq); |
| 296 | GDKfree(upgrade); |
| 297 | |
| 298 | /* ugh ugh, fallback to non inline, but optimized code */ |
| 299 | msg = OPTmultiplexSimple(cntxt, s->def); |
| 300 | if(msg) |
| 301 | freeException(msg); |
| 302 | s->def->inlineProp = 0; |
| 303 | return 0; |
| 304 | } |
| 305 | /* |
| 306 | * We have successfully constructed a variant |
| 307 | * of the to-be-inlined function. Put it in place |
| 308 | * of the original multiplex. |
| 309 | * But first, shift the arguments of the multiplex. |
| 310 | */ |
| 311 | delArgument(p,2); |
| 312 | delArgument(p,1); |
| 313 | inlineMALblock(mb,pc,mq); |
| 314 | |
| 315 | if( OPTdebug & OPTremap){ |
| 316 | fprintInstruction(stderr,mb,0,p,LIST_MAL_ALL); |
| 317 | fprintf(stderr,"#NEW BLOCK\n" ); |
| 318 | fprintFunction(stderr,mq, 0, LIST_MAL_ALL); |
| 319 | fprintf(stderr,"#INLINED RESULT\n" ); |
| 320 | fprintFunction(stderr,mb, 0, LIST_MAL_ALL); |
| 321 | } |
| 322 | |
| 323 | freeMalBlk(mq); |
| 324 | GDKfree(upgrade); |
| 325 | return 1; |
| 326 | } |
| 327 | /* |
| 328 | * The comparison multiplex operations with a constant head may be supported |
| 329 | * by reverse of the operation. |
| 330 | */ |
| 331 | static struct{ |
| 332 | char *src, *dst; |
| 333 | int len; |
| 334 | }OperatorMap[]={ |
| 335 | {"<" , ">" ,1}, |
| 336 | {">" , "<" ,1}, |
| 337 | {">=" , "<=" ,2}, |
| 338 | {"<=" , ">=" ,2}, |
| 339 | {"==" , "==" ,2}, |
| 340 | {"!=" , "!=" ,2}, |
| 341 | {0,0,0}}; |
| 342 | |
| 343 | static int |
| 344 | OPTremapSwitched(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci, Module scope){ |
| 345 | char *fcn; |
| 346 | int r,i; |
| 347 | (void) stk; |
| 348 | (void) scope; |
| 349 | |
| 350 | if( !isMultiplex(pci) && |
| 351 | !isVarConstant(mb,getArg(pci,1)) && |
| 352 | !isVarConstant(mb,getArg(pci,2)) && |
| 353 | !isVarConstant(mb,getArg(pci,4)) && |
| 354 | pci->argc != 5) |
| 355 | return 0; |
| 356 | fcn = VALget(&getVar(mb, getArg(pci, 2))->value); |
| 357 | for(i=0;OperatorMap[i].src;i++) |
| 358 | if( strcmp(fcn,OperatorMap[i].src)==0){ |
| 359 | /* found a candidate for a switch */ |
| 360 | getVarConstant(mb, getArg(pci, 2)).val.sval = putNameLen(OperatorMap[i].dst,OperatorMap[i].len); |
| 361 | getVarConstant(mb, getArg(pci, 2)).len = OperatorMap[i].len; |
| 362 | r= getArg(pci,3); getArg(pci,3)=getArg(pci,4);getArg(pci,4)=r; |
| 363 | r= OPTremapDirect(cntxt,mb, stk, pci, scope); |
| 364 | |
| 365 | /* always restore the allocated function name */ |
| 366 | getVarConstant(mb, getArg(pci, 2)).val.sval= fcn; |
| 367 | assert(strlen(fcn) <= INT_MAX); |
| 368 | getVarConstant(mb, getArg(pci, 2)).len = strlen(fcn); |
| 369 | |
| 370 | if (r) return 1; |
| 371 | |
| 372 | /* restore the arguments */ |
| 373 | r= getArg(pci,3); getArg(pci,3)=getArg(pci,4);getArg(pci,4)=r; |
| 374 | } |
| 375 | return 0; |
| 376 | } |
| 377 | |
| 378 | str |
| 379 | OPTremapImplementation(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci) |
| 380 | { |
| 381 | |
| 382 | InstrPtr *old, p; |
| 383 | int i, limit, slimit, doit= 0; |
| 384 | Module scope = cntxt->usermodule; |
| 385 | lng usec = GDKusec(); |
| 386 | char buf[256]; |
| 387 | str msg = MAL_SUCCEED; |
| 388 | |
| 389 | (void) pci; |
| 390 | old = mb->stmt; |
| 391 | limit = mb->stop; |
| 392 | slimit = mb->ssize; |
| 393 | if ( newMalBlkStmt(mb, mb->ssize) < 0 ) |
| 394 | throw(MAL,"optmizer.remap" , SQLSTATE(HY001) MAL_MALLOC_FAIL); |
| 395 | |
| 396 | for (i = 0; i < limit; i++) { |
| 397 | p = old[i]; |
| 398 | if (isMultiplex(p)){ |
| 399 | /* |
| 400 | * The next step considered is to handle inlined functions. |
| 401 | * It means we have already skipped the most obvious ones, |
| 402 | * such as the calculator functions. It is particularly |
| 403 | * geared at handling the PSM code. |
| 404 | */ |
| 405 | str mod = VALget(&getVar(mb, getArg(p, 1))->value); |
| 406 | str fcn = VALget(&getVar(mb, getArg(p, 2))->value); |
| 407 | Symbol s = findSymbol(cntxt->usermodule, mod,fcn); |
| 408 | |
| 409 | if (s && s->def->inlineProp ){ |
| 410 | |
| 411 | if( OPTdebug & OPTremap){ |
| 412 | fprintf(stderr,"#Multiplex inline\n" ); |
| 413 | fprintInstruction(stderr,mb,0,p,LIST_MAL_ALL); |
| 414 | } |
| 415 | |
| 416 | pushInstruction(mb, p); |
| 417 | if( OPTmultiplexInline(cntxt,mb,p,mb->stop-1) ){ |
| 418 | doit++; |
| 419 | |
| 420 | if( OPTdebug & OPTremap){ |
| 421 | fprintf(stderr,"#actions %d\n" ,doit); |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | } else if (OPTremapDirect(cntxt, mb, stk, p, scope) || |
| 426 | OPTremapSwitched(cntxt, mb, stk, p, scope)) { |
| 427 | freeInstruction(p); |
| 428 | doit++; |
| 429 | } else { |
| 430 | pushInstruction(mb, p); |
| 431 | } |
| 432 | } else if (p->argc == 4 && |
| 433 | getModuleId(p) == aggrRef && |
| 434 | getFunctionId(p) == avgRef) { |
| 435 | /* group aggr.avg -> aggr.sum/aggr.count */ |
| 436 | InstrPtr sum, avg,t, iszero; |
| 437 | InstrPtr cnt; |
| 438 | sum = copyInstruction(p); |
| 439 | if( sum == NULL) |
| 440 | throw(MAL, "remap" , SQLSTATE(HY001) MAL_MALLOC_FAIL); |
| 441 | cnt = copyInstruction(p); |
| 442 | if( cnt == NULL){ |
| 443 | freeInstruction(sum); |
| 444 | throw(MAL, "remap" , SQLSTATE(HY001) MAL_MALLOC_FAIL); |
| 445 | } |
| 446 | setFunctionId(sum, sumRef); |
| 447 | setFunctionId(cnt, countRef); |
| 448 | getArg(sum,0) = newTmpVariable(mb, getArgType(mb, p, 1)); |
| 449 | getArg(cnt,0) = newTmpVariable(mb, newBatType(TYPE_lng)); |
| 450 | pushInstruction(mb, sum); |
| 451 | pushInstruction(mb, cnt); |
| 452 | |
| 453 | t = newInstruction(mb, batcalcRef, putName("==" )); |
| 454 | getArg(t,0) = newTmpVariable(mb, newBatType(TYPE_bit)); |
| 455 | t = pushArgument(mb, t, getDestVar(cnt)); |
| 456 | t = pushLng(mb, t, 0); |
| 457 | pushInstruction(mb, t); |
| 458 | iszero = t; |
| 459 | |
| 460 | t = newInstruction(mb, batcalcRef, dblRef); |
| 461 | getArg(t,0) = newTmpVariable(mb, getArgType(mb, p, 0)); |
| 462 | t = pushArgument(mb, t, getDestVar(sum)); |
| 463 | pushInstruction(mb, t); |
| 464 | sum = t; |
| 465 | |
| 466 | t = newInstruction(mb, batcalcRef, putName("ifthenelse" )); |
| 467 | getArg(t,0) = newTmpVariable(mb, getArgType(mb, p, 0)); |
| 468 | t = pushArgument(mb, t, getDestVar(iszero)); |
| 469 | t = pushNil(mb, t, TYPE_dbl); |
| 470 | t = pushArgument(mb, t, getDestVar(sum)); |
| 471 | pushInstruction(mb, t); |
| 472 | sum = t; |
| 473 | |
| 474 | t = newInstruction(mb, batcalcRef, dblRef); |
| 475 | getArg(t,0) = newTmpVariable(mb, getArgType(mb, p, 0)); |
| 476 | t = pushArgument(mb, t, getDestVar(cnt)); |
| 477 | pushInstruction(mb, t); |
| 478 | cnt = t; |
| 479 | |
| 480 | avg = newInstruction(mb, batcalcRef, divRef); |
| 481 | getArg(avg, 0) = getArg(p, 0); |
| 482 | avg = pushArgument(mb, avg, getDestVar(sum)); |
| 483 | avg = pushArgument(mb, avg, getDestVar(cnt)); |
| 484 | freeInstruction(p); |
| 485 | pushInstruction(mb, avg); |
| 486 | } else { |
| 487 | pushInstruction(mb, p); |
| 488 | } |
| 489 | } |
| 490 | for(; i<slimit; i++) |
| 491 | if( old[i]) |
| 492 | freeInstruction(old[i]); |
| 493 | GDKfree(old); |
| 494 | |
| 495 | if (doit) |
| 496 | chkTypes(cntxt->usermodule,mb,TRUE); |
| 497 | /* Defense line against incorrect plans */ |
| 498 | if( mb->errors == MAL_SUCCEED && doit > 0){ |
| 499 | chkTypes(cntxt->usermodule, mb, FALSE); |
| 500 | chkFlow(mb); |
| 501 | chkDeclarations(mb); |
| 502 | } |
| 503 | /* keep all actions taken as a post block comment */ |
| 504 | usec = GDKusec()- usec; |
| 505 | snprintf(buf,256,"%-20s actions=%2d time=" LLFMT " usec" ,"remap" ,doit, usec); |
| 506 | newComment(mb,buf); |
| 507 | if( doit >= 0) |
| 508 | addtoMalBlkHistory(mb); |
| 509 | |
| 510 | if( OPTdebug & OPTremap){ |
| 511 | fprintf(stderr, "#REMAP optimizer exit\n" ); |
| 512 | fprintFunction(stderr, mb, 0, LIST_MAL_ALL); |
| 513 | } |
| 514 | return msg; |
| 515 | } |
| 516 | |