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}