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}