// The module, as the text format writes it.
//
// No `(module ...)` wrapper unless the module is named: the reference lays an
// unnamed one out as its FIELDS, one per line, because that is what a wax
// module converts to -- a list of them, not a nested document.

///|
/// Print a module's text form.
pub fn print_module(
  m : @wasm_bin.Module,
  trivia? : @trivia.Context? = None,
) -> String raise WatError {
  // Each field with where it was written: the comments written against it are
  // found by that, and so is the conditional branch it stood in.
  let written : Array[(Sexp, @basic.Location)] = []
  let kinds : Array[FieldKind] = []
  for k, f in m.text.field_order {
    kinds.push(field_kind(f))
    let one = field(m, f)
    let loc = if k < m.text.field_locs.length() {
      m.text.field_locs[k]
    } else {
      @basic.dummy_loc
    }
    written.push((if loc.is_dummy() { one } else { At(loc, one) }, loc))
  }
  let fields = regroup(m.text.conditionals, written, kinds~)
  // A type nothing declared is NOT written. The section holds every signature
  // the lowering interned -- a block's, an import's, a `call_indirect`'s --
  // and the text format spells all of those inline where they are used. Only a
  // type the source wrote down is a field.
  //
  // The element segments minted to declare function references are different:
  // there is nowhere else to put a declarative segment, so it follows the
  // fields that were written.
  // A type the source did not write but that carries a NAME was materialised
  // for that name -- the entry exists for it alone -- so the text writes it.
  let placed_types : Map[Int, Bool] = Map([])
  for f in m.text.field_order {
    if f is FTypes(ks) {
      for k in ks {
        placed_types[k] = true
      }
    }
  }
  for k in 0.. {
      let l : Array[Sexp] = [
        Block([Atom("module"), Atom(ident(n))], KBox, true),
      ]
      for f in fields {
        l.push(f)
      }
      render(List(l), top_depth=0, trivia~) + "\n"
    }
    None => render(VBlock(fields), trivia~) + "\n"
  }
}

///|
/// A field's guard path: the conditional frames it was written inside,
/// outermost first. Each frame is a group and which branch of it.
fn guards_of(
  conds : Array[@wasm_bin.CondGroup],
  loc : @basic.Location,
) -> Array[(Int, Bool)] {
  fn covers(outer : @basic.Location, inner : @basic.Location) -> Bool {
    inner.start.cnum >= outer.start.cnum && inner.end.cnum <= outer.end.cnum
  }

  let out : Array[(Int, Bool)] = []
  for k, c in conds {
    if covers(c.then_, loc) {
      out.push((k, true))
    } else if c.else_ is Some(e) && covers(e, loc) {
      out.push((k, false))
    }
  }
  // Outermost first: an enclosing group starts before the ones inside it.
  out.sort_by((a, b) => conds[a.0].loc.start.cnum - conds[b.0].loc.start.cnum)
  out
}

///|
/// Whether two guard paths can never both hold.
///
/// Read in lockstep from the root: at the first frame where the CONDITIONS
/// agree and the branches do not, the two leaves are on opposite sides of one
/// question and never coexist. Once the conditions differ the leaves are in
/// unrelated subtrees and nothing deeper is shared, so this is not
/// satisfiability -- `debug` and `not(debug)` count as independent -- it only
/// keeps one branch's definition from counting against the other branch's
/// import.
fn mutually_exclusive(
  conds : Array[@wasm_bin.CondGroup],
  g1 : Array[(Int, Bool)],
  g2 : Array[(Int, Bool)],
) -> Bool {
  let n = if g1.length() < g2.length() { g1.length() } else { g2.length() }
  for k in 0.. Array[Sexp] {
  if conds.is_empty() {
    return items.map(i => i.0)
  }
  // The frames an ENCLOSING level already wrote are not written again here.
  // A `#[if]` holding one block holds everything in that block too, and
  // wrapping at both levels says it twice.
  let paths = items.map(i => {
    let g = guards_of(conds, i.1)
    if g.length() <= outer.length() {
      []
    } else {
      g[outer.length():].to_owned()
    }
  })
  let order = Array::makei(items.length(), k => k)
  if needs_reorder(conds, kinds, paths) {
    let imports = order.filter(k => k < kinds.length() && kinds[k] is KImport)
    let others = order.filter(k => !(k < kinds.length() && kinds[k] is KImport))
    order.clear()
    for k in imports {
      order.push(k)
    }
    for k in others {
      order.push(k)
    }
  }
  build(conds, items, paths, order, 0, 0, order.length())
}

///|
/// Whether any import is written after a definition it could coexist with.
fn needs_reorder(
  conds : Array[@wasm_bin.CondGroup],
  kinds : Array[FieldKind],
  paths : Array[Array[(Int, Bool)]],
) -> Bool {
  let defs : Array[Array[(Int, Bool)]] = []
  for k in 0..
        for d in defs {
          if !mutually_exclusive(conds, d, paths[k]) {
            return true
          }
        }
      KDefine => defs.push(paths[k])
      KOther => ()
    }
  }
  false
}

///|
/// Build the fields at one guard depth: a run sharing a frame becomes one
/// `(@if ..)`, and a `then` run immediately followed by the matching `else`
/// run becomes the two-clause form they were written as.
fn build(
  conds : Array[@wasm_bin.CondGroup],
  items : Array[(Sexp, @basic.Location)],
  paths : Array[Array[(Int, Bool)]],
  order : Array[Int],
  depth : Int,
  from : Int,
  to : Int,
) -> Array[Sexp] {
  let out : Array[Sexp] = []
  let mut i = from
  while i < to {
    let g = paths[order[i]]
    if g.length() <= depth {
      out.push(items[order[i]].0)
      i = i + 1
      continue
    }
    let frame = g[depth]
    let mut j = i
    while j < to &&
          paths[order[j]].length() > depth &&
          paths[order[j]][depth] == frame {
      j = j + 1
    }
    let inner = build(conds, items, paths, order, depth + 1, i, j)
    // The other branch of the same group, if it is the very next run: that is
    // the shape it was written in, and splitting it would say the same thing
    // twice.
    let mut k = j
    let mut other : Array[Sexp] = []
    if frame.1 && j < to && paths[order[j]].length() > depth {
      let next = paths[order[j]][depth]
      if next.0 == frame.0 && !next.1 {
        while k < to &&
              paths[order[k]].length() > depth &&
              paths[order[k]][depth] == next {
          k = k + 1
        }
        other = build(conds, items, paths, order, depth + 1, j, k)
      }
    }
    if other.is_empty() && k == j {
      out.push(
        if frame.1 {
          cond_field(conds[frame.0], inner, [])
        } else {
          cond_else_only(conds[frame.0], inner)
        },
      )
      i = j
    } else {
      out.push(cond_field(conds[frame.0], inner, other))
      i = k
    }
  }
  out
}

///|
/// Whether a type is the array a bare string literal already lowers to: a
/// MUTABLE byte array. Anything else has to be named, because the spelling
/// alone would not say which it is.
fn default_string_type(m : @wasm_bin.Module, t : Int) -> Bool {
  guard m.types.get(t) is Some(sub) else { return false }
  guard sub.composite is Array(at) else { return false }
  at.element.mut_ && at.element.typ is Packed(I8)
}

///|
/// `(@if $c (@then) (@else ...))` -- an else run with no then beside it.
fn cond_else_only(c : @wasm_bin.CondGroup, else_ : Array[Sexp]) -> Sexp {
  At(
    c.loc,
    List([
      block([Atom("@if"), Atom(c.cond)]),
      List([Atom("@then")]),
      List([Atom("@else"), ..else_]),
    ]),
  )
}

///|
/// `(@if $cond (@then ...) (@else ...))`.
fn cond_field(
  c : @wasm_bin.CondGroup,
  then_ : Array[Sexp],
  else_ : Array[Sexp],
) -> Sexp {
  let l : Array[Sexp] = [block([Atom("@if"), Atom(c.cond)])]
  l.push(List([Atom("@then"), ..then_]))
  if !else_.is_empty() {
    l.push(List([Atom("@else"), ..else_]))
  }
  At(c.loc, List(l))
}

///|
/// What a field is, for the import hoist.
priv enum FieldKind {
  KImport
  KDefine
  KOther
}

///|
fn field_kind(f : @wasm_bin.FieldRef) -> FieldKind {
  match f {
    FImport(_) => KImport
    // What wasm requires an import to precede.
    FFunc(_) | FMemory(_) | FTable(_) | FTag(_) | FGlobal(_) => KDefine
    _ => KOther
  }
}

///|
/// One module field, written where the source put it.
fn field(m : @wasm_bin.Module, f : @wasm_bin.FieldRef) -> Sexp raise WatError {
  match f {
    // A `rec` group is one field holding several definitions; a lone type is
    // written bare, because a singleton `rec` is a DIFFERENT type and the
    // source said which it wrote.
    FTypes([k]) => subtype_field(m, k)
    FTypes(ks) => {
      let l : Array[Sexp] = [Atom("rec")]
      for k in ks {
        l.push(subtype_field(m, k))
      }
      List(l)
    }
    FFeature(name) => List([Atom("@feature"), Atom(quoted(name))])
    FStart(f) => List([Atom("start"), Atom(name_or(m.names.functions, f))])
    FExport(k) => export_field(m, k)
    FImport(k) => import_field(m, k)
    FFunc(k) => function_field(m, k, m.codes[k])
    FTable(k) => table_field(m, k)
    FMemory(k) => memory_field(m, k)
    FGlobal(k) => global_field(m, k)
    FTag(k) => tag_field(m, k)
    FElem(k) => elem_field(m, k)
    FData(k) => data_field(m, k)
  }
}

///|
/// One function: its name, its inline signature, its locals, its body.
///
/// The signature is written INLINE -- `(param $x i32) (result i32)` -- rather
/// than as a type reference, which is what the reference does and what keeps
/// the text readable without the type section beside it.
fn function_field(
  m : @wasm_bin.Module,
  k : Int,
  code : @wasm_bin.FunctionCode,
) -> Sexp raise WatError {
  let index = k + imported_count(m, "func")
  let locals = m.names.locals.get(index).unwrap_or(Map([]))
  let ft = match m.types[m.funcs[k]].composite {
    Func(ft) => ft
    _ => raise NotPrinted("a function whose type is not a function type")
  }
  // The head -- name, exports, signature -- breaks as one group, so a long
  // signature wraps under the name rather than each clause taking a line.
  let head : Array[Sexp] = [
    Atom("func"),
    Atom(name_or(m.names.functions, index)),
  ]
  for e in export_clauses(m, Func(index)) {
    head.push(e)
  }
  // `fn f: ft(..)` named a type, and the text names it rather than repeating
  // the signature that declaration pointed at.
  let wrote = m.text.decl_typeuse.get(OwnerFunc(index))
  let by_name = wrote is Some({ named: true, .. })
  if by_name {
    head.push(List([Atom("type"), Atom(name_or(m.names.types, m.funcs[k]))]))
  }
  // The signature is a group of its own, so it moves to its own line as a
  // unit when the name and exports have already filled one -- rather than the
  // first few parameters trailing the name and the rest wrapping.
  let sign : Array[Sexp] = []
  // All unnamed: ONE `(param i32 i32)` group. Any named: one group each,
  // because a name belongs to its own parameter and there is nowhere in the
  // shared form to put it.
  let mut any_named = false
  for p, _ in ft.params {
    if local_name(locals, p) != "" {
      any_named = true
    }
  }
  if by_name && !(wrote is Some({ spelled: true, .. })) {
    // Named and nothing else written: the signature is the type's, and
    // repeating it here would say the same thing twice.
    ()
  } else if any_named {
    for p, ty in ft.params {
      sign.push(
        List(
          named_type(local_name(locals, p), "param", valtype(ty, m.names.types)),
        ),
      )
    }
  } else if !ft.params.is_empty() {
    let g : Array[Sexp] = [Atom("param")]
    for ty in ft.params {
      g.push(Atom(valtype(ty, m.names.types)))
    }
    sign.push(List(g))
  }
  if (!by_name || wrote is Some({ spelled: true, .. })) &&
    !ft.results.is_empty() {
    let r : Array[Sexp] = [Atom("result")]
    for ty in ft.results {
      r.push(Atom(valtype(ty, m.names.types)))
    }
    sign.push(List(r))
  }
  if !sign.is_empty() {
    head.push(block(sign))
  }
  let out : Array[Sexp] = [block(head)]
  // The compilation hint belongs to the FUNCTION rather than to any
  // instruction, and the section keys it at offset zero -- which is why it is
  // written here, before the locals, rather than on the first statement.
  if code.priority is Some(p) {
    let l : Array[Sexp] = [
      Atom("@metadata.code.compilation_priority"),
      List([Atom("priority"), Atom(p.compilation.to_string())]),
    ]
    if p.optimization is Some(o) {
      // `run_once` is the reserved value, and prints as its keyword.
      l.push(
        if o == 127 {
          List([Atom("run_once")])
        } else {
          List([Atom("optimization"), Atom(o.to_string())])
        },
      )
    }
    out.push(List(l))
  }
  // The DECLARED locals follow the parameters in the same index space, so
  // their names are looked up past the parameter count. They pack onto as few
  // lines as they fit on, which is what `hov` means.
  if !code.locals.is_empty() {
    let l : Array[Sexp] = []
    for j, ty in code.locals {
      l.push(
        List(
          named_type(
            local_name(locals, ft.params.length() + j),
            "local",
            valtype(ty, m.names.types),
          ),
        ),
      )
    }
    out.push(Block(l, KHov, false))
  }
  let ctx = {
    m,
    locals,
    labels: m.names.labels.get(index).unwrap_or(Map([])),
    opened: 0,
    scope: [],
    conditionals: code.conditionals,
  }
  for
    st in statements(
      fold(code.body, code.spans, nested=code.nested_spans),
      ctx,
      code.conditionals,
    ) {
    out.push(st)
  }
  List(out)
}

///|
/// `(param $x i32)`, or `(param i32)` when nothing named it.
fn named_type(name : String, kind : String, ty : String) -> Array[Sexp] {
  if name == "" {
    [Atom(kind), Atom(ty)]
  } else {
    [Atom(kind), Atom(name), Atom(ty)]
  }
}

///|
/// A local's name, or nothing.
fn local_name(locals : Map[Int, Bytes], k : Int) -> String {
  match locals.get(k) {
    Some(b) => ident(b)
    None => ""
  }
}

///|
/// What printing one function body needs to know beyond the tree.
///
/// `opened` counts the blocks in the order they OPEN, which is the order the
/// name section numbers their labels in; `scope` is the same blocks stacked
/// innermost-last, which is what a branch depth counts against. The two differ
/// -- a name belongs to its block, a depth to where the branch stands -- so
/// both are carried.
priv struct Ctx {
  m : @wasm_bin.Module
  locals : Map[Int, Bytes]
  labels : Map[Int, Bytes]
  mut opened : Int
  scope : Array[String?]
  /// The `#[if(..)]` groups written in this body, for the statements at every
  /// level of it -- a conditional can hold a block whose body holds another.
  conditionals : Array[@wasm_bin.CondGroup]
}

///|
/// A run of statements, with the ones written inside a `#[if(..)]` put back
/// under it. Every level of a body goes through here: a conditional can sit
/// inside a block whose body has its own.
fn statements(
  nodes : Array[Node],
  ctx : Ctx,
  conds : Array[@wasm_bin.CondGroup],
  outer? : Array[(Int, Bool)] = [],
) -> Array[Sexp] raise WatError {
  let items : Array[(Sexp, @basic.Location)] = []
  for n in nodes {
    items.push((folded(n, ctx), n.loc))
  }
  regroup(conds, items, outer~)
}

///|
/// One folded instruction: `(head operand operand)`, or just `(head)` when it
/// takes none.
///
/// A structural instruction is written the same way, except that what follows
/// the head is the code it guards rather than values it consumes.
fn folded(node : Node, ctx : Ctx) -> Sexp raise WatError {
  let out = folded_bare(node, ctx)
  // Where the source wrote it, so a comment against it can be found. A node
  // no span covered was not written anywhere in particular.
  if node.loc.is_dummy() {
    out
  } else {
    At(node.loc, out)
  }
}

///|
fn folded_bare(node : Node, ctx : Ctx) -> Sexp raise WatError {
  // A string literal is written as one: the constants it lowered to are the
  // string, so writing both would say the same thing twice.
  if node.head is FromString(bytes, inner) {
    // The type is written only when the SOURCE named one. Naming it whenever
    // the type HAS a name was measured and is much worse -- 1611 exact to
    // 1559 -- because most strings land in a named type the reference leaves
    // out, so which was written has to be recorded rather than inferred.
    let l : Array[Sexp] = [Atom("@string")]
    // The DEFAULT string is a mutable byte array, and `(@string "..")` already
    // says that -- so only a string at some other element type names one.
    let t = match inner {
      ArrayNewFixed(t, _) => Some(t)
      _ => None
    }
    if t is Some(t) && !default_string_type(ctx.m, t) {
      l.push(Atom(name_or(ctx.m.names.types, t)))
    }
    l.push(Atom(quoted(bytes)))
    return List(l)
  }
  if node.head is FromChar(bytes, _) {
    return List([Atom("@char"), Atom(quoted(bytes))])
  }
  // A hinted instruction prints its annotations BEFORE itself, outside the
  // parentheses of the group it heads -- the hint is about the instruction,
  // not an operand of it. The wrapper is transparent so the annotation and
  // the group it labels break together.
  if node.head is Hinted(h, inner) {
    let annots = hint_annotations(h, ctx.m.names)
    if !annots.is_empty() {
      annots.push(folded({ ..node, head: inner }, ctx))
      return Block(annots, KBox, true)
    }
    return folded({ ..node, head: inner }, ctx)
  }
  if !node.bodies.is_empty() ||
    node.head is (Block(_, _) | Loop(_, _) | If(_, _, _)) {
    return structural(node, ctx)
  }
  // The head and its operands are ONE fill group, so they pack onto as many
  // lines as they need. They are not children of the list itself: those break
  // all together, which is right for a body's statements and wrong for a
  // folded instruction's operands.
  let head = mnemonic(node.head, ctx.m.names, ctx.locals, scope=ctx.scope)
  let l : Array[Sexp] = [head]
  for operand in node.operands {
    l.push(folded(operand, ctx))
  }
  List([Block(l, KBox, true)])
}

///|
/// `(block $l (result T) body...)`, and the two that shape their bodies
/// differently: `if`, whose operand comes before two named clauses.
fn structural(node : Node, ctx : Ctx) -> Sexp raise WatError {
  // The label is claimed BEFORE the body is printed, because that is the order
  // the blocks open in and so the order the names are numbered in.
  let opened = ctx.opened
  ctx.opened = opened + 1
  let name = match ctx.labels.get(opened) {
    Some(b) => Some(ident(b))
    None => None
  }
  let (word, bt) = match node.head {
    Block(bt, _) => ("block", bt)
    Loop(bt, _) => ("loop", bt)
    If(bt, _, _) => ("if", bt)
    TryTable(bt, _, _) => ("try_table", bt)
    LegacyTry(bt, _, _, _) => ("try", bt)
    _ => raise NotPrinted(constructor_of(node.head))
  }
  let head : Array[Sexp] = [Atom(word)]
  if name is Some(n) {
    head.push(Atom(n))
  }
  for t in blocktype(bt, ctx.m) {
    head.push(t)
  }
  // The `if` condition is an operand, so it joins the head's group; the
  // clauses that follow are list children and break onto their own lines.
  let l : Array[Sexp] = if node.operands.is_empty() {
    [block(head)]
  } else {
    let g : Array[Sexp] = [block(head)]
    for operand in node.operands {
      g.push(folded(operand, ctx))
    }
    [Block(g, KBox, true)]
  }
  // A `try_table`'s handlers are IMMEDIATES that branch OUT of it, so their
  // labels are read in the enclosing scope -- the try_table's own label is in
  // scope for its body and not for its handlers. Resolved before the push, not
  // after, or every handler names one block too deep.
  if node.head is TryTable(_, handlers, _) {
    let hs : Array[Sexp] = []
    for h in handlers {
      hs.push(catch_clause(h, ctx))
    }
    l.push(block(hs))
  }
  ctx.scope.push(name)
  // The legacy `try`'s handlers are inline BODIES that run in place, so each
  // is a named clause around the code it guards.
  let clauses = match node.head {
    If(_, _, _) => ["then", "else"]
    LegacyTry(_, _, catches, all) => {
      let names : Array[String] = ["do"]
      for c in catches {
        names.push("catch " + name_or(ctx.m.names.tags, c.0))
      }
      if all is Some(_) {
        names.push("catch_all")
      }
      names
    }
    _ => []
  }
  for j, body in node.bodies {
    let inner = statements(
      body,
      ctx,
      ctx.conditionals,
      outer=guards_of(ctx.conditionals, node.loc),
    )
    if j < clauses.length() {
      // An `else` with nothing in it is not written. `then` always is, even
      // empty, because it is what the condition guards.
      if !(clauses[j] == "else" && inner.is_empty()) {
        let c : Array[Sexp] = [Atom(clauses[j])]
        for x in inner {
          c.push(x)
        }
        l.push(List(c))
      }
    } else {
      for x in inner {
        l.push(x)
      }
    }
  }
  let _ = ctx.scope.pop()
  List(l)
}

///|
/// What a block produces, when it produces anything.
fn blocktype(bt : @wasm_bin.BlockType, m : @wasm_bin.Module) -> Array[Sexp] {
  match bt {
    Empty => []
    Value(t) => [List([Atom("result"), Atom(valtype(t, m.names.types))])]
    MultiValue(ts) | InlineType([], ts) => {
      let out : Array[Sexp] = []
      for t in ts {
        out.push(List([Atom("result"), Atom(valtype(t, m.names.types))]))
      }
      out
    }
    // A block's signature is spelled OUT, parameters and all: the text format
    // lets a block carry a whole function type inline, so there is never a
    // need to name one. Naming it whenever the type HAD a name was measured
    // and is worse, and so was naming it as soon as the block consumed
    // anything.
    TypeIndex(k) => typeuse(m, k)
    InlineType(params, results) => {
      let out : Array[Sexp] = []
      let p : Array[Sexp] = [Atom("param")]
      for t in params {
        p.push(Atom(valtype(t, m.names.types)))
      }
      out.push(List(p))
      let r : Array[Sexp] = [Atom("result")]
      for t in results {
        r.push(Atom(valtype(t, m.names.types)))
      }
      out.push(List(r))
      out
    }
  }
}

///|
/// One `try_table` handler: which tag it catches and where it branches to.
fn catch_clause(h : @wasm_bin.CatchHandler, ctx : Ctx) -> Sexp {
  let tags = ctx.m.names.tags
  match h {
    Catch(t, l) =>
      List([Atom("catch"), Atom(name_or(tags, t)), Atom(label(ctx.scope, l))])
    CatchRef(t, l) =>
      List([
        Atom("catch_ref"),
        Atom(name_or(tags, t)),
        Atom(label(ctx.scope, l)),
      ])
    CatchAll(l) => List([Atom("catch_all"), Atom(label(ctx.scope, l))])
    CatchAllRef(l) => List([Atom("catch_all_ref"), Atom(label(ctx.scope, l))])
  }
}

///|
/// The `(@metadata.code.*)` annotations an instruction's hints print as, in
/// the order the sections carry them.
///
/// A hint has to round-trip EXACTLY -- it is a byte in a custom section, not
/// advice -- so each one prints in the structured form when that form can
/// name the same byte, and as the raw payload when it cannot.
fn hint_annotations(
  h : @wasm_bin.InstrHints,
  names : @wasm_bin.Names,
) -> Array[Sexp] {
  let out : Array[Sexp] = []
  if h.branch is Some(likely) {
    out.push(
      List([
        Atom("@metadata.code.branch_hint"),
        Atom(if likely { "\"\\01\"" } else { "\"\\00\"" }),
      ]),
    )
  }
  if h.freq is Some(b) {
    out.push(List([Atom("@metadata.code.instr_freq"), freq_payload(b)]))
  }
  if h.targets is Some(ts) {
    let l : Array[Sexp] = [Atom("@metadata.code.call_targets")]
    for t in ts {
      l.push(
        List([
          Atom("target"),
          Atom(name_or(names.functions, t.0)),
          Atom(percent(t.1)),
        ]),
      )
    }
    out.push(List(l))
  }
  out
}

///|
/// A frequency hint's payload. The two reserved bytes print as their keywords;
/// otherwise the byte stands for a base-2 executions-per-call ratio, written
/// as `(freq n)` when that ratio is a whole number -- the hot-instruction case
/// -- and as the raw byte when it is not, because a fractional ratio's
/// shortest decimal need not re-encode to the same byte.
fn freq_payload(b : Int) -> Sexp {
  if b == 0 {
    return List([Atom("never_opt")])
  }
  if b == 127 {
    return List([Atom("always_opt")])
  }
  if b >= 32 && b <= 64 {
    let mut r = 1L
    for _ in 0..<(b - 32) {
      r = r * 2L
    }
    return List([Atom("freq"), Atom(r.to_string())])
  }
  Atom("\"\\" + hex2(b & 0xFF) + "\"")
}

///|
/// A call-target share, as the fraction the annotation writes: `73` is `0.73`,
/// and a whole one is written without a point.
fn percent(pct : Int) -> String {
  if pct % 100 == 0 {
    return (pct / 100).to_string()
  }
  let whole = pct / 100
  let frac = pct % 100
  if frac % 10 == 0 {
    whole.to_string() + "." + (frac / 10).to_string()
  } else {
    whole.to_string() +
    "." +
    (if frac < 10 { "0" } else { "" }) +
    frac.to_string()
  }
}