1class State {
2  constructor(wat, clif, asm) {
3    this.wat = wat;
4    this.clif = clif;
5    this.asm = asm;
6  }
7}
8
9const state = (window.STATE = new State(window.WAT, window.CLIF, window.ASM));
10
11/*** LRU Cache *****************************************************************/
12
13class LruCache {
14  constructor(maxSize, getFunc) {
15    // Maps preserve the insertion order, so we can use it to implement a naïve LRU
16    // cache.
17    this.cache = new Map();
18    this.maxSize = maxSize;
19    this.getFunc = getFunc;
20  }
21
22  get(key) {
23    let v = this.cache.get(key);
24    if (v !== undefined) {
25      // Remove the found element from the cache so it can be inserted it again
26      // at the end before returning.
27      this.cache.delete(key);
28    } else {
29      v = this.getFunc(key);
30      if (this.cache.size > this.cache.maxSize) {
31        // Evict the oldest item from the cache.
32        this.cache.delete(this.cache.keys().next().value);
33      }
34    }
35    this.cache.set(key, v);
36    return v;
37  }
38}
39
40/*** Colors for Offsets **********************************************************/
41
42const rgbToLuma = rgb => {
43  // Use the NTSC color space (https://en.wikipedia.org/wiki/YIQ) to determine
44  // the luminance (Y) of this color.  (This is an approximation using powers of two,
45  // to avoid multiplications and divisions. It's not accurate, but it's good enough
46  // for our purposes.)
47  let [r, g, b] = rgbToTriple(rgb);
48  return (((r << 8) + (g << 9) + (b << 7)) >> 10) + (g & 31);
49};
50
51// Convert a color as a 24-bit number into a list with 3 elements: R, G, and B,
52// each ranging [0, 255].
53const rgbToTriple = rgb => [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];
54
55// Use CRC24 as a way to calculate a color for a given Wasm offset. This
56// particular algorithm has been chosen because it produces bright, vibrant
57// colors, that don't repeat often, and is easily implementable.
58const calculateRgbForOffset = offset => {
59  const crc24 = (crc, byte) => {
60    // CRC computation adapted from Wikipedia[1] (shift-register based division versions.)
61    // [1] https://en.m.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks
62    crc ^= byte << 16;
63    for (let bit = 0; bit < 8; bit++) {
64      crc = ((crc << 1) ^ (crc & 0x800000 ? 0xfa5711 : 0)) & 0xffffff;
65    }
66    return crc;
67  };
68
69  // Feed the offset into the CRC24 algorithm, one byte at a time.
70  let color = offset;
71  while (offset) {
72    color = crc24(color, offset & 0xff);
73    offset >>= 8;
74  }
75
76  // Avoid colors that are too close to white.  Flip some bits around
77  // so that the color components are more pronounced.
78  return rgbToLuma(color) > 200 ? color ^ 0xa5a5a5 : color;
79};
80
81// Memoize all colors for a given Wasm offset. Cache isn't used here since,
82// when rendering the Wat side, we use the fact that if a color has not been
83// assigned during the rendering of the Native Asm side, that block of Wasm
84// instructions isn't colored.
85let offsetToRgb = new Map();
86const rgbForOffset = offset => {
87  let rgb = offsetToRgb.get(offset);
88  if (rgb === undefined) {
89    rgb = calculateRgbForOffset(offset);
90    offsetToRgb.set(offset, rgb);
91  }
92  return rgb;
93};
94
95// Convert a color in a 24-bit number to a string suitable for CSS styling.
96const rgbToCss = rgb => `rgba(${rgbToTriple(rgb).join(",")})`;
97
98// Darkens a color in a 24-bit number slightly by subtracting at most 0x20
99// from each color component; e.g. RGB(175, 161, 10) becomes RGB(143, 129, 0).
100// This loses some color information, but it's good enough for our use case here.
101const rgbDarken = rgb => {
102  let [r, g, b] = rgbToTriple(rgb);
103  return (
104    ((r - Math.min(r, 0x20)) << 16) |
105    ((g - Math.min(g, 0x20)) << 8) |
106    (b - Math.min(b, 0x20))
107  );
108};
109
110// Adjust the color styles of a DOM element for a given Wasm offset.
111const adjustColorForOffset = (element, offset) => {
112  let backgroundColor = rgbForOffset(offset);
113  element.style.backgroundColor = rgbToCss(backgroundColor);
114  element.classList.add(
115    rgbToLuma(backgroundColor) > 128 ? "dark-text" : "light-text",
116  );
117};
118
119/*** Event Handlers ************************************************************/
120
121// Connects callbacks to mouse hovering events so elements are properly highlighted when
122// hovered, and the bridging element is drawn between the instruction lists.
123const linkedElementCache = new LruCache(256, offset =>
124  document.querySelectorAll(`[data-wasm-offset="${offset}"]`),
125);
126
127const eachElementWithSameWasmOff = (event, closure) => {
128  let offset = event.target.dataset.wasmOffset;
129  if (offset !== null) {
130    // Run the loop inside an animation frame.  Since we're modifying the DOM,
131    // do so when the browser has some breathing room.
132    window.requestAnimationFrame(() => {
133      linkedElementCache.get(offset).forEach(closure);
134    });
135  }
136};
137
138const linkElements = element => {
139  element.addEventListener(
140    "click",
141    event => {
142      eachElementWithSameWasmOff(event, elem => {
143        if (elem === event.target) return; // Only scroll into view the other elements.
144        elem.scrollIntoView({
145          behavior: "smooth",
146          block: "center",
147          inline: "nearest",
148        });
149      });
150    },
151    { passive: true },
152  );
153
154  element.addEventListener("mouseenter", event => {
155    let offset = event.target.dataset.wasmOffset;
156    if (offset === null) return;
157
158    // Gather all elements related to the desired offset.
159    let elems = linkedElementCache.get(offset);
160
161    // Perform the DOM modification inside an animation frame to give the browser a bit of
162    // a breathing room.
163    window.requestAnimationFrame(() => {
164      // Draw a 2px dark outline in each block of instructions so it stands out a bit better
165      // when hovered.
166      let outline = `2px solid ${rgbToCss(rgbDarken(rgbForOffset(offset)))}`;
167      for (const elem of elems) {
168        elem.setAttribute("title", `Wasm offset @ ${offset}`);
169        elem.classList.add("hovered");
170        elem.style.outline = outline;
171      }
172    });
173  });
174
175  element.addEventListener("mouseleave", event => {
176    eachElementWithSameWasmOff(event, elem => {
177      elem.removeAttribute("title");
178      elem.classList.remove("hovered");
179      elem.style.outline = "";
180    });
181  });
182};
183
184/*** Rendering *****************************************************************/
185
186const repeat = (s, n) => {
187  return s.repeat(n >= 0 ? n : 0);
188};
189
190const renderAddress = addr => {
191  let hex = addr.toString(16);
192  return repeat("0", 8 - hex.length) + hex;
193};
194
195const renderBytes = bytes => {
196  let s = "";
197  for (let i = 0; i < bytes.length; i++) {
198    if (i != 0) {
199      s += " ";
200    }
201    const hexByte = bytes[i].toString(16);
202    s += hexByte.length == 2 ? hexByte : "0" + hexByte;
203  }
204  return s + repeat(" ", 30 - s.length);
205};
206
207const renderInst = (mnemonic, operands) => {
208  if (operands.length == 0) {
209    return mnemonic;
210  } else {
211    return mnemonic + " " + operands;
212  }
213};
214
215const createDivForCode = () => {
216  let div = document.createElement("div");
217  div.classList.add("highlight");
218  return div;
219};
220
221// Render the CLIF (if any).
222const clifElem = document.getElementById("clif");
223if (clifElem) {
224  for (const func of state.clif.functions) {
225    const funcElem = document.createElement("div");
226
227    const funcHeader = document.createElement("h3");
228    let func_name =
229      func.name === null ? `function[${func.func_index}]` : func.name;
230    let demangled_name =
231      func.demangled_name !== null ? func.demangled_name : func_name;
232    funcHeader.textContent = `Intermediate Representation of function <${demangled_name}>:`;
233    funcHeader.title = `Function ${func.func_index}: ${func_name}`;
234    funcElem.appendChild(funcHeader);
235
236    for (const inst of func.instructions) {
237      const instElem = createDivForCode();
238      instElem.textContent = `${inst.clif}\n`;
239      if (inst.wasm_offset != null) {
240        instElem.dataset.wasmOffset = inst.wasm_offset;
241        adjustColorForOffset(instElem, inst.wasm_offset);
242        linkElements(instElem);
243      }
244      funcElem.appendChild(instElem);
245    }
246
247    clifElem.appendChild(funcElem);
248  }
249}
250
251// Render the ASM.
252const asmElem = document.getElementById("asm");
253for (const func of state.asm.functions) {
254  const funcElem = document.createElement("div");
255
256  const funcHeader = document.createElement("h3");
257  let functionName =
258    func.name === null ? `function[${func.func_index}]` : func.name;
259  let demangledName =
260    func.demangled_name !== null ? func.demangled_name : functionName;
261  funcHeader.textContent = `Disassembly of function <${demangledName}>:`;
262  funcHeader.title = `Function ${func.func_index}: ${functionName}`;
263  funcElem.appendChild(funcHeader);
264
265  let currentBlock = createDivForCode();
266  let disasmBuffer = [];
267  let lastOffset = null;
268
269  const addCurrentBlock = offset => {
270    currentBlock.dataset.wasmOffset = offset;
271
272    if (offset !== null) {
273      adjustColorForOffset(currentBlock, offset);
274      linkElements(currentBlock);
275    }
276
277    currentBlock.innerText = disasmBuffer.join("\n");
278    funcElem.appendChild(currentBlock);
279    disasmBuffer = [];
280  };
281
282  for (const inst of func.instructions) {
283    if (lastOffset !== inst.wasm_offset) {
284      addCurrentBlock(lastOffset);
285      currentBlock = createDivForCode();
286      lastOffset = inst.wasm_offset;
287    }
288    disasmBuffer.push(
289      `${renderAddress(inst.address)}    ${renderBytes(inst.bytes)}    ${renderInst(inst.mnemonic, inst.operands)}`,
290    );
291  }
292  addCurrentBlock(lastOffset);
293
294  asmElem.appendChild(funcElem);
295}
296
297// Render the WAT.
298const watElem = document.getElementById("wat");
299for (const chunk of state.wat.chunks) {
300  if (chunk.wasm_offset === null) continue;
301  const block = createDivForCode();
302  block.dataset.wasmOffset = chunk.wasm_offset;
303  block.innerText = chunk.wat;
304
305  if (offsetToRgb.get(chunk.wasm_offset) !== undefined) {
306    adjustColorForOffset(block, chunk.wasm_offset);
307    linkElements(block);
308  }
309
310  watElem.appendChild(block);
311}
312