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