Update fidlviz for v2 wire format

This CL adds a checkbox that enables use of the v2 wire format. It is
checked by default.

Change-Id: I843af7e71d922ed7e3f0915eacca5d04212e0043
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/584462
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
diff --git a/fidlviz/index.html b/fidlviz/index.html
index 7d3784c..af2dd4a 100644
--- a/fidlviz/index.html
+++ b/fidlviz/index.html
@@ -45,9 +45,17 @@
                     <div id="OutputPart" class="split-item split-item--second">
                         <div class="part-header">
                             <h2 id="OutputTitle" class="part-title">&nbsp;</h2>
-                            <input id="OutputSlider" class="slider"
-                            type="range" min="1" max="8"
-                            value="8" list="Tickmarks">
+                            <div class="part-form">
+                                <div id="OutputV2Control">
+                                    <input id="OutputV2Checkbox" type="checkbox" checked>
+                                    <label for="OutputV2Checkbox">Use V2 wire format</label>
+                                </div>
+                                <div>
+                                    <input id="OutputSlider" class="slider"
+                                        type="range" min="1" max="8"
+                                        value="8" list="Tickmarks">
+                                </div>
+                            </div>
                         </div>
                         <div id="OutputEditor" class="editor"></div>
                     </div>
diff --git a/fidlviz/script.js b/fidlviz/script.js
index e8c47f6..330c5f3 100644
--- a/fidlviz/script.js
+++ b/fidlviz/script.js
@@ -632,6 +632,7 @@
       case "vector":
       case "enum":
       case "envelope":
+      case "pad4":
       case "box":
       case "table":
       case "union": {
@@ -653,10 +654,14 @@
           push(node.type + " {");
           newline();
           for (const field of node.fields) {
-            push(INDENT.repeat(indent + 1));
+            if (field.type !== "$out") {
+              push(INDENT.repeat(indent + 1));
+            }
             thunks.push(helper(field, indent + 1, /* topLevelObject = */ false));
-            push(",");
-            newline();
+            if (field.type !== "$out") {
+              push(",");
+              newline();
+            }
           }
           push(INDENT.repeat(indent) + "}");
         }
@@ -834,7 +839,7 @@
   node.lowered.push(lowered);
   // If there is already a loweredFrom, overwrite it. Usually this is when a
   // lowering stage copies a whole node (including its loweredFrom) field, and
-  // then calls addLowered. Ocassionally there genuinely are multiple things it
+  // then calls addLowered. Occasionally there genuinely are multiple things it
   // was loweredFrom, like bits/enums -- the whole structure and the value
   // inside all lower the same output. In these cases letting the last
   // addLowered call win works -- it will be the largest structure, the whole
@@ -869,19 +874,19 @@
 }
 
 // Helper combinator for lowering.
-function doLowering(tree, self, helper) {
+function doLowering(tree, config, self, helper) {
   function augmentedHelper(node) {
     const result = helper(node);
     if (result !== undefined) {
       // Reapply self. A proper lowering should not get into infinite recursion.
-      return self(result);
+      return self(result, config);
     }
     if ("fields" in node) {
-      return {...copyNode(node), fields: node.fields.map(self)};
+      return {...copyNode(node), fields: node.fields.map(f => self(f, config))};
     }
     // Very important that this copies the node rather than returning the same
     // object. Otherwise highlighting gets into infinite recursion.
-    return {...copyNode(node)};
+    return copyNode(node);
   }
   const lowered = augmentedHelper(tree);
   addLowered(tree, lowered);
@@ -889,8 +894,8 @@
 }
 
 // Lowering stage 1: tables and unions.
-function lower1(tree) {
-  return doLowering(tree, lower1, node => {
+function lower1(tree, config) {
+  return doLowering(tree, config, lower1, node => {
     switch (node.type) {
       case "table": {
         if (node.fields.length === 0) {
@@ -983,8 +988,8 @@
 }
 
 // Lowering stage 2: bits and enums. Assumes earlier lowerings are done.
-function lower2(tree) {
-  return doLowering(tree, lower2, node => {
+function lower2(tree, config) {
+  return doLowering(tree, config, lower2, node => {
     switch (node.type) {
       case "bits": {
         const type = (node.fields[0].signed === "u" ? "uint" : "int")
@@ -1022,8 +1027,8 @@
 }
 
 // Lowering stage 3: strings. Assumes earlier lowerings are done.
-function lower3(tree) {
-  return doLowering(tree, lower3, node => {
+function lower3(tree, config) {
+  return doLowering(tree, config, lower3, node => {
     switch (node.type) {
       case "null": {
         if (node.kind !== "string") {
@@ -1076,8 +1081,8 @@
 }
 
 // Lowering stage 4: vectors. Assumes earlier lowerings are done.
-function lower4(tree) {
-  return doLowering(tree, lower4, node => {
+function lower4(tree, config) {
+  return doLowering(tree, config, lower4, node => {
     switch (node.type) {
       case "null": {
         if (node.kind !== "vector") {
@@ -1147,7 +1152,7 @@
 }
 
 // Lowering stage 5: handles and envelopes. Assumes earlier lowerings are done.
-function lower5(tree) {
+function lower5(tree, config) {
   function countPresentHandles(node) {
     if (node.type === "handle") {
       return 1;
@@ -1162,7 +1167,7 @@
     return 0;
   }
 
-  return doLowering(tree, lower5, node => {
+  return doLowering(tree, config, lower5, node => {
     switch (node.type) {
       case "null": {
         if (node.kind !== "handle") {
@@ -1184,6 +1189,13 @@
       }
       case "envelope": {
         if (node.fields.length === 0) {
+          if (config.v2Enabled) {
+            return {
+              type: "$pointer",
+              value: "absent",
+              note: node.note,
+            };
+          }
           return {
             type: "struct",
             fields: [
@@ -1198,9 +1210,45 @@
           };
         }
         disableAddLowered = true;
-        const bytes = countTotalBytes(lowerBytes(lower6(lower5(node.fields[0]))));
+        const payload = lowerBytes(lower6(lower5(node.fields[0], config), config), config);
+        const bytes = countTotalBytes(payload);
         disableAddLowered = false;
         const handles = countPresentHandles(node.fields[0]);
+        if (config.v2Enabled) {
+          const lastField = payload.fields[payload.fields.length - 1]
+          if (bytes === 8 && lastField.padding && lastField.size >= 4) {
+            return {
+              type: "struct",
+              fields: [
+                {
+                  ...copyNode(node),
+                  type: "pad4",
+                  note: "Pad inlined envelope value to 4 bytes"
+                },
+                {type: "int", signed: "u", width: 16,
+                 orig: handles.toString(), value: handles,
+                note: `Envelope: ${handles} handle${plural(handles)}`},
+                {type: "int", signed: "u", width: 16, orig: "1", value: 1,
+                 note: "Envelope flags: inlined representation"},
+              ],
+              note: node.note,
+            };
+          }
+          return {
+            type: "struct",
+            fields: [
+              {type: "int", signed: "u", width: 32, orig: bytes.toString(), value: bytes,
+               note: `Envelope: ${bytes} byte${plural(bytes)}`},
+              {type: "int", signed: "u", width: 16, orig: handles.toString(), value: handles,
+               note: `Envelope: ${handles} handle${plural(handles)}`},
+              {type: "int", signed: "u", width: 16, orig: "0", value: 0,
+               note: "Envelope flags: out-of-line representation"},
+              {type: "$out", fields: node.fields,
+               note: "Envelope: out-of-line data"},
+            ],
+            note: node.note,
+          };
+        }
         return {
           type: "struct",
           fields: [
@@ -1218,9 +1266,9 @@
   });
 }
 
-// Lowering stage 6: boxes. Assumes earlier lowerings are done.
-function lower6(tree) {
-  return doLowering(tree, lower6, node => {
+// Lowering stage 6: arrays and boxes. Assumes earlier lowerings are done.
+function lower6(tree, config) {
+  return doLowering(tree, config, lower6, node => {
     switch (node.type) {
       case "null": {
         if (node.kind !== "struct") {
@@ -1261,8 +1309,8 @@
 }
 
 // Final lowering stage, to bytes. Assumes all earlier lowerings are done.
-function lowerBytes(tree) {
-  // object can be "primary", "secondary", or false.
+function lowerBytes(tree, config) {
+  // object can be "primary", "secondary", "pad4", or false.
   function lower(tree, object) {
     const lowered = helper(tree, object);
     addLowered(tree, lowered);
@@ -1341,6 +1389,9 @@
           fields: [lower(node.fields[0], "secondary")],
         };
       }
+      case "pad4": {
+        return lower(node.fields[0], "pad4");
+      }
       case "struct": {
         const fields = [];
         let size = 0, align = 0;
@@ -1390,7 +1441,10 @@
         }
         padToAlign(align, pad => `${pad} byte${plural(pad)} tail padding for `
           + `struct's ${pad}-byte alignment`);
-        if (object) {
+        if (object === "pad4") {
+          padToAlign(4, pad => `${pad} byte${plural(pad)} tail padding for `
+            + `object inlined in envelope`);
+        } else if (object) {
           padToAlign(8, pad => `${pad} byte${plural(pad)} tail padding for `
             + `${object} object ${align}-byte alignment`);
         }
@@ -1440,12 +1494,12 @@
 
 // Applies lowering functions from...to inclusive.
 // 7 means lowerBytes.
-function lowerFromTo(tree, from, to) {
+function lowerFromTo(tree, from, to, config) {
   const lowerings = [
     lower1, lower2, lower3, lower4, lower5, lower6, lowerBytes
   ];
   for (let i = from - 1; i < to; i++) {
-    tree = lowerings[i](tree);
+    tree = lowerings[i](tree, config);
   }
   return tree;
 }
@@ -1660,6 +1714,8 @@
   const $inputEditor = ace.edit("InputEditor", inputConfig);
   const $outputEditor = ace.edit("OutputEditor", outputConfig);
   const $outputSlider = document.getElementById("OutputSlider");
+  const $outputV2Control = document.getElementById("OutputV2Control");
+  const $outputV2Checkbox = document.getElementById("OutputV2Checkbox");
   const $outputTitle = document.getElementById("OutputTitle");
   const $outputTooltip = document.getElementById("OutputTooltip");
   const $resetButton = document.getElementById("ResetButton");
@@ -1731,9 +1787,11 @@
     if (debugEnabled) {
       $debugButton.classList.add("button--active");
       $outputSlider.max = LAST_DEBUG_OUTPUT_MODE;
+      $outputV2Control.hidden = true;
     } else {
       $debugButton.classList.remove("button--active");
       $outputSlider.max = LAST_OUTPUT_MODE;
+      $outputV2Control.hidden = false;
     }
     // Skip this at very beginning when called from restore().
     if (restoring === undefined) {
@@ -1895,6 +1953,10 @@
     const outputMode = $outputSlider.value;
     const previouslyHadError = state.error;
 
+    const config = {
+      v2Enabled: $outputV2Checkbox.checked,
+    };
+
     function getOutput(input) {
       $inputEditor.session.clearAnnotations();
       state.error = false;
@@ -1921,7 +1983,7 @@
               return stringify(tree);
           }
         }
-        const outputTree = lowerFromTo(state.tree, 1, outputMode - 1);
+        const outputTree = lowerFromTo(state.tree, 1, outputMode - 1, config);
         state.outputTree = outputTree;
         return outputMode === LAST_OUTPUT_MODE ? formatBytes(outputTree) : format(outputTree);
       } catch (ex) {
@@ -2365,6 +2427,7 @@
       outputModeWhenDebugIs = {false: $outputSlider.max, true: $outputSlider.min};
     }
     setOuputMode(outputModeWhenDebugIs[debugEnabled]);
+    $outputV2Checkbox.checked = true;
     // Hidden at first to avoid a flash from default 50% editor split to the
     // restored value. This also applies for the output slider (Debug or not
     // Debug can cause flash). So we just hide the whole container till ready.
@@ -2423,6 +2486,13 @@
     setOuputMode($outputSlider.value);
     update();
   };
+  // Update the output view whenever the v2 checkox changes.
+  $outputV2Checkbox.oninput = () => {
+    if (restoreInProgress) {
+      return;
+    }
+    update();
+  };
   // Also allow moving the slider using Ctrl-[ and Ctrl-].
   document.addEventListener("keydown", (e) => {
     let delta;
diff --git a/fidlviz/style.css b/fidlviz/style.css
index d3ffdbd..8a05d9e 100644
--- a/fidlviz/style.css
+++ b/fidlviz/style.css
@@ -96,6 +96,12 @@
     text-overflow: ellipsis;
 }
 
+.part-form {
+    display: flex;
+    gap: 20px;
+    align-items: center;
+}
+
 .button--active {
     font-weight: bold;
 }