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