001// DtraceDiff.java 002 003package daikon.tools; 004 005import static daikon.VarInfo.VarFlags; 006import static daikon.tools.nullness.NullnessUtil.*; 007 008import daikon.Daikon; 009import daikon.FileIO; 010import daikon.Global; 011import daikon.PptMap; 012import daikon.PptTopLevel; 013import daikon.ProglangType; 014import daikon.ValueTuple; 015import daikon.VarInfo; 016import daikon.config.Configuration; 017import gnu.getopt.*; 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Map; 025import java.util.Set; 026import java.util.regex.Pattern; 027import org.checkerframework.checker.nullness.qual.NonNull; 028import org.checkerframework.checker.nullness.qual.Nullable; 029import org.plumelib.util.RegexUtil; 030import org.plumelib.util.StringsPlume; 031 032/** 033 * This tool is used to find the differences between two dtrace files based on analysis of the 034 * files' content, rather than a straight textual comparison. 035 */ 036public class DtraceDiff { 037 038 /** The usage message for this program. */ 039 private static String usage = 040 StringsPlume.joinLines( 041 "Usage: DtraceDiff [OPTION]... [DECLS1]... DTRACE1 [DECLS2]... DTRACE2", 042 "DTRACE1 and DTRACE2 are the data trace files to be compared.", 043 "You may optionally specify corresponding DECLS files for each one.", 044 "If no DECLS file is specified, it is assumed that the declarations", 045 "are included in the data trace file instead.", 046 "OPTIONs are:", 047 " -h, --" + Daikon.help_SWITCH, 048 " Display this usage message", 049 " --" + Daikon.ppt_regexp_SWITCH, 050 " Only include ppts matching regexp", 051 " --" + Daikon.ppt_omit_regexp_SWITCH, 052 " Omit all ppts matching regexp", 053 " --" + Daikon.var_regexp_SWITCH, 054 " Only include variables matching regexp", 055 " --" + Daikon.var_omit_regexp_SWITCH, 056 " Omit all variables matching regexp", 057 " --" + Daikon.config_SWITCH, 058 " Specify a configuration file ", 059 " --" + Daikon.config_option_SWITCH, 060 " Specify a configuration option ", 061 "See the Daikon manual for more information."); 062 063 /** Set this flag true for debugging output. */ 064 private static boolean debug = false; 065 066 /** 067 * Entry point for DtraceDiff program. 068 * 069 * @param args command-line arguments, like those of {@link #mainHelper} and {@link #main} 070 */ 071 public static void main(String[] args) { 072 try { 073 mainHelper(args); 074 } catch (daikon.Daikon.DaikonTerminationException e) { 075 daikon.Daikon.handleDaikonTerminationException(e); 076 } 077 } 078 079 /** 080 * This entry point is useful for testing. It returns a boolean to indicate return status instead 081 * of croaking with an error. 082 * 083 * @param args command-line arguments, like those of {@link #mainHelper} and {@link #main} 084 * @return true if DtraceDiff completed without an error 085 */ 086 public static boolean mainTester(String[] args) { 087 try { 088 mainHelper(args); 089 return true; 090 } catch (DiffError de) { 091 // System.out.printf("Diff error for args %s: %s%n", 092 // Arrays.toString(args), de.getMessage()); 093 return false; 094 } 095 } 096 097 /** 098 * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is 099 * appropriate to be called progrmmatically. 100 * 101 * @param args command-line arguments, like those of {@link #main} 102 */ 103 public static void mainHelper(final String[] args) { 104 Set<File> declsfile1 = new HashSet<>(); 105 String dtracefile1 = null; 106 Set<File> declsfile2 = new HashSet<>(); 107 String dtracefile2 = null; 108 109 LongOpt[] longopts = 110 new LongOpt[] { 111 // Process only part of the trace file 112 new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 113 new LongOpt(Daikon.ppt_omit_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 114 new LongOpt(Daikon.var_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 115 new LongOpt(Daikon.var_omit_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 116 // Configuration options 117 new LongOpt(Daikon.config_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 118 new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 119 }; 120 121 Getopt g = new Getopt("daikon.tools.DtraceDiff", args, "h:", longopts); 122 int c; 123 while ((c = g.getopt()) != -1) { 124 switch (c) { 125 126 // long option 127 case 0: 128 String option_name = longopts[g.getLongind()].getName(); 129 if (Daikon.help_SWITCH.equals(option_name)) { 130 System.out.println(usage); 131 throw new Daikon.NormalTermination(); 132 } else if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { 133 if (Daikon.ppt_regexp != null) { 134 throw new Error( 135 "multiple --" 136 + Daikon.ppt_regexp_SWITCH 137 + " regular expressions supplied on command line"); 138 } 139 String regexp_string = Daikon.getOptarg(g); 140 if (!RegexUtil.isRegex(regexp_string)) { 141 throw new Daikon.UserError( 142 "Bad regexp " 143 + regexp_string 144 + " for " 145 + Daikon.ppt_regexp_SWITCH 146 + ": " 147 + RegexUtil.regexError(regexp_string)); 148 } 149 Daikon.ppt_regexp = Pattern.compile(regexp_string); 150 break; 151 } else if (Daikon.ppt_omit_regexp_SWITCH.equals(option_name)) { 152 if (Daikon.ppt_omit_regexp != null) { 153 throw new Error( 154 "multiple --" 155 + Daikon.ppt_omit_regexp_SWITCH 156 + " regular expressions supplied on command line"); 157 } 158 String regexp_string = Daikon.getOptarg(g); 159 if (!RegexUtil.isRegex(regexp_string)) { 160 throw new Daikon.UserError( 161 "Bad regexp " 162 + regexp_string 163 + " for " 164 + Daikon.ppt_omit_regexp_SWITCH 165 + ": " 166 + RegexUtil.regexError(regexp_string)); 167 } 168 Daikon.ppt_omit_regexp = Pattern.compile(regexp_string); 169 break; 170 } else if (Daikon.var_regexp_SWITCH.equals(option_name)) { 171 if (Daikon.var_regexp != null) { 172 throw new Error( 173 "multiple --" 174 + Daikon.var_regexp_SWITCH 175 + " regular expressions supplied on command line"); 176 } 177 String regexp_string = Daikon.getOptarg(g); 178 if (!RegexUtil.isRegex(regexp_string)) { 179 throw new Daikon.UserError( 180 "Bad regexp " 181 + regexp_string 182 + " for " 183 + Daikon.var_regexp_SWITCH 184 + ": " 185 + RegexUtil.regexError(regexp_string)); 186 } 187 Daikon.var_regexp = Pattern.compile(regexp_string); 188 break; 189 } else if (Daikon.var_omit_regexp_SWITCH.equals(option_name)) { 190 if (Daikon.var_omit_regexp != null) { 191 throw new Error( 192 "multiple --" 193 + Daikon.var_omit_regexp_SWITCH 194 + " regular expressions supplied on command line"); 195 } 196 String regexp_string = Daikon.getOptarg(g); 197 if (!RegexUtil.isRegex(regexp_string)) { 198 throw new Daikon.UserError( 199 "Bad regexp " 200 + regexp_string 201 + " for " 202 + Daikon.var_omit_regexp_SWITCH 203 + ": " 204 + RegexUtil.regexError(regexp_string)); 205 } 206 Daikon.var_omit_regexp = Pattern.compile(regexp_string); 207 break; 208 } else if (Daikon.config_SWITCH.equals(option_name)) { 209 String config_file = Daikon.getOptarg(g); 210 try (InputStream stream = new FileInputStream(config_file)) { 211 Configuration.getInstance().apply(stream); 212 } catch (IOException e) { 213 throw new RuntimeException("Could not open config file " + config_file); 214 } 215 break; 216 } else if (Daikon.config_option_SWITCH.equals(option_name)) { 217 String item = Daikon.getOptarg(g); 218 Configuration.getInstance().apply(item); 219 break; 220 } else { 221 throw new RuntimeException("Unknown long option received: " + option_name); 222 } 223 224 // short options 225 case 'h': 226 System.out.println(usage); 227 throw new Daikon.NormalTermination(); 228 229 case '?': 230 break; // getopt() already printed an error 231 232 default: 233 System.out.println("getopt() returned " + c); 234 break; 235 } 236 } 237 238 for (int i = g.getOptind(); i < args.length; i++) { 239 if (args[i].indexOf(".decls") != -1) { 240 if (dtracefile1 == null) { 241 declsfile1.add(new File(args[i])); 242 } else if (dtracefile2 == null) declsfile2.add(new File(args[i])); 243 else { 244 throw new daikon.Daikon.UserError(usage); 245 } 246 } else { // presume any other file is a dtrace file 247 if (dtracefile1 == null) { 248 dtracefile1 = args[i]; 249 } else if (dtracefile2 == null) dtracefile2 = args[i]; 250 else { 251 throw new daikon.Daikon.UserError(usage); 252 } 253 } 254 } 255 if ((dtracefile1 == null) || (dtracefile2 == null)) { 256 throw new daikon.Daikon.UserError(usage); 257 } 258 dtraceDiff(declsfile1, dtracefile1, declsfile2, dtracefile2); 259 } 260 261 public static void dtraceDiff( 262 Set<File> declsfile1, String dtracefile1, Set<File> declsfile2, String dtracefile2) { 263 264 // System.out.printf("dtrace files = %s, %s%n", dtracefile1, dtracefile2); 265 FileIO.resetNewDeclFormat(); 266 267 try { 268 Map<PptTopLevel, PptTopLevel> pptmap = new HashMap<>(); // map ppts1 -> ppts2 269 PptMap ppts1 = FileIO.read_declaration_files(declsfile1); 270 PptMap ppts2 = FileIO.read_declaration_files(declsfile2); 271 272 try (FileIO.ParseState state1 = new FileIO.ParseState(dtracefile1, false, true, ppts1); 273 FileIO.ParseState state2 = new FileIO.ParseState(dtracefile2, false, true, ppts2)) { 274 275 while (true) { 276 // *** should do some kind of progress bar here? 277 // Read from dtracefile1 until we get a sample record or a decl record or an EOF. 278 while (true) { 279 FileIO.read_data_trace_record_setstate(state1); 280 if ((state1.rtype == FileIO.RecordType.SAMPLE) 281 || (state1.rtype == FileIO.RecordType.DECL)) { 282 break; 283 } else if ((state1.rtype == FileIO.RecordType.EOF) 284 || (state1.rtype == FileIO.RecordType.TRUNCATED)) { 285 break; 286 } 287 } 288 // Read from dtracefile2 until we get a sample record or a decl record or an EOF. 289 while (true) { 290 FileIO.read_data_trace_record_setstate(state2); 291 if ((state2.rtype == FileIO.RecordType.SAMPLE) 292 || (state2.rtype == FileIO.RecordType.DECL)) { 293 break; 294 } else if ((state2.rtype == FileIO.RecordType.EOF) 295 || (state2.rtype == FileIO.RecordType.TRUNCATED)) { 296 break; 297 } 298 } 299 300 // things had better be the same 301 if (state1.rtype == state2.rtype) { 302 @SuppressWarnings("nullness") // dependent: state1 is ParseState 303 @NonNull PptTopLevel ppt1 = state1.ppt; 304 if (ppt1 == null) { 305 // Null means the ppt should be excluded because it matches 306 // the omit_regexp or doesn't match the ppt_regexp. 307 continue; 308 } 309 @SuppressWarnings("nullness") // dependent: state2 is ParseState 310 @NonNull PptTopLevel ppt2 = state2.ppt; 311 if (state1.rtype == FileIO.RecordType.SAMPLE) { 312 @SuppressWarnings("nullness") // dependent: state1 is SAMPLE 313 @NonNull ValueTuple vt1 = state1.vt; 314 @SuppressWarnings("nullness") // dependent: state2 is SAMPLE 315 @NonNull ValueTuple vt2 = state2.vt; 316 VarInfo[] vis1 = ppt1.var_infos; 317 VarInfo[] vis2 = ppt2.var_infos; 318 319 // Check to see that Ppts match the first time we encounter them 320 PptTopLevel foundppt = pptmap.get(ppt1); 321 if (foundppt == null) { 322 if (!ppt1.name.equals(ppt2.name)) { 323 ppt_mismatch_error(state1, dtracefile1, state2, dtracefile2); 324 } 325 for (int i = 0; (i < ppt1.num_tracevars) && (i < ppt2.num_tracevars); i++) { 326 // *** what about comparability and aux info? 327 if (!vis1[i].name().equals(vis2[i].name()) 328 || (vis1[i].is_static_constant != vis2[i].is_static_constant) 329 || (vis1[i].isStaticConstant() 330 && vis2[i].isStaticConstant() 331 && !values_are_equal( 332 vis1[i], vis1[i].constantValue(), vis2[i].constantValue())) 333 || ((vis1[i].type != vis2[i].type) 334 || (vis1[i].file_rep_type != vis2[i].file_rep_type))) 335 ppt_var_decl_error(vis1[i], state1, dtracefile1, vis2[i], state2, dtracefile2); 336 } 337 if (ppt1.num_tracevars != ppt2.num_tracevars) { 338 ppt_decl_error(state1, dtracefile1, state2, dtracefile2); 339 } 340 pptmap.put(ppt1, ppt2); 341 } else if (foundppt != ppt2) { 342 ppt_mismatch_error(state1, dtracefile1, state2, dtracefile2); 343 } 344 345 // check to see that variables on this pair of samples match 346 for (int i = 0; i < ppt1.num_tracevars; i++) { 347 if (vis1[i].is_static_constant) { 348 continue; 349 } 350 boolean missing1 = vt1.isMissingNonsensical(vis1[i]); 351 boolean missing2 = vt2.isMissingNonsensical(vis2[i]); 352 Object val1 = vt1.getValueOrNull(vis1[i]); 353 Object val2 = vt2.getValueOrNull(vis2[i]); 354 // Require that missing1 == missing2. Also require that if 355 // the values are present, they are the same. 356 if (!((missing1 == missing2) 357 && (missing1 358 // At this point, missing1 == false, missing2 == false, 359 // val1 != null, val2 != null. 360 || values_are_equal( 361 vis1[i], 362 castNonNull(val1), 363 castNonNull(val2))))) // application invariant 364 ppt_var_value_error( 365 vis1[i], val1, state1, dtracefile1, vis2[i], val2, state2, dtracefile2); 366 } 367 } else if (state1.rtype == FileIO.RecordType.DECL) { 368 // compare decls 369 VarInfo[] vis1 = ppt1.var_infos; 370 VarInfo[] vis2 = ppt2.var_infos; 371 if (!ppt1.name.equals(ppt2.name)) { 372 ppt_mismatch_error(state1, dtracefile1, state2, dtracefile2); 373 } 374 if (ppt1.num_declvars != ppt2.num_declvars) { 375 ppt_decl_error(state1, dtracefile1, state2, dtracefile2); 376 } 377 // check to see that the decls match 378 for (int i = 0; i < ppt1.num_declvars; i++) { 379 if (!compare_varinfos(vis1[i], vis2[i])) { 380 if (!debug) { 381 ppt_var_decl_error(vis1[i], state1, dtracefile1, vis2[i], state2, dtracefile2); 382 } else { 383 System.out.printf("ERROR: dtrace decl mismatch within: %s%n", ppt1.name); 384 printVarinfo(vis1[i]); 385 printVarinfo(vis2[i]); 386 } 387 } 388 } 389 } else { 390 return; // EOF on both files ==> normal return 391 } 392 // state1.rtype != state2.rtype 393 } else if ((state1.rtype == FileIO.RecordType.TRUNCATED) 394 || (state2.rtype == FileIO.RecordType.TRUNCATED)) 395 return; // either file reached truncation limit, return quietly 396 else if (state1.rtype == FileIO.RecordType.EOF) { 397 assert state2.ppt != null 398 : "@AssumeAssertion(nullness): application invariant: status is not EOF or" 399 + " TRUNCATED"; 400 throw new DiffError( 401 String.format( 402 "ppt %s is at line %d in %s but is missing at end of %s", 403 state2.ppt.name(), state2.get_linenum(), dtracefile2, dtracefile1)); 404 } else { 405 assert state1.ppt != null 406 : "@AssumeAssertion(nullness): application invariant: status is not EOF or" 407 + " TRUNCATED"; 408 throw new DiffError( 409 String.format( 410 "ppt %s is at line %d in %s but is missing at end of %s", 411 state1.ppt.name(), state1.get_linenum(), dtracefile1, dtracefile2)); 412 } 413 } 414 } 415 } catch (IOException e) { 416 System.out.println(); 417 e.printStackTrace(); 418 throw new Error(e); 419 } 420 } 421 422 /** 423 * Compare two VarInfos for equality. Note there are many fields not compared: comparability, 424 * constant, exclosing-var and parent, for example. 425 * 426 * @param vi1 a VarInfo to compare 427 * @param vi2 a VarInfo to compare 428 * @return true if the VarInfos match 429 */ 430 private static boolean compare_varinfos(VarInfo vi1, VarInfo vi2) { 431 if (!vi1.name().equals(vi2.name())) { 432 return false; 433 } 434 if (!vi1.str_name().equals(vi2.str_name())) { 435 return false; 436 } 437 if (!(vi1.var_kind == vi2.var_kind)) { 438 return false; 439 } 440 if (!vi1.type.equals(vi2.type)) { 441 return false; 442 } 443 if (!vi1.file_rep_type.equals(vi2.file_rep_type)) { 444 return false; 445 } 446 if (!vi1.var_flags.equals(vi2.var_flags)) { 447 return false; 448 } 449 return true; 450 } 451 452 /** 453 * Used for debugging -- prints some of a VarInfo fields. Note there are many fields not printed: 454 * comparability, constant, exclosing-var and parent, for example. 455 * 456 * @param vi the VarInfo to print 457 */ 458 private static void printVarinfo(VarInfo vi) { 459 System.out.printf("variable %s%n", vi.str_name()); 460 System.out.printf(" var-kind %s%n", vi.var_kind); 461 System.out.printf(" dec-type %s%n", vi.type); 462 System.out.printf(" rep-type %s%n", vi.file_rep_type); 463 if (!vi.var_flags.isEmpty()) { 464 System.out.printf(" flags"); 465 for (VarFlags flag : vi.var_flags) { 466 System.out.printf(" %s", flag.name().toLowerCase()); 467 } 468 System.out.printf("%n"); 469 } 470 } 471 472 /** 473 * Compare two VarInfo fields for equality. 474 * 475 * @param vi a VarInfo that holds val1 476 * @param val1 a VarInfo field to compare 477 * @param val2 a VarInfo field to compare 478 * @return true if the fields match 479 */ 480 private static boolean values_are_equal(VarInfo vi, Object val1, Object val2) { 481 ProglangType type = vi.file_rep_type; 482 // System.out.printf("values_are_equal type = %s%n", type); 483 if (type.isArray()) { 484 // array case 485 if (type.isPointerFileRep()) { 486 long[] v1 = (long[]) val1; 487 long[] v2 = (long[]) val2; 488 if (v1.length != v2.length) { 489 return false; 490 } 491 for (int i = 0; i < v1.length; i++) { 492 if (((v1[i] == 0) || (v2[i] == 0)) && (v1[i] != v2[i])) { 493 return false; 494 } 495 } 496 return true; 497 } else if (type.baseIsScalar()) { 498 long[] v1 = (long[]) val1; 499 long[] v2 = (long[]) val2; 500 if (v1.length != v2.length) { 501 return false; 502 } 503 for (int i = 0; i < v1.length; i++) { 504 if (v1[i] != v2[i]) { 505 return false; 506 } 507 } 508 return true; 509 } else if (type.baseIsFloat()) { 510 double[] v1 = (double[]) val1; 511 double[] v2 = (double[]) val2; 512 if (v1.length != v2.length) { 513 return false; 514 } 515 for (int i = 0; i < v1.length; i++) { 516 if (!((Double.isNaN(v1[i]) && Double.isNaN(v2[i])) || Global.fuzzy.eq(v1[i], v2[i]))) { 517 return false; 518 } 519 } 520 return true; 521 } else if (type.baseIsString()) { 522 String[] v1 = (String[]) val1; 523 String[] v2 = (String[]) val2; 524 if (v1.length != v2.length) { 525 return false; 526 } 527 for (int i = 0; i < v1.length; i++) { 528 // System.out.printf("string array[%d] %s %s%n", i, v1[i], v2[i]); 529 if ((v1[i] == null) && (v2[i] == null)) { 530 // nothing to do 531 } else if ((v1[i] == null) || (v2[i] == null)) { 532 return false; 533 } else if (!v1[i].equals(v2[i])) { 534 return false; 535 } 536 } 537 return true; 538 } 539 } else { 540 // scalar case 541 if (type.isPointerFileRep()) { 542 long v1 = ((Long) val1).longValue(); 543 long v2 = ((Long) val2).longValue(); 544 return !(((v1 == 0) || (v2 == 0)) && (v1 != v2)); 545 } else if (type.isScalar()) { 546 return ((Long) val1).longValue() == ((Long) val2).longValue(); 547 } else if (type.isFloat()) { 548 double d1 = ((Double) val1).doubleValue(); 549 double d2 = ((Double) val2).doubleValue(); 550 return (Double.isNaN(d1) && Double.isNaN(d2)) || Global.fuzzy.eq(d1, d2); 551 } else if (type.isString()) { 552 return ((String) val1).equals((String) val2); 553 } 554 } 555 throw new Error("Unexpected value type found"); // should never happen 556 } 557 558 @SuppressWarnings("nullness") // dependent: ParseState for error reporting 559 private static void ppt_mismatch_error( 560 FileIO.ParseState state1, String dtracefile1, FileIO.ParseState state2, String dtracefile2) { 561 throw new DiffError( 562 String.format( 563 "Mismatched program point:%n ppt %s at %s:%d%n ppt %s at %s:%d", 564 state1.ppt.name, 565 dtracefile1, 566 state1.get_linenum(), 567 state2.ppt.name, 568 dtracefile2, 569 state2.get_linenum())); 570 } 571 572 @SuppressWarnings("nullness") // dependent: ParseState for error reporting 573 private static void ppt_decl_error( 574 FileIO.ParseState state1, String dtracefile1, FileIO.ParseState state2, String dtracefile2) { 575 throw new DiffError( 576 String.format( 577 "Mismatched program point declaration:%n ppt %s at %s:%d%n ppt %s at %s:%d", 578 state1.ppt.name, 579 dtracefile1, 580 state1.get_linenum(), 581 state2.ppt.name, 582 dtracefile2, 583 state2.get_linenum())); 584 } 585 586 @SuppressWarnings("nullness") // dependent: ParseState for error reporting 587 private static void ppt_var_decl_error( 588 VarInfo vi1, 589 FileIO.ParseState state1, 590 String dtracefile1, 591 VarInfo vi2, 592 FileIO.ParseState state2, 593 String dtracefile2) { 594 assert state1.ppt.name.equals(state2.ppt.name); 595 throw new DiffError( 596 String.format( 597 "Mismatched variable declaration in program point %s:%n" 598 + " variable %s at %s:%d%n" 599 + " variable %s at %s:%d", 600 state1.ppt.name, 601 vi1.name(), 602 dtracefile1, 603 state1.get_linenum(), 604 vi2.name(), 605 dtracefile2, 606 state2.get_linenum())); 607 } 608 609 @SuppressWarnings("nullness") // nullable parameters suppress warnings at call sites 610 private static void ppt_var_value_error( 611 VarInfo vi1, 612 @Nullable Object val1, 613 FileIO.ParseState state1, 614 String dtracefile1, 615 VarInfo vi2, 616 @Nullable Object val2, 617 FileIO.ParseState state2, 618 String dtracefile2) { 619 assert vi1.name().equals(vi2.name()); 620 assert state1.ppt.name.equals(state2.ppt.name); 621 throw new DiffError( 622 String.format( 623 "Mismatched values for variable %s in program point %s:%n" 624 + " value %s at %s:%d%n" 625 + " value %s at %s:%d", 626 vi1.name(), 627 state1.ppt.name, 628 val1, 629 dtracefile1, 630 state1.get_linenum(), 631 val2, 632 dtracefile2, 633 state2.get_linenum())); 634 } 635 636 /** 637 * Exception thrown for diffs. Allows differences to be distinguished from other exceptions that 638 * might occur. 639 */ 640 public static class DiffError extends Error { 641 static final long serialVersionUID = 20071203L; 642 643 public DiffError(String err_msg) { 644 super(err_msg); 645 } 646 } 647}