All files / src/core trace.ts

100% Statements 16/16
100% Branches 10/10
100% Functions 4/4
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74                                                                              173x       173x   1668x               173x 323x 321x     173x 352x 352x 684x 684x   1x     351x     173x 173x    
export type Vars = Readonly<Record<string, unknown>>;
 
export interface Step<S> {
  readonly op: string;
  readonly state: S;
  readonly caption: string;
  /** Source line this step came from, set with `at`. 1-based. */
  readonly line?: number;
  /** Variables to show with this step, set with `watch`. */
  readonly vars?: Vars;
}
 
export interface Trace<S> {
  readonly steps: readonly Step<S>[];
  record(op: string, state: S, caption?: string): void;
  /** Tags the steps recorded from now on with a 1-based source line. Records nothing itself. */
  at(line: number): void;
  /**
   * Shows these variables with the steps recorded from now on. Merges with earlier calls;
   * a value of `undefined` hides that variable. Records nothing itself.
   */
  watch(vars: Record<string, unknown>): void;
  /**
   * How to draw a state. Structure tracers set this so `render` needs no view.
   * Method syntax on purpose: it lets any trace be stored as `Trace<unknown>`, for example in a registry of mixed figures.
   */
  view?(state: S): string;
  /**
   * The natural width and height `view` would draw a state at, worked out without drawing it.
   * Optional: `render` uses it to size long traces quickly, and draws each step to measure it otherwise.
   */
  size?(state: S): readonly [width: number, height: number];
}
 
/** Turns one state into SVG markup. Views must escape any text they put in the markup. */
export type View<S> = (state: S) => string;
 
/** Records a snapshot of `state` at every step. The first step is the initial state. */
export function createTrace<S>(initial: S, caption = ""): Trace<S> {
  const steps: Step<S>[] = [];
  let line: number | undefined;
  let vars: Vars | undefined;
 
  const record = (op: string, state: S, caption = "") => {
    // Clone so later mutations by the algorithm don't rewrite earlier steps.
    steps.push({
      op,
      state: structuredClone(state),
      caption,
      ...(line !== undefined && { line }),
      ...(vars && { vars }),
    });
  };
  const at = (n: number) => {
    if (!Number.isInteger(n) || n < 1) throw new RangeError(`line ${n} must be a whole number from 1`);
    line = n;
  };
  // A new object per call, so steps can share it safely: it never changes after this.
  const watch = (next: Record<string, unknown>) => {
    const copy: Record<string, unknown> = {};
    for (const [name, value] of Object.entries(next)) {
      try {
        copy[name] = structuredClone(value);
      } catch {
        throw new TypeError(`watch: cannot copy "${name}"; functions, DOM nodes and class instances with private fields are not supported`);
      }
    }
    vars = { ...vars, ...copy };
  };
 
  record("init", initial, caption);
  return { steps, record, at, watch };
}