docs: function replace(node, {enter, leave}) {
      let rep = (enter && enter(node)) || node
  
      switch (rep.type) {
          // regular non-leaf nodes
          case 'Apply':
              for (let i = 0; i < rep.args.length; i++) {
                  const arg = rep.args[i]
                  rep.args[i] = replace(arg, {enter, leave})
              }
              break
  
          // Skip leaf nodes because they're handled by the enter/leave calls at
          // the start/end of replace.
          case 'Identifier':
          case 'Number':
          case 'Ellipsis':
              break
  
          // irregular non-leaf nodes
          case 'Parentheses':
              rep.body = replace(rep.body, {enter, leave})
              break
  
          case 'List':
          case 'Sequence':
              for (let i = 0; i < rep.items.length; i++) {
                  const item = rep.items[i]
                  rep.items[i] = replace(item, {enter, leave})
              }
              break
  
          case 'System':
              for (let i = 0; i < rep.relations.length; i++) {
                  const rel = rep.relations[i]
                  rep.relations[i] = replace(rel, {enter, leave})
              }
              break
  
          case 'Placeholder':
              // TODO(kevinb) handle children of the placeholder
              // e.g. we there might #a_0 could match x_0, y_0, z_0, etc.
              break
  
          default:
              throw new Error('unrecognized node')
      }
  
      return (leave && leave(rep)) || rep
  }